1<!DOCTYPE html><!-- This site was created in Webflow. https://webflow.com --><!-- Last Published: Sat Oct 03 2026 13:28:24 GMT+0000 (Coordinated Universal Time) --><html data-wf-domain="redentrenadores-268bb8.webflow.io" data-wf-page="68c5a72ac71fa3f37858fcdc" data-wf-site="68c5a729c71fa3f37858fbbe" lang="es"><head><meta charset="utf-8"/><link href="https://cdn.prod.website-files.com" rel="preconnect" crossorigin="anonymous"/><title>Encuentra entrenadores personales cerca de ti | RedEntrenadores</title><meta content="Entrenadores personales en España para todos los niveles. Filtra por ciudad, objetivos y precio. Lee reseñas reales y contacta gratis sin compromiso." name="description"/><meta content="Encuentra entrenadores personales cerca de ti | RedEntrenadores" property="og:title"/><meta content="Entrenadores personales en España para todos los niveles. Filtra por ciudad, objetivos y precio. Lee reseñas reales y contacta gratis sin compromiso." property="og:description"/><meta content="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68cf1d169359063e6094caad_logo-256x256.png" property="og:image"/><meta content="Encuentra entrenadores personales cerca de ti | RedEntrenadores" name="twitter:title"/><meta content="Entrenadores personales en España para todos los niveles. Filtra por ciudad, objetivos y precio. Lee reseñas reales y contacta gratis sin compromiso." name="twitter:description"/><meta property="og:type" content="website"/><meta content="summary_large_image" name="twitter:card"/><meta content="width=device-width, initial-scale=1" name="viewport"/><meta content="Ibl2YfMxHx18YwgwBo_Cf7Y0noaXDXPp8qxAux6bR7I" name="google-site-verification"/><meta content="Webflow" name="generator"/><link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/css/redentrenadores-268bb8.webflow.shared.5fdd5e548.min.css" rel="stylesheet" type="text/css" integrity="sha384-X91eVImZbdbFw8t2Y6QKDl7HUD/gaoQ44rfY64Qq+p7zJMJt635EzDj9JzMjxLwR" crossorigin="anonymous"/><link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/css/redentrenadores-268bb8.webflow.68c5a72ac71fa3f37858fcdc.e54952729.opt.min.css" rel="stylesheet" type="text/css" integrity="sha384-5UlScpvMKqm8C4+4f8Onv2kQgzVXS8JO8MzuvM5V2zX9xIoiEbLiymzueQuWTzEY" crossorigin="anonymous"/><link href="https://fonts.googleapis.com" rel="preconnect"/><link href="https://fonts.gstatic.com" rel="preconnect" crossorigin="anonymous"/>
1<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js" type="text/javascript"></script>
1<script type="text/javascript">WebFont.load({ google: { families: ["Open Sans:300,300italic,400,400italic,600,600italic,700,700italic,800,800italic","Barlow:300,400,500,600,700","DM Sans:300,400,500,600,700","Plus Jakarta Sans:300,400,500,600,700"] }});</script>
1<script type="text/javascript">!function(o,c){var n=c.documentElement,t=" w-mod-";n.className+=t+"js",("ontouchstart"in o||o.DocumentTouch&&c instanceof DocumentTouch)&&(n.className+=t+"touch")}(window,document);</script>
1<link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68cf1cb49e59be031d2f13dc_favicon-32x32.png" rel="shortcut icon" type="image/x-icon"/><link href="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68cf1d169359063e6094caad_logo-256x256.png" rel="apple-touch-icon"/>
1<script type="application/ld+json"> 2{ 3 "@context": "https://schema.org", 4 "@type": "WebPage", 5 "@id": "https://www.redentrenadores.com/#webpage", 6 "name": "Entrenadores personales online o cerca de ti", 7 "description": "Entrenadores personales en España para todos los niveles. Filtra por ciudad u online, objetivos y precio; lee opiniones reales y reserva en minutos.", 8 "inLanguage": "es", 9 "url": "https://www.redentrenadores.com", 10 "isPartOf": { 11 "@type": "WebSite", 12 "@id": "https://www.redentrenadores.com/#website", 13 "name": "RedEntrenadores", 14 "url": "https://www.redentrenadores.com", 15 "inLanguage": "es", 16 "publisher": { 17 "@id": "https://www.redentrenadores.com/#organization" 18 } 19 }, 20 "publisher": { 21 "@type": "Organization", 22 "@id": "https://www.redentrenadores.com/#organization", 23 "name": "RedEntrenadores", 24 "url": "https://www.redentrenadores.com", 25 "logo": { 26 "@type": "ImageObject", 27 "@id": "https://www.redentrenadores.com/#logo", 28 "url": "https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif", 29 "caption": "Logo de RedEntrenadores" 30 }, 31 "sameAs": [ 32 "https://facebook.com/redentrenadores", 33 "https://twitter.com/redentrenadores", 34 "https://www.instagram.com/redentrenadores" 35 ], 36 "contactPoint": { 37 "@type": "ContactPoint", 38 "contactType": "customer service", 39 "email": "[email protected]", 40 "availableLanguage": [ 41 "es" 42 ] 43 } 44 } 45} 46</script>
46<script>(function(w,i,g){w[g]=w[g]||[];if(typeof w[g].push=='function')w[g].push.apply(w[g],Array.isArray(i)?i:[i]);})(window,['G-GSEP18CGXX'],'google_tags_first_party');</script>
46<script async="" src="/avljl2rk9q5pNjhjNWE3MjljNzFmYTNmMzc4NThmYmJl/peOuxr4X3pGz_RiXea8PdbkeTQo"></script>
46<script type="text/javascript">window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('set', 'developer_id.dZGVlNj', true);gtag('set', 'developer_id.dYWYxNW', true);gtag('js', new Date());gtag('config', 'G-GSEP18CGXX');</script>
46<script type="text/javascript">!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.agent='plwebflow';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init', '1689976748571004');fbq('track', 'PageView');</script>
46<!-- Google Tag Manager -->
47<script>
vendor: 327 bytes, lines 47-51
47(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 48new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 49j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 50'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 51})(window,document,'script','dataLayer','
51GTM-KBD83QKS
vendor: 3 bytes, line 51
51');
51</script>
51 52<!-- End Google Tag Manager --> 53 54<!-- Start cookieyes banner -->
55<script id="cookieyes" type="text/javascript" src="https://cdn-cookieyes.com/client_data/3b90490e3918e885f5cdab3d/script.js"></script>
55 56<!-- End cookieyes banner --> 57 58 59<!-- Hotjar / Contentsquare: async porque es analitica pura, nada en el repo 60 depende de que este cargado (grep sin resultados fuera de backups de este 61 mismo bloque). Eran 623 KB bloqueando el primer pintado: el mayor peso de 62 los 4760ms de "solicitudes que bloquean el renderizado" de PageSpeed. -->
63<script src="https://t.contentsquare.net/uxa/e67e16087d6b8.js" async></script>
63 64 65 66<!-- Memberstack: solo fuera de /lp/*. Fuente y porque: 67 widgets/custom-code-memberstack-por-ruta.html -->
68<script> 69(function () { 70 // Las landings de paid no tienen nada que dependa de Memberstack. Cualquier 71 // otra ruta lo carga igual que siempre, bloqueando, porque los widgets del 72 // panel dan por hecho que $memberstackDom ya existe cuando ellos corren. 73 if (/^\/lp(\/|$)/.test(location.pathname)) return; 74 75 document.write( 76 '
76<script data-memberstack-app="app_cmgl62z3b00kw0tt21gk97j2x"' + 77 ' src="https://static.memberstack.com/scripts/v1/memberstack.js"' + 78 ' type="text/javascript"><\/script>' 79 ); 80})(); 81</script>
81 82 83<!-- Para que los dropdown sean blancos --> 84<style> 85.w-dropdown-list { 86 background-color: white !important; 87} 88 89.w-dropdown.w--open .w-dropdown-list { 90 background-color: white !important; 91} 92 93.search-dropdown { 94 background-color: white !important; 95} 96</style> 97 98 99<!-- Limita el texto descriptivo de cada entrenador -->
100<script> 101document.addEventListener("DOMContentLoaded", function() { 102 const elements = document.querySelectorAll(".limit-text"); 103 104 elements.forEach(el => { 105 const text = el.textContent.trim(); 106 107 if (text.length > 80) { 108 el.textContent = text.substring(0, 80) + "..."; 109 } 110 }); 111}); 112</script>
112 113 114 115<!-- Canonical autorreferenciado (auditoria SEO 2026-08) -->
116<script> 117(function () { 118 if (document.querySelector('link[rel="canonical"]')) return; 119 var p = location.pathname; 120 while (p.length > 1 && p.charAt(p.length - 1) === '/') p = p.slice(0, -1); 121 var l = document.createElement('link'); 122 l.rel = 'canonical'; 123 l.href = 'https://www.redentrenadores.com' + (p || '/'); 124 document.head.appendChild(l); 125})(); 126</script>
126 127 128 129<!-- Impide que se descargue el video de fondo de la seccion de FAQ. 130 Es decorativo, esta al final de la pagina y su webm pesa 4,6 MB en las 61 131 paginas que lo llevan (home, listado y las 59 de ciudad). Ocultarlo por CSS 132 no basta: Webflow crea el <video> con autoplay y el navegador se lo baja 133 igual aunque el contenedor este en display:none (comprobado: paused=false, 134 buffered=15s). Sin data-video-urls el script de Webflow no crea el <video>, 135 asi que no hay nada que pedir. 136 Para revertirlo, basta con borrar este bloque y quitar el display:none de 137 la clase bg-video_faqs. 138 ACTUALIZACION 2026-08-11: en /entrenadores-personales/* el elemento se 139 borro directamente (ver commit), asi que este bloque ya no hace nada ahi. 140 Sigue haciendo falta en / y /entrenador-personal hasta que se borre el 141 elemento tambien en esas dos plantillas (vive dentro de un componente que 142 la API no puede recorrer). -->
143<script> 144document.addEventListener('DOMContentLoaded', function () { 145 document.querySelectorAll('[data-video-urls]').forEach(function (el) { 146 el.removeAttribute('data-video-urls'); 147 }); 148}); 149</script>
149 150 151 152<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 153 AMPLITUDE - analitica + session replay (2026-08-14) 154 El proyecto no tiene bundler (no existe package.json en el repo), asi que 155 en vez de `npm install @amplitude/unified` se carga EL MISMO paquete como 156 modulo ESM desde jsDelivr por import() dinamico. La llamada de init es la 157 de la documentacion: initAll, no init. 158 159 CONSENTIMIENTO: no arranca hasta que cookieyes tiene la categoria 160 "analytics" en yes. Esto vale tanto para la analitica como para Session 161 Replay, que graba el 100% de las sesiones (sampleRate 1) y sin 162 consentimiento no puede grabar nada. Verificado sobre el propio banner.js 163 que sirve el sitio: la cookie es "cookieyes-consent" con pares 164 clave:valor separados por coma, las categorias son necessary / functional 165 / analytics / performance / advertisement, y al aceptar se despacha 166 "cookieyes_consent_update" sobre document. Bajo RGPD analytics arranca en 167 "no", asi que por defecto aqui no se carga nada. 168 169 El stub de window.amplitude existe desde el primer momento y encola los 170 track() previos a la carga del modulo; al inicializar se vacia la cola. Si 171 el usuario nunca da su consentimiento, la cola se descarta sin enviarse. 172 Los embeds de widgets/ llaman a amplitude.track(...) contando con esto. 173 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ -->
174<script> 175(function () { 176 window.amplitude = window.amplitude || { _q: [], track: function () { this._q.push(['track', arguments]); } }; 177 178 function hayConsentimiento() { 179 var m = document.cookie.match(/(?:^|;\s*)cookieyes-consent=([^;]*)/); 180 if (!m) return false; 181 return decodeURIComponent(m[1]).split(',').indexOf('analytics:yes') !== -1; 182 } 183 184 var arrancado = false; 185 function arrancar() { 186 if (arrancado || !hayConsentimiento()) return; 187 arrancado = true; 188 import('https://cdn.jsdelivr.net/npm/@amplitude/unified@1/+esm').then(function (amplitude) { 189 var pendientes = (window.amplitude && window.amplitude._q) || []; 190 // Amplitude ingestion key â public by design; move to an env var when you set up environments. 191 amplitude.initAll('21ce582269eabdf7c4208d1960a7731e', {"analytics":{"autocapture":true},"sessionReplay":{"sampleRate":1}}); 192 window.amplitude = amplitude; 193 pendientes.forEach(function (c) { amplitude[c[0]].apply(null, c[1]); }); 194 }).catch(function () { arrancado = false; }); 195 } 196 197 arrancar(); 198 document.addEventListener('cookieyes_consent_update', arrancar); 199})(); 200</script>
200 201 202<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 203 SIMULAR PANEL DE ENTRENADOR (2026-08-21) 204 Deja que el equipo recorra /panel-entrenador viendo los datos de un 205 entrenador concreto, sin entrar con su cuenta y sin poder tocar nada. Lo 206 enciende la pestana "Simular entrenador" de /admin, que abre el panel con 207 ?simular=<token>. Solo lectura: el shim corta cualquier metodo que no sea 208 GET, y las Edge Functions lo rechazan tambien en el servidor. 209 210 Va aqui y no como registered script porque esos tienen un tope de 2.000 211 caracteres y este no cabe (son 10 KB). Se pega MINIFICADO: la fuente 212 legible, con el porque de cada parte, es 213 widgets/custom-code-simular-entrenador.js del repo, y lo de abajo sale de 214 widgets/custom-code-simular-entrenador.min.js. Para cambiarlo: editar la 215 fuente, regenerar con terser y volver a pegar. No editar esto a mano. 216 217 Se corta solo en cuanto la ruta no empieza por /panel-entrenador, asi que 218 en el resto del sitio no hace nada mas que existir. 219 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ -->
220<script> 221/*! Simular panel de entrenador â RedEntrenadores 222 * Version minificada. La fuente legible, con el porque de cada parte, esta en 223 * widgets/custom-code-simular-entrenador.js del repo. Para cambiarlo: editar 224 * ese fichero, regenerar con `npx terser` y volver a pegar aqui. No editar 225 * este bloque a mano. */ 226!function(){var e='https://yrzanatzshdpvypazgeb.supabase.co/functions/v1/',t='rt-simulacion',r=['comprobar-titulo'],n=/^rt-perfil-/;if(0===location.pathname.indexOf('/panel-entrenador')){var o=new URL(location.href),a=o.searchParams.get('simular');if(a){!function(e){try{sessionStorage.setItem(t,JSON.stringify(e))}catch(e){}}({token:a,nombre:o.searchParams.get('simular_nombre')||'este entrenador'}),o.searchParams.delete('simular'),o.searchParams.delete('simular_nombre');var i=o.pathname+(o.searchParams.toString()?'?'+o.searchParams:'')+o.hash;history.replaceState(null,'',i)}var s=function(){try{return JSON.parse(sessionStorage.getItem(t)||'null')}catch(e){return null}}();if(s&&s.token)if(g(s.token)<Date.now())h();else{window.rtSimulacion={entrenador:s.nombre,expira:new Date(g(s.token))};var l='rt-sim:'+s.token.split('.')[1]+':',c=Storage.prototype.getItem,u=Storage.prototype.setItem,d=Storage.prototype.removeItem;Storage.prototype.getItem=function(e){return c.call(this,v(this)?b(e):e)},Storage.prototype.setItem=function(e,t){return u.call(this,v(this)?b(e):e,t)},Storage.prototype.removeItem=function(e){return d.call(this,v(this)?b(e):e)};var m=window.fetch;if(window.fetch=function(t,n){var o='string'==typeof t?t:t&&t.url||'';if(0!==o.indexOf(e))return m.apply(this,arguments);var a=(n&&n.method||t&&t.method||'GET').toUpperCase(),i=o.slice(54).split(/[/?#]/)[0];if('GET'!==a&&'OPTIONS'!==a&&r.indexOf(i)<0)return function(){var e=document.getElementById('rt-sim-aviso');if(!e)return;e.textContent='No se ha guardado nada: el panel simulado es solo de lectura.',e.hidden=!1,clearTimeout(f),f=setTimeout(function(){e.hidden=!0},5e3)}(),Promise.resolve(new Response('simulacion_solo_lectura',{status:403}));if('string'==typeof t){var l=new Headers(n&&n.headers||{});l.set('Authorization','Bearer '+s.token);var c={};for(var u in n||{})c[u]=n[u];return c.headers=l,m.call(this,t,c)}var d=new Request(t,n);return d.headers.set('Authorization','Bearer '+s.token),m.call(this,d)},window.$memberstackDom)y(window.$memberstackDom);else try{var p;Object.defineProperty(window,'$memberstackDom',{configurable:!0,get:function(){return p},set:function(e){p=y(e)}})}catch(e){}var f=null;'loading'===document.readyState?document.addEventListener('DOMContentLoaded',x):x()}}function h(){try{sessionStorage.removeItem(t)}catch(e){}}function g(e){var t=String(e||'').split('.');return 4===t.length?Number(t[2]):0}function b(e){return n.test(String(e))?l+e:e}function v(e){try{return e===window.localStorage}catch(e){return!1}}function y(e){if(!e||'function'!=typeof e.getMemberCookie||e.__rtSimParcheado)return e;var t=e.getMemberCookie.bind(e);return e.getMemberCookie=function(){return Promise.resolve().then(t).then(function(e){return e||s.token}).catch(function(){return s.token})},e.__rtSimParcheado=!0,e}function x(){if(!document.getElementById('rt-sim-banner')){var e=document.createElement('style');e.textContent=['#rt-sim-banner{position:fixed;left:0;right:0;bottom:0;z-index:10000;','background:#FEF3C7;border-top:1px solid #92400E;color:#92400E;','font-family:\'Plus Jakarta Sans\',-apple-system,system-ui,sans-serif;font-size:13px;','display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:10px 16px;box-sizing:border-box}','#rt-sim-banner .rt-sim-texto{flex:1;
226min-width:200px;margin:0}','#rt-sim-banner strong{color:#1B1E22}','#rt-sim-banner .rt-sim-salir{padding:9px 18px;border:1px solid #089F3B;border-radius:8px;','background:#fff;color:#089F3B;text-transform:uppercase;font-weight:700;letter-spacing:0.03em;','font-size:11px;font-family:inherit;cursor:pointer;flex-shrink:0}','#rt-sim-banner .rt-sim-salir:hover{background:#EAF3DE}','#rt-sim-aviso{flex-basis:100%;background:#FCEBEB;color:#791F1F;border-radius:8px;','padding:10px 12px;font-size:13px;margin:0}','#rt-sim-aviso[hidden]{display:none}','body{padding-bottom:64px}'].join(''),document.head.appendChild(e);var t,r=document.createElement('div');r.id='rt-sim-banner',r.innerHTML='<p class="rt-sim-texto">Estás viendo el panel de <strong>'+(t=s.nombre,String(null==t?'':t).replace(/[&<>"']/g,function(e){return{'&':'&','<':'<','>':'>','"':'"','\'':'''}[e]})+'</strong>. Solo lectura: nada de lo que hagas aquà se guarda.</p><button type="button" class="rt-sim-salir">Salir de la simulación</button><p id="rt-sim-aviso" hidden></p>'),document.body.appendChild(r),r.querySelector('.rt-sim-salir').addEventListener('click',function(){h(),function(){try{for(var e=[],t=0;t<localStorage.length;t++){var r=localStorage.key(t);r&&0===r.indexOf('rt-sim:')&&e.push(r)}e.forEach(function(e){d.call(localStorage,e)})}catch(e){}}(),location.href='/admin'})}}}(); 227 228</script>
228<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a7b1ad624d451ff7ab95b57%2Fdeep_link_panel_tras_login-0.0.1.js" type="text/javascript"></script>
228<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a8188e2745a991cf146025e%2Fredirect_urls_antiguas_panel-1.0.0.js" type="text/javascript"></script>
228<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a90ae0a69c0c92f2517a3b6%2Fawintracking2-1.0.0.js" type="text/javascript"></script>
228<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6aa83d1b2d69974135a99d99%2Fguardian_sesion_panel-1.0.0.js" type="text/javascript"></script>
228<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6aaed9e15b8290737a4ca482%2Fguardia_onboarding_entrenador-1.0.0.js" type="text/javascript"></script>
228<script type="text/javascript">window.__WEBFLOW_CURRENCY_SETTINGS = {"currencyCode":"USD","symbol":"$","decimal":".","fractionDigits":2,"group":",","template":"{{wf {\"path\":\"symbol\",\"type\":\"PlainText\"} }} {{wf {\"path\":\"amount\",\"type\":\"CommercePrice\"} }} {{wf {\"path\":\"currencyCode\",\"type\":\"PlainText\"} }}","hideDecimalForWholeNumbers":false};</script>
228</head><body><div data-animation="default" class="rl_navbar5_component padding-top-medium w-nav" data-easing2="ease" data-easing="ease" data-collapse="medium" role="banner" data-duration="400" data-wf--header-v6--variant="base" ms-code-scroll-offset="40"><div class="rl_navbar5_container"><a href="/" aria-current="page" class="rl_navbar5_logo-link w-nav-brand w--current"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif" loading="lazy" width="63" sizes="63px" alt="Logotipo de RedEntrenadores" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-500.avif 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-800.avif 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif 2246w" class="rl_navbar5_logo"/></a><nav role="navigation" data-ms-content="!members" class="rl_navbar5_menu w-nav-menu"><div class="nav-pills-grupo"><a href="/pregunta-al-experto" class="nav-pill-item">Pregunta al experto</a><div class="nav-pill-sep"></div><a href="/donde-entrenar" class="nav-pill-item">Dónde entrenar</a></div><div class="rl_navbar5_menu-left"><div data-hover="true" data-delay="300" class="rl_navbar5_menu-dropdown w-dropdown"><div class="rl_navbar5_dropdown-toggle w-dropdown-toggle"><div class="rl-dropdown-icon w-embed"><svg width=" 100%" height=" 100%" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 229<path fill-rule="evenodd" clip-rule="evenodd" d="M2.55806 6.29544C2.46043 6.19781 2.46043 6.03952 2.55806 5.94189L3.44195 5.058C3.53958 4.96037 3.69787 4.96037 3.7955 5.058L8.00001 9.26251L12.2045 5.058C12.3021 4.96037 12.4604 4.96037 12.5581 5.058L13.4419 5.94189C13.5396 6.03952 13.5396 6.19781 13.4419 6.29544L8.17678 11.5606C8.07915 11.6582 7.92086 11.6582 7.82323 11.5606L2.55806 6.29544Z" fill="currentColor"/> 230</svg></div><div class="rl_navbar5_link-text">Recursos</div></div><nav class="rl_navbar5_dropdown-list-copy w-dropdown-list"><div class="rl_navbar5_container"><div class="rl_navbar5_dropdown-content"><div class="rl_navbar5_dropdown-content-left"><div class="rl_navbar5_dropdown-link-list"><h4 class="rl_navbar5_dropdown-column-text">Herramientas gratis</h4><a href="/calculadoras/precio-entrenador-personal" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a637ee83ad287f97c093036_euro%20coin.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Cuánto cuesta un entrenador</div><p class="rl_navbar5_text-small">La horquilla real por sesión y al mes, con 126 tarifas publicadas.</p></div></a><a href="/calculadoras/donde-te-conviene-entrenar" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a6333b155392ae72dc3544a_icono%20lugar.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">¿Dónde te conviene entrenar?</div><p class="rl_navbar5_text-small">Gimnasio, a domicilio, al aire libre u online: seis preguntas y la pega de cada uno.</p></div></a><a href="/calculadoras/plan-primer-mes" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176197f33ec0043320f2a_dumbbell-01.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Tu primer mes de entrenamiento</div><p class="rl_navbar5_text-small">Cuántas sesiones y con qué foco cada semana, según los dÃas que tengas y dónde entrenes.</p></div></a><a href="/calculadoras/calculadora-calorias" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176422d38a3a4f0d2241b_weight-scale.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Calculadora de calorÃas</div><p class="rl_navbar5_text-small">Calcula las calorÃas diarias que necesitas según tu edad, peso y nivel de actividad fÃsica.</p></div></a><a href="/calculadoras/calculadora-proteina" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a17619966cea24bd9bfe87_ta
230pe-measure.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Calculadora de proteÃna</div><p class="rl_navbar5_text-small">Cuánta proteÃna necesitas al dÃa según tu peso y tu objetivo, con las calorÃas al lado.</p></div></a><a href="/calculadoras/calculadora-imc" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6ac016796b7d40191f7cf036_indice-imc.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Calculadora de IMC</div><p class="rl_navbar5_text-small">Tu Ãndice de masa corporal y el rango de peso que corresponde a tu altura.</p></div></a><a href="/calculadoras/test-nivel-fitness" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176695fe1a56f4bbbe03b_workout-run.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Test de nivel de fitness</div><p class="rl_navbar5_text-small">Evalúa tu condición fÃsica actual y descubre qué tipo de entrenador necesitas.</p></div></a><a href="/calculadoras/pruebas-fisicas-oposiciones" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69c411fa43d84eac3751591f_medal.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Pruebas fÃsicas de oposición</div><p class="rl_navbar5_text-small">Compara tus marcas con las que te piden y descubre cuál es tu punto débil.</p></div></a></div><div class="rl_navbar5_dropdown-link-list"><h4 class="rl_navbar5_dropdown-column-text">Entrenadores por ciudad</h4><a href="https://www.redentrenadores.com/entrenadores-personales/madrid" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Madrid</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/barcelona" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Barcelona</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/zaragoza" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Zaragoza</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/valencia" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Valencia</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/sevilla" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Sevilla</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/malaga" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Málaga</div></div></a><a href="https://www.redentrenadores.com/entrenadores-personales/murcia" target="_blank" class="rl_navbar5_dropdown-link w-inline-block"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a59d-03a4a53d" class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Murcia</div></div></a><a href="/donde-entrenar" class="rl_navbar5_dropdown-link"><div class="rl_navbar5_icon-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/69a176ab30392e5a11168bd3_icon%20simple.svg" loading="lazy" alt="" class="rl_navbar5_icon"/></div><div class="rl_navbar5_item-right"><div class="rl_navbar5_item-title">Dónde entrenar</div></div></a><a id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a5a0-03a4a53d" href="/entrenador-personal" class="btn-green indide-menu w-inline-block"><div>Ver más ciudades</div></a></div></div><div class="rl_navbar5_dropdown-content-right"><div class="rl_navbar5_dropdown-content-wrapper"><h4 class="rl_navbar5_dropdown-column-text white">ArtÃculos destacados</h4><div id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a5a7-03a4a53d" class="rl_navbar5_blog-list-wrapper"><div class="rl_navbar5_blog-list"><a href="https://www.redentrenadores.com/blog/precio-entrenador-personal-online" target="_blank" class="rl_navbar5_blog-item w-inline-block"><div class="rl_navbar5_blog-image-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840.webp" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-500.webp 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-800.webp 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-1080.webp 1080w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-1600.webp 1600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-2000.webp 2000w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-2600.webp 2600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840-p-3200.webp 3200w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d4c5d1b40a16e775245_pexels-cottonbro-4064840.webp 4152w" alt="Persona realizando un entrenamiento personal" class="rl_navbar5_blog-image"/></div><div class="rl_navbar5_large-item-content"><div class="rl_navbar5_item-title white">¿Cuánto cuesta realmente un entrenador personal online?</div><div class="rl_navbar5_item-link">Leer artÃculo</div></div></a><a href="https://www.redentrenadores.com/blog/tipos-entrenamiento-personal" target="_blank" class="rl_navbar5_blog-item w-inline-block"><div class="rl_navbar5_blog-image-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d6acffc97162b06e822_In-home-personal-training.webp" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d6acffc97162b06e822_In-home-personal-training-p-500.webp 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d6acffc97162b06e822_In-home-personal-training.webp 600w" alt="Entrenador personal entrenando a un cliente a domicilio" class="rl_navbar5_blog-image"/></div><div class="rl_navbar5_large-item-content"><div class="rl_navbar5_item-title white">Tipos de entrenamiento personal: descubre cuál se adapta mejor a ti</div><div class="rl_navbar5_item-link">Leer artÃculo</div></div></a><a href="https://www.redentrenadores.com/blog/entrenador-personal-nutricionista-fisioterapeuta" target="_blank" class="rl_navbar5_blog-item w-inline-block"><div id="w-node-_64a5fb61-5186-864a-70be-7b9103a4a5c2-03a4a53d" class="rl_navbar5_blog-image-wrapper"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341.webp" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-500.webp 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-800.webp 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-1080.webp 1080w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-1600.webp 1600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-2000.webp 2000w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-2600.webp 2600w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341-p-3200.webp 3200w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/693c5d7b2b95815407c9ec1e_pexels-olly-3813341.webp 7680w" alt="Persona realizando un entrenamiento personal" class="rl_navbar5_blog-image"/></div><div class="rl_navbar5_large-item-content"><div class="rl_navbar5_item-title white">Entrenador personal, nutricionista y fisioterapeuta: el equipo perfecto para transformar tu cuerpo</div><div class="rl_navbar5_item-link">Leer artÃculo</div></div></a></div></div><div class="rl-button-group"><a href="/blog" class="rl-button-link w-inline-block"><div class="rl-button-link-text">Ver todos los artÃculos</div><div class="rl-button-link-icon w-embed">
230<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 231<path d="M6 3L11 8L6 13" stroke="CurrentColor" stroke-width="1.5"/> 232</svg></div></a></div></div><div class="rl_navbar5_dropdown-background-layer"></div></div></div></div></nav></div></div><div class="rl_navbar5_menu-right"><div data-hover="true" data-delay="0" data-ms-content="!members" class="drop-menu_entrenador w-dropdown"><div class="rl_navbar5_dropdown-toggle menu w-dropdown-toggle"><div class="rl-dropdown-icon w-icon-dropdown-toggle"></div><div>¿Eres entrenador?</div></div><nav class="dropdown-list-2 w-dropdown-list"><a href="/panel-entrenador" class="w-dropdown-link">Zona para entrenadores</a><a href="/registrate" class="w-dropdown-link">Activar perfil gratuito</a></nav></div><a href="https://www.redentrenadores.com/encuentra-entrenador" class="btn-green menu w-button">Buscar entrenador</a></div></nav><div class="w-layout-hflex"><a data-ms-content="members" href="/panel-entrenador" class="btn-green white_version w-button">Mi panel</a><div class="rl_navbar5_menu-button w-nav-button"><div data-ms-content="!members" class="rl_menu-icon"><div class="rl_menu-icon_line-top"></div><div class="rl_menu-icon_line-middle"><div class="rl_menu-icon_line-middle-inner"></div></div><div class="rl_menu-icon_line-bottom"></div></div></div></div></div><div class="w-embed w-script">
232<script> 233/* Webflow solo permite 991/767/479 como punto de colapso y su JS decide por su 234 cuenta (ignora el CSS). Con data-collapse=all lo gestiona en todos los anchos 235 y el <style> de abajo devuelve la fila a partir de 1180px. Este embed es hijo 236 del navbar y se ejecuta al parsearse, antes de que cargue webflow.js al final 237 del body, asi que el atributo ya esta puesto cuando Webflow inicializa. 238 El listener de resize hace falta porque, al creer que siempre esta colapsado, 239 Webflow ya no cierra el menu al cruzar un breakpoint: sin esto, quien abre el 240 menu a 1100px y luego amplia la ventana se queda con el menu enganchado 241 (Webflow mueve el <nav> dentro de .w-nav-overlay al abrir). Se cierra con su 242 propio boton para que Webflow restaure el DOM y limpie sus estilos inline. */ 243(function(){ 244 var s=document.currentScript,n=s&&s.parentElement&&s.parentElement.closest(".w-nav"); 245 if(!n)return; 246 n.setAttribute("data-collapse","all"); 247 var t; 248 window.addEventListener("resize",function(){ 249 clearTimeout(t); 250 t=setTimeout(function(){ 251 if(window.innerWidth<1180)return; 252 var b=n.querySelector(".w-nav-button.w--open"); 253 if(b)b.click(); 254 },100); 255 }); 256})(); 257</script>
257 258<style> 259/* El menu colapsa a hamburguesa hasta 1179px: por debajo de ~1165 no caben los 260 dos enlaces destacados y el flexbox aplasta el logo de 250 a 99px (medido). 261 Este bloque replica, para 992-1179px, las reglas que Webflow ya aplica bajo 262 991px al desplegar el menu. Revertir: borrar este embed entero y poner 263 "Collapse menu at" = medium en el Navbar. 264 Fuente en el repo: widgets/custom-code-header-colapso.css */ 265@media screen and (min-width:992px) and (max-width:1179px){.nav-pills-grupo{flex-direction:column;width:100%;background-color:transparent;border-radius:0;padding:0;row-gap:0}.nav-pill-item{width:100%;justify-content:center;font-size:1.125rem;padding:.75rem 0}.nav-pill-sep{display:none}.rl_menu-icon_line-middle{background-color:#000;justify-content:center;align-items:center;width:24px;height:2px;margin-top:6px;margin-bottom:6px;padding-bottom:0;padding-right:0;display:flex}.rl_navbar5_dropdown-toggle{flex-flow:row;justify-content:center;align-items:center;min-width:250px;padding:.75rem 0;font-size:1.125rem;display:flex}.rl_navbar5_dropdown-toggle.menu{width:100%;min-width:100%}.rl_navbar5_dropdown-toggle.menu.profile{justify-content:flex-end;align-items:center}.rl_navbar5_blog-item{width:100%}.rl_menu-icon_line-top{background-color:#000;width:24px;height:2px;padding-bottom:0;padding-right:0}.rl_menu-icon{flex-direction:column;justify-content:center;align-items:center;width:48px;height:48px;margin-right:-.5rem;padding-bottom:0;padding-right:0;display:flex}.rl_navbar5_dropdown-background-layer{border-radius:4px;min-width:100%}.rl_navbar5_dropdown-content-left{align-content:start;align-items:start;padding-left:0;padding-right:0}.rl_navbar5_large-item-content{width:100%}.rl_navbar5_dropdown-content{flex-direction:column}.rl_navbar5_menu-button{flex-flow:row}.rl_navbar5_menu-button.w--open{background-color:#0000}.rl_navbar5_blog-image{object-fit:cover}.rl_navbar5_blog-list{width:100%}.rl_menu-icon_line-middle-inner{width:4px;height:0;padding-bottom:0;padding-right:0}.rl-dropdown-icon{grid-column-gap:0px;grid-row-gap:0px;justify-content:center;align-items:center;inset:auto 0% auto auto}.rl_menu-icon_line-bottom{background-color:#000;width:24px;height:2px;padding-bottom:0;padding-right:0}.rl_navbar5_menu-right{flex-direction:column;grid-template-rows:auto auto;grid-template-columns:1fr;align-items:stretch;margin-top:1rem}.rl_navbar5_menu{-webkit-text-fill-color:inherit;background-color:#fff;background-clip:border-box;border-bottom:1px solid #000;align-items:flex-start;height:100vh;margin-left:0;padding:1rem 5% 15rem;position:absolute;overflow:auto}.rl_navbar5_dropdown-content-right{align-items:flex-start;max-width:none}.rl_navbar5_menu-left{flex-flow:column;justify-content:center;align-items:center}.rl_navbar5_menu-dropdown{flex-flow:column;width:100%;display:flex;position:relative}.rl_navbar5_dropdown-content-wrapper{width:100%}.drop-menu_entrenador{width:100%}.drop-menu_entrenador.profile{margin-left:0}.rl_navbar5_dropdown-list-copy{position:relative;overflow:hidden}.rl_navbar5_dropdown-list-copy.w--open{border-style:none;align-items:flex-start;min-width:auto;margin-bottom:1.5rem;padding:0 0 0;position:relative;top:0;left:0;right:0}} 266@media screen and (min-width:1180px){.w-nav[data-collapse=all] .w-nav-menu{display:flex}.w-nav[data-collapse=all] .w-nav-button{display:none}} 267</style></div></div><section class="general-seccion"><div class="wrap_general"><div class="hero_wrapp"><div class="text-green_hero">resultados reales.</div><div class="text-green_hero _2">sin excusas</div><h1 class="h1_hero">Encuentra entrenador personal para alcanzar tus objetivos en cualquier ciudad de España</h1><div class="w-embed w-script"><!-- ============================================================ 268 RedEntrenadores â cabecera de la home: te buscamos el entrenador 269 270 El usuario escribe con sus palabras a quién necesita, la Edge Function 271 `clasificar-busqueda` lo traduce a objetivo / modalidad / ciudad / cuándo, 272 y el pop-up recomendador (widget-popup-recomendador.html) se abre con esas 273 respuestas ya puestas. No hay reparto nuevo ni backend nuevo: el envÃo a 274 los 3 entrenadores sigue siendo el de siempre. 275 276 Las cuatro casillas NO son decorativas: son los cuatro pasos del pop-up, y 277 se encienden MIENTRAS se escribe (pausa de 700 ms). Esa es la promesa de 278 la pantalla: cada casilla verde es una pregunta que ya no hay que hacer. 279 Encenderlas solo al pulsar el botón no servÃa de nada â el pop-up se abre 280 encima medio segundo después y no daba tiempo ni a verlas. 281 282 Tres guardas en la clasificación en vivo, y las tres importan: 283 - Se clasifica al hacer pausa, nunca por tecla. 284 - Una respuesta que llega tarde no pisa a otra más nueva (`peticion`). 285 - Si la frase no ha cambiado desde la última vez, no se vuelve a pedir; 286 al enviar se reaprovecha y ese envÃo no gasta llamada. 287 288 Cómo engancha con el pop-up, sin tocarlo: 289 290 `window.__reRecNucleo.estado` cruza por referencia entre los dos bloques 291 del pop-up, asà que escribir ahà es escribir en su estado real. Y 292 `abrir()` reinicia el Ãndice y la fase, pero NO las respuestas, asà que 293 lo que dejemos escrito sobrevive a la apertura. 294 295 Además de las respuestas se marca `estado.sabidos`, que es lo que hace 296 que el pop-up NO vuelva a preguntar lo que la persona acaba de escribir: 297 quien lo lee es widget-popup-recomendador-saltar-sabidos.html, el parche 298 que vive entre los bloques 2 y 3 del pop-up en el Footer V1. Si alguien 299 borra ese parche, esto no se rompe: el pop-up vuelve a preguntarlo todo, 300 solo que con las respuestas ya rellenas. 301 302 Con las cuatro casillas en verde el pop-up abre directamente en el 303 formulario de contacto: es la única pantalla que no se puede saltar, 304 porque el nombre y el email no salen de una frase. 305 306 El texto original Sà se guarda, desde el 23/08/2026, pero no viaja con la 307 solicitud: `clasificar-busqueda` lo mete en `busquedas_texto`, una tabla 308 de uso interno sin grant para `anon` ni `authenticated`. Es deliberado â 309 Esteve lee lo que pide la gente con sus palabras, el entrenador no. Como se 310 clasifica mientras se escribe, de una misma persona caen varias filas con 311 la frase a medias: `sesion` las agrupa y `enviada` marca la que acabó 312 abriendo el reparto. 313 314 Si el pop-up no está cargado o la clasificación falla, el CTA no se queda 315 muerto: lleva a /entrenador-personal con los filtros que se hayan 316 entendido en la URL, que es lo que ya lee widget-listado-entrenadores-2-js. 317 318 Hay una sola búsqueda que NO abre el pop-up: la de un gimnasio que busca a 319 quién contratar. No es un usuario, y si se le deja pasar acaba saliendo un 320 reparto a tres entrenadores indistinguible del de un cliente, porque el 321 texto original no viaja con la solicitud. Se corta antes y se le da el 322 email (`esReclutamiento`, 12/09/2026). Lo demás sigue abriendo el pop-up 323 aunque no se haya entendido nada: preguntar es el plan B de siempre. 324 325 Dónde vive: un HtmlEmbed dentro del hero de la Home de Webflow. El botón 326 verde que habÃa antes ahà está escondido (Visibility=false), no borrado: 327 si algún dÃa se quiere volver atrás, se enciende y ya está. 328 329 Ojo con el encabezado: la Home ya tiene su H1 ("Encuentra entrenador 330 personal para alcanzar tus objetivos en cualquier ciudad de España") y es 331 el que recibe los clics de Google. Por eso el tÃtulo de aquà es un H2 y 332 no un H1 â dos H1 en la misma página no es un detalle de estilo. 333 334 Alturas medidas (23/08/2026): 293px a 1280 de ancho y 307px a 375, y 367 335 en móvil con las cuatro casillas encendidas y la leyenda visible. Va 336 dentro de un hero que ya ocupa pantalla entera, asà que si alguien vuelve 337 a subir tamaños conviene volver a medirlo en móvil, no solo en escritorio. 338 339 El código vive en widgets/widget-home-hero-buscar.html del repo. Si se 340 edita el embed en Webflow, hay que traerlo de vuelta aquÃ: no se 341 sincronizan solos. 342 ============================================================ --> 343<style> 344 /* Va encima de la foto del hero (.hero_wrapp tiene background-image), por eso 345 el fondo es blanco translúcido y no blanco: deja ver la imagen sin dejar el
346 texto sobre ella. El 0.88 es el suelo â por debajo, el #1B1E22 empieza a 347 pelearse con las zonas claras de la foto. */ 348 /* `font-weight:400` no es de adorno: .hero_wrapp declara 700 y la fuente de 349 titulares para todo lo que lleva dentro, asà que sin esto el subtÃtulo y 350 las casillas salen en negrita. */ 351 .re-hb { background:rgba(255,255,255,0.88); border:1px solid #089F3B; border-radius:8px; padding:18px 20px 20px; font-family:'Plus Jakarta Sans', Arial, sans-serif; font-weight:400; } 352 .re-hb__wrap { max-width:720px; margin:0 auto; } 353 .re-hb__h1 { font-family:'Barlow', Arial, sans-serif; font-size:22px; font-weight:700; line-height:1.3; color:#1B1E22; margin:0 0 4px; } 354 .re-hb__h1 em { font-style:normal; color:#089F3B; } 355 .re-hb__sub { font-size:14px; line-height:1.5; color:#4B5563; margin:0 0 12px; } 356 357 .re-hb__oculto { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; } 358 .re-hb__box { border:1px solid #089F3B; border-radius:8px; padding:12px 14px; background:rgba(255,255,255,0.72); transition:border-color .15s, box-shadow .15s; } 359 .re-hb__box.is-focus { border-color:#089F3B; box-shadow:0 0 0 3px rgba(8,159,59,0.15); } 360 .re-hb__input { width:100%; min-height:44px; border:0; outline:0; resize:none; padding:0; background:transparent; font-family:inherit; font-size:15px; line-height:1.5; color:#1B1E22; } 361 .re-hb__input::placeholder { color:#9ca3af; } 362 .re-hb__foot { display:flex; align-items:center; justify-content:flex-end; gap:12px; margin-top:8px; } 363 .re-hb__cta { padding:11px 22px; border:0; border-radius:8px; background:#089F3B; color:#FFFFFF; font-family:inherit; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.03em; display:inline-flex; align-items:center; gap:8px; cursor:pointer; transition:background .15s; } 364 .re-hb__cta:hover { background:#067A2D; } 365 .re-hb__cta[disabled] { opacity:.5; cursor:not-allowed; } 366 .re-hb__error { font-size:12.5px; color:#791F1F; background:#FCEBEB; border-radius:8px; padding:8px 12px; margin:12px 0 0; display:none; } 367 .re-hb__error.is-visible { display:block; } 368 369 /* Quien busca entrenador PARA SU GIMNASIO no se ha equivocado de sitio, asà 370 que no lleva el rojo de error: va el aviso neutro del sistema (§30). */ 371 .re-hb__aviso { font-size:13px; line-height:1.5; color:#4B5563; background:#F3F4F6; border-radius:8px; padding:10px 14px; margin:12px 0 0; display:none; } 372 .re-hb__aviso.is-visible { display:block; } 373 .re-hb__aviso a { color:#067A2D; font-weight:700; text-decoration:underline; } 374 375 /* La leyenda solo aparece cuando de verdad hemos entendido algo: si sale de 376 entrada, promete un trabajo que todavÃa no ha pasado. */ 377 .re-hb__leyenda { font-size:12.5px; color:#4B5563; margin:12px 0 0; display:none; } 378 .re-hb__leyenda.is-visible { display:block; } 379 .re-hb__criterios { display:flex; flex-wrap:wrap; gap:6px 18px; list-style:none; margin:8px 0 0; padding:0; font-size:12.5px; } 380 .re-hb__crit { display:inline-flex; align-items:center; gap:6px; color:#6b7280; } 381 .re-hb__crit.is-ok { color:#089F3B; font-weight:600; } 382 .re-hb__crit svg { flex-shrink:0; } 383 .re-hb__crit-check { display:none; } 384 .re-hb__crit.is-ok .re-hb__crit-check { display:inline; } 385 .re-hb__crit.is-ok .re-hb__crit-vacio { display:none; } 386 387 .re-hb__ejemplos { display:flex; flex-wrap:wrap; gap:8px; margin:12px 0 0; } 388 .re-hb__ejemplo { background:rgba(255,255,255,0.72); border:1px solid #089F3B; border-radius:8px; padding:6px 12px; font-family:inherit; font-size:12.5px; color:#1B1E22; cursor:pointer; } 389 .re-hb__ejemplo:hover { background:#EAF3DE; color:#067A2D; } 390 391 /* En móvil el bloque va debajo del hero de siempre, asà que cada pÃxel se 392 paga con scroll: los ejemplos son un extra y se van. Las cuatro casillas 393 se quedan porque son la promesa del buscador, no adorno. */ 394 @media (max-width: 600px) { 395 .re-hb { padding:16px 20px 20px; } 396 .re-hb__h1 { font-size:19px; } 397 .re-hb__sub { font-size:13.5px; margin-bottom:10px; } 398 .re-hb__input { min-height:40px; font-size:15px; } 399 .re-hb__ejemplos { display:none; } 400 } 401</style> 402 403<section class="re-hb"> 404 <div class="re-hb__wrap"> 405 <h2 class="re-hb__h1">Cuéntanos qué necesitas y <em>te buscamos</em> el entrenador</h2> 406 <p class="re-hb__sub">EscrÃbelo con tus palabras y te proponemos los tres que mejor encajan contigo. Gratis y sin compromiso.</p> 407 408 <div class="re-hb__box" id="re-hb-box"> 409 <label class="re-hb__oculto" for="re-hb-texto">Cuéntanos qué entrenador necesitas</label> 410 <textarea class="re-hb__input" id="re-hb-texto" rows="2" maxlength="600" placeholder="Quiero perder unos kilos entrenando en casa, en Valencia, y empezar cuanto antes"></textarea> 411 <div class="re-hb__foot"> 412 <button type="button" class="re-hb__cta" id="re-hb-cta">
413 <span id="re-hb-cta-texto">Recomiéndame un entrenador</span> 414 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h13M13 6l6 6-6 6"/></svg> 415 </button> 416 </div> 417 </div> 418 <p class="re-hb__error" id="re-hb-error"></p> 419 <p class="re-hb__aviso" id="re-hb-aviso"></p> 420 421 <p class="re-hb__leyenda" id="re-hb-leyenda">Esto es lo que ya hemos entendido:</p> 422 <ul class="re-hb__criterios" id="re-hb-criterios"> 423 <li class="re-hb__crit" data-campo="objetivo"> 424 <svg class="re-hb__crit-vacio" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/></svg> 425 <svg class="re-hb__crit-check" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 12.2l2.4 2.4 4.6-4.9" stroke-linecap="round" stroke-linejoin="round"/></svg> 426 <span>Objetivo</span> 427 </li> 428 <li class="re-hb__crit" data-campo="modalidad"> 429 <svg class="re-hb__crit-vacio" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/></svg> 430 <svg class="re-hb__crit-check" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 12.2l2.4 2.4 4.6-4.9" stroke-linecap="round" stroke-linejoin="round"/></svg> 431 <span>Dónde entrenas</span> 432 </li> 433 <li class="re-hb__crit" data-campo="ciudad"> 434 <svg class="re-hb__crit-vacio" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/></svg> 435 <svg class="re-hb__crit-check" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 12.2l2.4 2.4 4.6-4.9" stroke-linecap="round" stroke-linejoin="round"/></svg> 436 <span>Ciudad</span> 437 </li> 438 <li class="re-hb__crit" data-campo="cuando"> 439 <svg class="re-hb__crit-vacio" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/></svg> 440 <svg class="re-hb__crit-check" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 12.2l2.4 2.4 4.6-4.9" stroke-linecap="round" stroke-linejoin="round"/></svg> 441 <span>Cuándo empiezas</span> 442 </li> 443 </ul> 444 445 <div class="re-hb__ejemplos" id="re-hb-ejemplos"> 446 <button type="button" class="re-hb__ejemplo">Perder peso en casa</button> 447 <button type="button" class="re-hb__ejemplo">Ganar músculo en Madrid</button> 448 <button type="button" class="re-hb__ejemplo">Recuperarme de una lesión</button> 449 </div> 450 </div> 451</section> 452
453<script> 454(function () { 455 "use strict"; 456 457 var SUPABASE_URL = "https://yrzanatzshdpvypazgeb.supabase.co"; 458 var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlyemFuYXR6c2hkcHZ5cGF6Z2ViIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ0NjczNzcsImV4cCI6MjEwMDA0MzM3N30.WNRs7Bc8lA1JUw-KbOHYDBlEjTIPjwxx3cvlBmdpgHU"; 459 460 // 6 desde el 12/09/2026, antes 8. El suelo no está para filtrar ruido â 461 // "Xdccfvgxgb" pasaba de largo con 8 â sino para no gastar una llamada por 462 // cada tecla. Con 8 se quedaban fuera búsquedas de una sola palabra que sà 463 // se entienden solas: "policÃa" y "bombero" tienen 7, y a quien las escribe 464 // se le respondÃa "cuéntanos un poco más" en vez de clasificarlas. 465 var MIN_CARACTERES = 6; 466 var LISTADO = "/entrenador-personal"; 467 var PAUSA = 300; // ms sin teclear antes de mandar a clasificar 468 469 // Las pruebas fÃsicas de una oposición son el caso que más veces se escribe 470 // sin usar la palabra "oposición": se escribe el cuerpo y ya ("PolicÃa 471 // municipal", 12/09/2026, que se fue de la home sin una sola casilla 472 // encendida). Encajan en "Preparación para deportistas", que es con lo que 473 // los entrenadores marcan preparar una prueba fÃsica, asà que aquà no se 474 // inventa una especialidad nueva: solo se nombra lo que la gente sà escribe. 475 // 476 // Solo cuerpos con prueba fÃsica de acceso, y ninguno ambiguo: "militar" a 477 // secas se queda fuera porque "entrenamiento militar" es una forma de 478 // entrenar, no una oposición. La misma lista, palabra por palabra, vive en 479 // supabase/functions/clasificar-busqueda/index.ts: si aquà se añade un 480 // cuerpo y allà no, la respuesta del modelo apaga la casilla que esto acaba 481 // de encender, que es peor que no encenderla. 482 var OPOSICIONES = /oposici|opositar|opositor|pruebas? f[iÃ]sicas?|polic[iÃ]a|guardia civil|guardia urbana|bombero|mossos|ertzaintza|polic[iÃ]a foral|ej[eé]rcito|fuerzas armadas|academia militar|funcionario de prisiones|agente forestal|socorrista/i; 483 484 // Lectura local, instantánea y a propósito corta de miras: solo las formas de 485 // decirlo que no admiten duda. Enciende las casillas mientras la IA responde 486 // (~0,9 s) y luego la respuesta buena manda. Si aquà se meten patrones 487 // ambiguos, la casilla se enciende y se apaga sola: peor que no encenderla. 488 var PISTAS = { 489 objetivo: [ 490 [/adelgaz|perder (peso|kilos|grasa)|bajar de peso/i, "Pérdida de peso"], 491 [/masa muscular|ganar m[uú]sculo|hipertrofia|volumen/i, "Ganar masa muscular"], 492 [/lesi[oó]n|rehabilitac|recuperarme|hernia/i, "Recuperación de lesiones"], 493 [/funcional/i, "Entrenamiento funcional"], 494 [/marat[oó]n|competici[oó]n|triatl[oó]n|media marat/i, "Preparación para deportistas"], 495 [OPOSICIONES, "Preparación para deportistas"], 496 [/(ponerme|estar) en forma|forma f[iÃ]sica/i, "Mejora de forma fÃsica"] 497 ], 498 modalidad: [ 499 [/online|videollamada|a distancia|por v[iÃ]deo/i, "Online"], 500 [/a domicilio|en mi casa|a mi casa|en mi piso|venga a casa/i, "A domicilio"], 501 [/gimnasio|gym/i, "En un gimnasio"], 502 [/aire libre|al parque|en el parque|la playa|exteriores/i, "En exteriores"] 503 ], 504 cuando: [ 505 [/cuanto antes|lo antes posible|empezar ya|urgente|esta semana|inmediat/i, "Lo antes posible"], 506 [/me estoy informando|solo miro|solo mirando|por curiosidad/i, "Solo me estoy informando"] 507 ] 508 }; 509 510 // Un gimnasio buscando a quién contratar no es un usuario: si se le deja 511 // seguir, el pop-up le pregunta su objetivo, elige uno de la lista porque hay 512 // que elegir, y salen tres entrenadores a los que les llega una solicitud 513 // idéntica a la de un cliente â el texto original no viaja con el reparto, asà 514 // que no tienen forma de saberlo. Pasó el 28/08/2026: tres entrenadores de 515 // Madrid recibieron "Omar, mejora de forma fÃsica, en un gimnasio"; dos ni lo 516 // abrieron. Por eso esto corta ANTES de abrir el pop-up. 517 // 518 // Las dos listas son deliberadamente estrechas, porque el falso positivo es 519 // caro: un usuario de verdad al que le decimos que se ha equivocado de sitio 520 // se va. De ahà que "contratar" no esté en ningún lado â "quiero contratar un 521 // entrenador para mi gimnasio" es exactamente lo que escribe un cliente. Lo 522 // que no dice nunca un cliente es "incorporar", "vacante" o "plantilla". 523 // 524 // Ni una barra invertida en estas tres: el canal por el que se escribe el 525 // embed en Webflow se come los escapes, y un limite de palabra que llega roto 526 // no da error: deja de casar y ya esta. Por eso "cv" se acota con clases de 527 // caracteres y no con limites de palabra. 528 var RECLUTA_SOLO = /vacante|ofert[ao] de (trabajo|empleo)|bolsa de (trabajo|empleo)|selecci[oó]n de personal|emplead[oa]s?|curr[iÃ]culum|(^|[^a-z])cv([^a-z]|$)|n[oó]mina|contratar personal/i; 529 var RECLUTA_VERBO = /incorporar|incorporaci[oó]n|fichar|plantilla|puesto|jornada|turnos?|buscamos|necesitamos|empleo/i; 530 var RECLUTA_NEGOCIO = /mi (equipo|plantilla|centro|estudio|club|box|empresa|negocio|gimnasio|sala)|nuestr[oa] (equipo|plantilla|centro|estudio|club|box|empresa|gimnasio|sala)/i; 531 532 function esReclutamiento(valor) { 533 if (RECLUTA_SOLO.test(valor)) return true; 534 return RECLUTA_VERBO.test(valor) && RECLUTA_NEGOCIO.test(valor); 535 } 536 537 var box = document.getElementById("re-hb-box"); 538 var texto = document.getElementById("re-hb-texto"); 539 var cta = document.getElementById("re-hb-cta"); 540 var ctaTexto = document.getElementById("re-hb-cta-texto"); 541 var error = document.getElementById("re-hb-error"); 542 var aviso = document.getElementById("re-hb-aviso"); 543 var criterios = document.getElementById("re-hb-criterios"); 544 var leyenda = document.getElementById("re-hb-leyenda"); 545 if (!box || !texto || !cta) return; 546 547 var enviando = false;
548 549 // Estado de la clasificación en vivo. `ultimoTexto` es la frase exacta que 550 // corresponde a `ultimosCampos`: si al enviar no ha cambiado, se reaprovecha 551 // y el envÃo no gasta una segunda llamada. 552 var temporizador = null; 553 var peticion = 0; 554 var abortador = null; 555 var ultimoTexto = ""; 556 var ultimosCampos = {}; 557 558 // Agrupa las frases a medias de una misma persona escribiendo. No identifica 559 // a nadie ni se persiste: muere al recargar, que es justo lo que se quiere. 560 var SESION = String(Math.random()).slice(2) + String(Math.random()).slice(2, 8); 561 var ultimaBusquedaId = null; // fila de `busquedas_texto` de la ultima clasificacion 562 var ciudades = []; // las de verdad, para no encender "Ciudad" con un pueblo sin nadie 563 564 function medir(evento, props) { 565 try { 566 if (window.amplitude && window.amplitude.track) window.amplitude.track(evento, props || {}); 567 } catch (e) {} 568 } 569 570 function marcar(campos) { 571 var encendidas = 0; 572 Array.prototype.forEach.call(criterios.querySelectorAll(".re-hb__crit"), function (li) { 573 var valor = campos[li.getAttribute("data-campo")]; 574 li.classList.toggle("is-ok", !!valor); 575 if (valor) { encendidas++; li.setAttribute("title", valor); } else li.removeAttribute("title"); 576 }); 577 leyenda.classList.toggle("is-visible", encendidas > 0); 578 } 579 580 function mostrarError(msg) { 581 error.textContent = msg; 582 error.classList.toggle("is-visible", !!msg); 583 } 584 585 // El mailto lleva asunto y el texto que la persona ya ha escrito: si ha 586 // contado lo que busca, que no lo tenga que volver a escribir. 587 // 588 // Los saltos de lÃnea van por fromCharCode por lo mismo que el rango de 589 // tildes de abajo: un salto escrito como escape no sobrevive al canal por el
590 // que se escribe el embed, y ahi dentro romperia la cadena, no solo el formato. 591 function mostrarAvisoReclutamiento(valor) { 592 if (!aviso) return; 593 var salto = String.fromCharCode(10) + String.fromCharCode(10); 594 var cuerpo = "Hola," + salto + "Os escribo desde la web. Esto es lo que busco:" + salto + valor + salto; 595 aviso.innerHTML = 596 "Esto lo llevamos por email. Este buscador propone entrenadores a quien busca uno para sà mismo, " + 597 "asà que si lo que necesitas es un entrenador <strong>para tu gimnasio o tu equipo</strong>, " + 598 'escrÃbenos a <a href="mailto:[email protected]?subject=' + 599 encodeURIComponent("Busco entrenador para mi centro") + 600 "&body=" + encodeURIComponent(cuerpo) + 601 '">[email protected]</a> y te contestamos nosotros.'; 602 aviso.classList.add("is-visible"); 603 } 604 605 function ocultarAviso() { 606 if (aviso) { aviso.classList.remove("is-visible"); aviso.innerHTML = ""; } 607 } 608 609 // El rango de tildes se construye con fromCharCode a propósito: escrito como 610 // clase literal, los combinantes no sobreviven al canal por el que se escribe 611 // el embed en Webflow y la comparación se queda muda. 612 var COMBINANTES = new RegExp("[" + String.fromCharCode(768) + "-" + String.fromCharCode(879) + "]", "g"); 613 614 function sinTildes(s) { 615 return (s || "").normalize("NFD").replace(COMBINANTES, "").toLowerCase(); 616 } 617 618 // La lista de ciudades se pide una sola vez (3 KB) y en cuanto el usuario 619 // toca el cuadro, no al cargar la home: es la cabecera de la página más 620 // visitada y la mayorÃa de visitas no llegan a escribir nada. 621 function cargarCiudades() { 622 if (ciudades.length) return; 623 fetch(SUPABASE_URL + "/rest/v1/entrenadores_publico?select=ciudad_nombre&tipo_entrenador=eq.personal&ciudad_nombre=not.is.null", { 624 headers: { apikey: SUPABASE_ANON_KEY, Authorization: "Bearer " + SUPABASE_ANON_KEY } 625 }) 626 .then(function (res) { return res.ok ? res.json() : []; }) 627 .then(function (filas) { 628 var vistas = {}; 629 filas.forEach(function (f) { if (f.ciudad_nombre) vistas[f.ciudad_nombre] = true; }); 630 ciudades = Object.keys(vistas); 631 }, function () {}); 632 } 633 634 function leerEnLocal(valor) { 635 var campos = { objetivo: "", modalidad: "", ciudad: "", cuando: "" }; 636 Object.keys(PISTAS).forEach(function (campo) { 637 for (var i = 0; i < PISTAS[campo].length; i++) { 638 if (PISTAS[campo][i][0].test(valor)) { campos[campo] = PISTAS[campo][i][1]; return; } 639 } 640 }); 641 var plano = sinTildes(valor); 642 for (var j = 0; j < ciudades.length; j++) { 643 if (plano.indexOf(sinTildes(ciudades[j])) !== -1) { campos.ciudad = ciudades[j]; break; } 644 } 645 return campos; 646 } 647 648 box.addEventListener("focusin", function () { box.classList.add("is-focus"); cargarCiudades(); }); 649 box.addEventListener("focusout", function () { box.classList.remove("is-focus"); }); 650 texto.addEventListener("input", function () { 651 if (error.classList.contains("is-visible")) mostrarError(""); 652 ocultarAviso(); 653 programarClasificacion(); 654 }); 655 texto.addEventListener("keydown", function (ev) { 656 if (ev.key === "Enter" && (ev.metaKey || ev.ctrlKey)) { ev.preventDefault();
656 enviar(); } 657 }); 658 659 Array.prototype.forEach.call(document.querySelectorAll("#re-hb-ejemplos .re-hb__ejemplo"), function (btn) { 660 btn.addEventListener("click", function () { 661 texto.value = btn.textContent.trim(); 662 texto.focus(); 663 mostrarError(""); 664 medir("home_buscador_ejemplo", { texto: btn.textContent.trim() }); 665 programarClasificacion(); 666 }); 667 }); 668 669 function clasificar(valor, signal) { 670 return fetch(SUPABASE_URL + "/functions/v1/clasificar-busqueda", { 671 method: "POST", 672 signal: signal, 673 headers: { 674 apikey: SUPABASE_ANON_KEY, 675 Authorization: "Bearer " + SUPABASE_ANON_KEY, 676 "Content-Type": "application/json" 677 }, 678 body: JSON.stringify({ texto: valor, sesion: SESION }) 679 }).then(function (res) { 680 if (!res.ok) throw new Error("clasificar-busqueda -> " + res.status); 681 return res.json(); 682 }); 683 } 684 685 // Marca en `busquedas_texto` la frase con la que se pulso el boton. No gasta 686 // llamada al modelo (la funcion corta antes) y es de ida: nada de lo que pasa 687 // aqui puede retrasar ni romper la apertura del pop-up. 688 function marcarEnviada(id) { 689 if (!id) return; 690 try { 691 fetch(SUPABASE_URL + "/functions/v1/clasificar-busqueda", { 692 method: "POST", 693 keepalive: true, 694 headers: { 695 apikey: SUPABASE_ANON_KEY, 696 Authorization: "Bearer " + SUPABASE_ANON_KEY, 697 "Content-Type": "application/json" 698 }, 699 body: JSON.stringify({ marcar_enviada: id }) 700 }).then(null, function () {}); 701 } catch (e) {} 702 } 703 704 // Las cuatro casillas se van encendiendo mientras la persona escribe: es lo 705 // que hace creÃble el "te lo buscamos nosotros". Se clasifica al hacer pausa, 706 // no en cada tecla, y una respuesta que llega tarde nunca pisa a otra más 707 // nueva (de ahà el contador `peticion`). 708 function programarClasificacion() { 709 clearTimeout(temporizador); 710 var valor = texto.value.trim(); 711 if (valor.length < MIN_CARACTERES) { 712 peticion++; // descarta lo que estuviera en vuelo 713 ultimoTexto = ""; 714 ultimosCampos = {}; 715 marcar({}); 716 return; 717 } 718 if (valor === ultimoTexto) return; // ya sabemos qué dice esta frase 719 marcar(leerEnLocal(valor)); // algo se enciende ya, sin esperar a nadie 720 temporizador = setTimeout(clasificarEnVivo, PAUSA); 721 } 722 723 function clasificarEnVivo() { 724 var valor = texto.value.trim(); 725 if (valor.length < MIN_CARACTERES || valor === ultimoTexto) return; 726 727 var mia = ++peticion; 728 if (abortador) { try { abortador.abort(); } catch (e) {} } 729 abortador = ("AbortController" in window) ? new AbortController() : null; 730 731 clasificar(valor, abortador ? abortador.signal : undefined).then(function (campos) { 732 if (mia !== peticion) return; // ha seguido escribiendo: manda la última 733 ultimoTexto = valor; 734 ultimosCampos = campos; 735 ultimaBusquedaId = campos.busqueda_id || null; 736 marcar(campos); 737 }, function () { 738 // En vivo el fallo es mudo a propósito: nadie ha pedido nada todavÃa y un 739 // aviso rojo mientras se escribe solo asusta. Al enviar sà se avisa. 740 }); 741 } 742 743 // Deja las respuestas escritas en el estado real del pop-up y lo abre. Si no 744 // está cargado (o falla), devuelve false y el que llama se va al listado. 745 function abrirPopUp(campos) { 746 var nucleo = window.__reRecNucleo; 747 var api = window.reRecomendador; 748 if (!nucleo || !nucleo.estado || !api || !api.abrir) return false; 749 try { 750 var estado = nucleo.estado; 751 var sabidos = estado.sabidos || (estado.sabidos = {}); 752 753 if (campos.objetivo) { 754 estado.objetivo = campos.objetivo; 755 estado.objetivoRespondido = true; 756 sabidos.objetivo = true; 757 } 758 if (campos.modalidad) estado.modalidad = campos.modalidad; 759 if (campos.ciudad) estado.ciudad = campos.ciudad; 760 if (campos.cuando) { estado.cuando = campos.cuando; sabidos.cuando = true; } 761 762 // "Dónde" son dos preguntas en una pantalla: solo se puede dar por sabida 763 // si online, o si presencial con ciudad. El parche de pasos lo vuelve a 764 // comprobar, pero marcarlo mal aquà serÃa pedirle que se salte una 765 // pantalla con un campo a medias. 766 if (estado.modalidad && (estado.modalidad === "Online" || estado.ciudad)) { 767 sabidos.donde = true; 768 } 769 770 api.abrir(); 771 return true; 772 } catch (e) { 773 return false;
774 } 775 } 776 777 function alListado(campos) { 778 var p = new URLSearchParams(); 779 if (campos.objetivo) p.set("objetivo", campos.objetivo); 780 if (campos.modalidad) p.set("modalidad", campos.modalidad); 781 if (campos.ciudad) p.set("ciudad", campos.ciudad); 782 var q = p.toString(); 783 location.href = LISTADO + (q ? "?" + q : ""); 784 } 785 786 function enviar() { 787 if (enviando) return; 788 var valor = texto.value.trim(); 789 if (valor.length < MIN_CARACTERES) { 790 mostrarError("Cuéntanos un poco más: qué quieres conseguir y dónde te gustarÃa entrenar."); 791 texto.focus(); 792 return; 793 } 794 795 // Antes de nada: si esto es un gimnasio buscando a quién contratar, no se 796 // abre el reparto. Se le da el email y ahà acaba â la frase ya está 797 // guardada por la clasificación en vivo, y se marca como enviada porque sà 798 // pulsó el botón: si no, en `busquedas_texto` parecerÃa que se fue sin 799 // llegar a pedir nada. 800 if (esReclutamiento(valor)) { 801 clearTimeout(temporizador); 802 mostrarError(""); 803 mostrarAvisoReclutamiento(valor); 804 marcarEnviada(ultimaBusquedaId); 805 medir("home_buscador_reclutamiento", { caracteres: valor.length }); 806 return; 807 } 808 809 clearTimeout(temporizador); 810 enviando = true; 811 cta.setAttribute("disabled", "disabled"); 812 ctaTexto.textContent = "Buscandoâ¦"; 813 mostrarError(""); 814 ocultarAviso(); 815 medir("home_buscador_enviado", { caracteres: valor.length }); 816 817 // Si la frase es la misma que ya clasificamos mientras escribÃa, no se 818 // vuelve a preguntar: el envÃo sale gratis y es instantáneo. 819 var reutiliza = valor === ultimoTexto; 820 var pendiente = reutiliza ? Promise.resolve(ultimosCampos) : clasificar(valor); 821 822 pendiente 823 // Si la IA se cae, se manda lo que se entendió en local en vez de nada: 824 // el reparto sale peor afinado, pero sale. 825 .catch(function () { 826 var local = leerEnLocal(valor); 827 local.error = true; 828 return local; 829 }) 830 .then(function (campos) { 831 // Cual de todas las frases de esta sesion es la que se envio. Se avisa 832 // y se sigue: si la marca se pierde, el reparto no se entera. 833 marcarEnviada(campos.busqueda_id || ultimaBusquedaId); 834 835 var entendidos = ["objetivo", "modalidad", "ciudad", "cuando"].filter(function (k) { return !!campos[k]; }); 836 marcar(campos); 837 medir("home_buscador_clasificado", { 838 entendidos: entendidos, 839 cuantos: entendidos.length, 840 fallo: !!campos.error, 841 reutilizada: reutiliza 842 }); 843 if (!abrirPopUp(campos)) alListado(campos); 844 }) 845 .then(null, function () { 846 mostrarError("No hemos podido procesar tu búsqueda. Inténtalo otra vez en un momento."); 847 }) 848 .then(function () { 849 enviando = false; 850 cta.removeAttribute("disabled"); 851 ctaTexto.textContent = "Recomiéndame un entrenador"; 852 }); 853 } 854 855 cta.addEventListener("click", enviar); 856})(); 857</script>
857 858</div></div></div><a href="/pregunta-al-experto" class="banner-pae-oscuro w-inline-block"><div class="banner-pae-oscuro_grid"><div class="banner-pae-oscuro_texto"><div class="banner-pae-oscuro_estado"><div class="banner-pae-oscuro_punto"></div><div class="banner-pae-oscuro_eyebrow">Entrenadores respondiendo ahora</div></div><h2 class="banner-pae-oscuro_titulo">Pregunta a un entrenador personal verificado</h2><p class="banner-pae-oscuro_cuerpo">Escribe tu duda y varios entrenadores te responden, casi siempre el mismo dÃa. Gratis y sin registro.</p><div class="banner-pae-oscuro_cta">Escribe tu pregunta gratis<svg fill="none" viewBox="0 0 18 14" height="12" width="16" class="banner-pae-oscuro_flecha"><path stroke-linecap="round" stroke-width="2" stroke="#ffffff" d="M1 7h15m0 0-5.5-5.5M16 7l-5.5 5.5"></path></svg></div></div><div class="banner-pae-oscuro_lateral"><div class="banner-pae-oscuro_caja"><div class="banner-pae-oscuro_caja-label">¿Cuál es tu duda?</div><div class="banner-pae-oscuro_caja-input">¿Cuántos dÃas a la semana debo entrenar?</div><div class="banner-pae-oscuro_caja-boton">Continuar</div></div><div class="banner-pae-oscuro_cifras"><div class="banner-pae-oscuro_cifra"><div class="banner-pae-oscuro_cifra-dato">20</div><div class="banner-pae-oscuro_cifra-nota">respuestas de media</div></div><div class="banner-pae-oscuro_cifra"><div class="banner-pae-oscuro_cifra-dato">3 horas</div><div class="banner-pae-oscuro_cifra-nota">hasta la primera</div></div><div class="banner-pae-oscuro_cifra"><div class="banner-pae-oscuro_cifra-dato">35</div><div class="banner-pae-oscuro_cifra-nota">entrenadores han respondido</div></div></div></div></div></a></section><section class="como-funciona-v2_section"><div class="como-funciona-v2_wrap"><div class="como-funciona-v2_header"><p class="como-funciona-v2_eyebrow">Cómo funciona</p><h2 class="como-funciona-v2_title">Cómo encontrar un entrenador personal </h2><p class="como-funciona-v2_subtitle">Tres pasos para empezar a entrenar con el profesional que mejor encaje con tus objetivos</p></div><div class="como-funciona-v2_grid"><div class="como-funciona-v2_card"><div class="como-funciona-v2_media"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0e00e943fd5e31fa7a3_como-funciona-01-encuentra-entrenador.jpg" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0e00e943fd5e31fa7a3_como-funciona-01-encuentra-entrenador-p-500.jpg 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0e00e943fd5e31fa7a3_como-funciona-01-encuentra-entrenador.jpg 800w" alt="Grupo de personas entrenando al aire libre con conos y steps en una sesión dirigida" class="como-funciona-v2_img"/></div><div class="como-funciona-v2_body"><div class="como-funciona-v2_number">01</div><h3 class="como-funciona-v2_card-title">Busca tu entrenador</h3><p class="como-funciona-v2_card-text">Explora perfiles verificados, filtra por zona de <span>tu ciudad</span><span>, modalidad y objetivo. O, si lo prefieres, usa uno de nuestros recomendadores.</span></p></div></div><div class="como-funciona-v2_card"><div class="como-funciona-v2_media"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0e7baa0ae41225f3d23_como-funciona-02-contacta-gratis.jpg" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0e7baa0ae41225f3d23_como-funciona-02-contacta-gratis-p-500.jpg 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0e7baa0ae41225f3d23_como-funciona-02-contacta-gratis.jpg 800w" alt="Mujer de espaldas sujetando una barra cargada en un rack de sentadillas" class="como-funciona-v2_img"/></div><div class="como-funciona-v2_body"><div class="como-funciona-v2_number">02</div><h3 class="como-funciona-v2_card-title">Contacta gratis</h3><p class="como-funciona-v2_card-text">EscrÃbele directamente a través de nuestra plataforma. Sin intermediarios, sin costes ocultos. Solo tú y tu entrenador.</p></div></div><div class="como-funciona-v2_card"><div class="como-funciona-v2_media"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0f0d8771ef2f9e60ac1_como-funciona-03-logra-tus-objetivos.jpg" loading="lazy" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0f0d8771ef2f9e60ac1_como-funciona-03-logra-tus-objetivos-p-500.jpg 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/6a79c0f0d8771ef2f9e60ac1_como-funciona-03-logra-tus-objetivos.jpg 800w" alt="Tres corredores en silueta sobre el asfalto a contraluz al amanecer" class="como-funciona-v2_img"/></div><div class="como-funciona-v2_body"><div class="como-funciona-v2_number">03</div><h3 class="como-funciona-v2_card-title">Logra tus objetivos</h3><p class="como-funciona-v2_card-text">Comienza a entrenar con metodologÃa probada y seguimiento continuo. Los resultados llegan con constancia y el profesional adecuado.</p></div></div></div><a href="/entrenador-personal" class="como-funciona-v2_cta w-inline-block">Ver entrenadores en <span>tu ciudad</span></a></div></section><section class="banner-destacados"><div class="banner-destacados_cabecera"><div class="banner-destacados_titulos"><div class="banner-destacados_eyebrow">Entrenadores destacados</div><h2 class="banner-destacados_titulo">Entrenadores destacados<span class="banner-destacados_punto">.</span></h2></div><div class="banner-destacados_intro-col"><p class="banner-destacados_intro">Perfiles completos, con titulaciones verificadas y opiniones reales de sus clientes. Contactar es gratis.</p><a href="/entrenador-personal" class="banner-destacados_cta w-inline-block">Ver todos los entrenadores<svg fill="none" viewBox="0 0 18 14" height="12" width="16" class="banner-destacados_flecha-cta"><path stroke-linecap="round" stroke-width="2" stroke="#ffffff" d="M1 7h15m0 0-5.5-5.5M16 7l-5.5 5.5"></path></svg></a></div></div><div class="w-embed w-script"><!-- ============================================================ 859 RedEntrenadores â rejilla del banner "Entrenadores destacados" 860 861 Cuatro tarjetas de entrenador: primero los del plan Destacado y, cuando 862 no llegan a llenar la rejilla, los perfiles más completos detrás 863 (RPC buscar_entrenadores_listado con p_orden = 'destacados', añadido en 864 supabase/migrations/20260824201904_listado_orden_destacados.sql). 865 866 Hasta el 24/08/2026 este banner pedÃa p_orden = 'calidad', que ordena
867 por porcentaje_calidad. Con ese orden la sección era "los cuatro 868 perfiles más completos" y no "los entrenadores destacados": el +35 de 869 plan vive en score_prioridad, que en 'calidad' es el quinto criterio de 870 desempate y no se alcanza nunca. El dÃa que se activó el primer 871 destacado, la home seguÃa enseñando cuatro perfiles del plan estándar 872 mientras él pagaba por "aparecer destacado en la home". 873 874 Vive DENTRO del componente de Webflow "Banner Entrenadores destacados" 875 (grupo Banners), como único hijo del embed que sustituye a la rejilla. 876 La cabecera del banner âeyebrow, H2, párrafo y CTA "Ver todos los 877 entrenadores"â NO está aquÃ: son elementos nativos de Webflow con 878 clases banner-destacados_*, para que el copy y el H2 se editen en el 879 Designer. Aquà solo vive lo que tiene que salir de la base de datos. 880 881 Por eso el CSS de las tarjetas va en este <style> y no en clases de 882 Webflow: Webflow solo publica el CSS de las clases que usa algún 883 elemento del sitio, y estas tarjetas las pinta el JS. Es el mismo 884 motivo por el que existe el componente "Listado de entrenadores 885 (estilos)". 886 887 Qué se enseña en cada tarjeta y de dónde sale: 888 dorsal 01-04 posición en el resultado, no un dato del entrenador 889 foto foto_url a 1:1 (object-fit: cover); si no hay, las 890 iniciales sobre gris, como en el listado 891 nota + (n) rating_promedio y total_reviews. Sin opiniones no se 892 pinta la cápsula: un "0" en un banner de destacados 893 resta más de lo que informa 894 nombre/ciudad nombre y ciudad_nombre 895 bio titulo_perfil (una lÃnea, el mismo texto que el listado) 896 pÃldoras tipo_atencion[0] y especialidades[0] (§50) 897 "Solo online" badges contiene `online_total` (lo calcula la vista 898 `entrenadores_publico`). Va encima de la foto, no 899 entre las pÃldoras: quien llega desde una página de 900 ciudad da por hecho que entrena presencial 901 902 Ajustes opcionales, en un data-* del propio div o de cualquier padre 903 (sirve envolver la instancia del componente en un div): 904 data-ciudad="Valencia" filtra por ciudad (casa por nombre, no por 905 slug, igual que el resto de widgets) 906 data-limite="8" cuántas tarjetas pedir (por defecto 4) 907 908 Si Supabase falla o no devuelve a nadie se esconde el banner ENTERO, 909 no solo la rejilla: la cabecera se quedarÃa con un "Ver todos los 910 entrenadores" colgando de una sección vacÃa. 911 ============================================================ --> 912 913<div id="re-dest" class="re-dest" hidden></div> 914 915<style> 916 #re-dest { 917 display: grid; 918 grid-template-columns: repeat(4, minmax(0, 1fr)); 919 gap: 16px; 920 box-sizing: border-box; 921 width: 100%; 922 max-width: 1260px; 923 margin: 0 auto; 924 padding: 0 25px 56px; 925 font-family: 'Plus Jakarta Sans', sans-serif; 926 } 927 928 /* Los cortes son los de Webflow (991 / 479) para que la rejilla respire 929 igual que el resto del sitio, no los 1000/560 del diseño original. */ 930 @media screen and (max-width: 991px) { 931 #re-dest { grid-template-columns: repeat(2, minmax(0, 1fr)); } 932 } 933 @media screen and (max-width: 479px) { 934 #re-dest { grid-template-columns: minmax(0, 1fr); } 935 } 936 937 /* ------------------------------------------------------------ tarjeta */ 938 939 #re-dest .re-dest-tarjeta { 940 display: flex; 941 flex-direction: column; 942 min-width: 0; 943 background: #ffffff; 944 border: 1px solid #d9dde2; 945 border-radius: 12px; 946 box-shadow: 0 4px 14px rgba(0, 0, 0, .09); 947 overflow: hidden; 948 text-decoration: none; 949 color: inherit; 950 transition: box-shadow .18s ease, transform .18s ease; 951 } 952 #re-dest a.re-dest-tarjeta:hover { 953 box-shadow: 0 8px 22px rgba(0, 0, 0, .13); 954 transform: translateY(-2px); 955 } 956 957 #re-dest .re-dest-foto { 958 position: relative; 959 width: 100%; 960 aspect-ratio: 1 / 1; 961 background: #F3F3F3; 962 border-bottom: 1px solid #d9dde2; 963 } 964 #re-dest .re-dest-foto img { 965 display: block; 966 width: 100%; 967 height: 100%; 968 object-fit: cover; 969 } 970 #re-dest .re-dest-iniciales { 971 display: flex; 972 width: 100%; 973 height: 100%; 974 align-items: center; 975 justify-content: center; 976 font-family: 'Barlow', sans-serif; 977 font-size: 44px; 978 font-weight: 800; 979 color: #9ca3af; 980 } 981 982 #re-dest .re-dest-orden { 983 position: absolute; 984 left: 0; 985 top: 0; 986 padding: 6px 12px; 987 background: #089F3B; 988 color: #ffffff; 989 font-family: 'Barlow', sans-serif; 990 font-size: 14px; 991 font-weight: 800; 992 letter-spacing: .03em; 993 } 994
995 /* Badge "Solo online". Arriba a la derecha: arriba-izquierda va el dorsal y 996 abajo-derecha la nota. Misma pastilla blanca con borde que .re-dest-rating 997 y no verde, porque informa de la modalidad, no premia nada (§50). */ 998 #re-dest .re-dest-online { 999 position: absolute; 1000 right: 12px; 1001 top: 12px; 1002 display: inline-flex; 1003 align-items: center; 1004 gap: 5px; 1005 padding: 5px 10px; 1006 background: #ffffff; 1007 border: 1px solid #d9dde2; 1008 border-radius: 999px; 1009 box-shadow: 0 2px 6px rgba(0,0,0,.18); 1010 font-size: 12px; 1011 font-weight: 700; 1012 color: #4B5563; 1013 white-space: nowrap; 1014 } 1015 #re-dest .re-dest-online svg { width: 13px; height: 13px; display: block; flex-shrink: 0; } 1016 1017 #re-dest .re-dest-rating { 1018 position: absolute; 1019 right: 12px; 1020 bottom: 12px; 1021 display: inline-flex; 1022 align-items: center; 1023 gap: 5px; 1024 padding: 5px 10px; 1025 background: #ffffff; 1026 border: 1px solid #d9dde2; 1027 border-radius: 999px; 1028 } 1029 #re-dest .re-dest-estrella { display: block; } 1030 #re-dest .re-dest-nota { font-size: 13px; font-weight: 700; color: #1B1E22; } 1031 /* #6b7280 y no #9ca3af: el gris claro no llega a AA como texto (§1.3). */ 1032 #re-dest .re-dest-num { font-size: 12px; color: #6b7280; } 1033 1034 #re-dest .re-dest-cuerpo { 1035 display: flex; 1036 flex: 1; 1037 flex-direction: column; 1038 gap: 10px; 1039 padding: 18px 20px 22px; 1040 } 1041 #re-dest .re-dest-identidad { display: flex; flex-direction: column; } 1042 #re-dest .re-dest-nombre { 1043 margin: 0; 1044 font-family: 'Barlow', sans-serif; 1045 font-size: 22px; 1046 line-height: 1.15; 1047 font-weight: 700; 1048 color: #1B1E22; 1049 } 1050 #re-dest .re-dest-ciudad { font-size: 13px; color: #6b7280; } 1051 #re-dest .re-dest-bio { 1052 margin: 0; 1053 font-size: 14px; 1054 line-height: 21px; 1055 color: #4B5563; 1056 } 1057 1058 #re-dest .re-dest-pills { display: flex; flex-wrap: wrap; gap: 6px; } 1059 #re-dest .re-dest-pill { 1060 padding: 4px 11px; 1061 background: #F3F4F6; 1062 border-radius: 999px; 1063 font-size: 12px; 1064 font-weight: 600; 1065 color: #4B5563; 1066 } 1067 1068 #re-dest .re-dest-ver { 1069 display: inline-flex; 1070 align-items: center; 1071 align-self: flex-start; 1072 gap: 8px; 1073 margin-top: auto; 1074 padding-top: 10px; 1075 font-size: 13px; 1076 font-weight: 600; 1077 text-transform: uppercase; 1078 letter-spacing: .03em; 1079 color: #089F3B; 1080 } 1081 1082 /* ---------------------------------------------------------- esqueleto */ 1083 1084 #re-dest .re-dest-skel .re-dest-foto, 1085 #re-dest .re-dest-sk { 1086 background: #F3F3F3; 1087 animation: re-dest-latido 1.4s ease-in-out infinite; 1088 } 1089 #re-dest .re-dest-sk { border-radius: 6px; height: 12px; } 1090 #re-dest .re-dest-sk-nombre { width: 60%; height: 20px; } 1091 #re-dest .re-dest-sk-ciudad { width: 35%; } 1092 #re-dest .re-dest-sk-bio { width: 100%; } 1093 #re-dest .re-dest-sk-bio2 { width: 80%; } 1094 @keyframes re-dest-latido { 0%, 100% { opacity: 1; } 50% { opacity: .55; } } 1095 @media (prefers-reduced-motion: reduce) { 1096 #re-dest .re-dest-skel .re-dest-foto, 1097 #re-dest .re-dest-sk { animation: none; } 1098 #re-dest .re-dest-tarjeta { transition: none; } 1099 } 1100</style> 1101
1102<script> 1103(function () { 1104 var SUPABASE_URL = "https://yrzanatzshdpvypazgeb.supabase.co"; 1105 var SUPABASE_ANON_KEY = "sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD"; 1106 var SUPABASE_UMD = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js"; 1107 var PERFIL_BASE = "/entrenador-personal/"; 1108 var POR_DEFECTO = 4; 1109 1110 var rejilla = document.getElementById('re-dest'); 1111 if (!rejilla) return; 1112 1113 // La cabecera (eyebrow, H2, CTA) es nativa de Webflow y envuelve a este 1114 // embed. Si no hay entrenadores que enseñar hay que esconder la sección 1115 // entera, no solo la rejilla. 1116 var banner = rejilla.closest ? rejilla.closest('.banner-destacados') : null; 1117 1118 function esc(s) { 1119 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 1120 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 1121 }); 1122 } 1123 1124 function ocultar() { (banner || rejilla).setAttribute('hidden', ''); } 1125 1126 // closest() se incluye a sà mismo, asà que el data-* vale tanto en este 1127 // div como en cualquier padre que envuelva la instancia del componente. 1128 function ajuste(nombre) { 1129 var host = rejilla.closest ? rejilla.closest('[data-' + nombre + ']') : null; 1130 return host ? (host.getAttribute('data-' + nombre) || '').trim() : ''; 1131 } 1132 1133 var ciudad = ajuste('ciudad'); 1134 var limite = parseInt(ajuste('limite'), 10); 1135 if (!(limite > 0)) limite = POR_DEFECTO; 1136 1137 // ---------------------------------------------------------- esqueleto 1138 1139 function esqueleto() { 1140 var una = '<div class="re-dest-tarjeta re-dest-skel" aria-hidden="true">' + 1141 '<div class="re-dest-foto"></div>' + 1142 '<div class="re-dest-cuerpo">' + 1143 '<div class="re-dest-sk re-dest-sk-nombre"></div>' + 1144 '<div class="re-dest-sk re-dest-sk-ciudad"></div>' + 1145 '<div class="re-dest-sk re-dest-sk-bio"></div>' + 1146 '<div class="re-dest-sk re-dest-sk-bio2"></div>' + 1147 '</div>' + 1148 '</div>'; 1149 rejilla.innerHTML = new Array(limite + 1).join(una); 1150 } 1151 1152 // ------------------------------------------------------------ pintado 1153 1154 var ESTRELLA = '<svg class="re-dest-estrella" width="13" height="13" viewBox="0 0 20 20" aria-hidden="true" focusable="false">' + 1155 '<path d="M10 1.6l2.5 5.3 5.7.8-4.1 4 1 5.7L10 14.7 4.9 17.4l1-5.7-4.1-4 5.7-.8L10 1.6Z" fill="#089F3B"></path></svg>'; 1156 1157 var ICONO_PANTALLA = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' + 1158 '<rect x="2.6" y="4" width="18.8" height="12.8" rx="2"></rect><path d="M9 20.4h6"></path></svg>'; 1159
1160 var FLECHA = '<svg width="14" height="11" viewBox="0 0 18 14" fill="none" aria-hidden="true" focusable="false">' + 1161 '<path d="M1 7h15m0 0-5.5-5.5M16 7l-5.5 5.5" stroke="#089F3B" stroke-width="2" stroke-linecap="round"></path></svg>'; 1162 1163 // "5" y "4,5", como el diseño: sin decimal cuando es redondo y con coma 1164 // cuando no, que es como se escribe una nota en español. 1165 function nota(n) { 1166 var v = Number(n) || 0; 1167 return (Math.round(v * 10) % 10 === 0 ? String(Math.round(v)) : v.toFixed(1)).replace('.', ','); 1168 } 1169 1170 function ratingHTML(t) { 1171 if (!t.total_reviews) return ''; 1172 return '<div class="re-dest-rating">' + ESTRELLA + 1173 '<span class="re-dest-nota">' + esc(nota(t.rating_promedio)) + '</span>' + 1174 '<span class="re-dest-num">(' + esc(t.total_reviews) + ')</span>' + 1175 '</div>'; 1176 } 1177 1178 function iniciales(nombre) { 1179 return (nombre || '?').trim().split(/\s+/).slice(0, 2).map(function (p) { 1180 return p.charAt(0); 1181 }).join('').toUpperCase(); 1182 } 1183 1184 function fotoHTML(t) { 1185 if (!t.foto_url) { 1186 return '<div class="re-dest-iniciales" aria-hidden="true">' + esc(iniciales(t.nombre)) + '</div>'; 1187 } 1188 return '<img src="' + esc(t.foto_url) + '" alt="' + esc(t.nombre) + ', entrenador personal' + 1189 (t.ciudad_nombre ? ' en ' + esc(t.ciudad_nombre) : '') + '" loading="lazy" />'; 1190 } 1191 1192 function onlineHTML(t) { 1193 // La regla "solo online" la calcula la vista `entrenadores_publico` 1194 // (columna `badges`, código `online_total`, migración 20260825201550), no 1195 // este widget: asà la ficha, el listado y esta rejilla no pueden acabar 1196 // diciendo cosas distintas del mismo entrenador. 1197 if (!t.badges || t.badges.indexOf('online_total') === -1) return ''; 1198 return '<span class="re-dest-online" title="Solo entrena online: no atiende presencial">' + ICONO_PANTALLA + 'Solo online</span>'; 1199 } 1200 1201 function pillsHTML(t) { 1202 var pills = []; 1203 if (t.tipo_atencion && t.tipo_atencion.length) pills.push(t.tipo_atencion[0]); 1204 if (t.especialidades && t.especialidades.length) pills.push(t.especialidades[0]); 1205 if (!pills.length) return ''; 1206 return '<div class="re-dest-pills">' + pills.map(function (p) { 1207 return '<span class="re-dest-pill">' + esc(p) + '</span>'; 1208 }).join('') + '</div>'; 1209 } 1210 1211 function tarjetaHTML(t, i) { 1212 var dorsal = (i + 1 < 10 ? '0' : '') + (i + 1); 1213 return '<a class="re-dest-tarjeta" href="' + PERFIL_BASE + encodeURIComponent(t.slug) + '">' + 1214 '<div class="re-dest-foto">' + 1215 fotoHTML(t) + 1216 '<div class="re-dest-orden">' + dorsal + '</div>' + 1217 onlineHTML(t) + 1218 ratingHTML(t) + 1219 '</div>' + 1220 '<div class="re-dest-cuerpo">' + 1221 '<div class="re-dest-identidad">' + 1222 '<p class="re-dest-nombre">' + esc(t.nombre) + '</p>' + 1223 (t.ciudad_nombre ? '<span class="re-dest-ciudad">' + esc(t.ciudad_nombre) + '</span>' : '') + 1224 '</div>' + 1225 (t.titulo_perfil ? '<p class="re-dest-bio">' + esc(t.titulo_perfil) + '</p>' : '') + 1226 pillsHTML(t) + 1227 '<span class="re-dest-ver">Ver perfil' + FLECHA + '</span>' + 1228 '</div>' + 1229 '</a>'; 1230 } 1231 1232 // -------------------------------------------------------------- datos 1233 1234 function conSupabase(hecho, fallo) { 1235 if (window.supabase && window.supabase.createClient) return hecho(); 1236 var s = document.querySelector('script[data-re-supabase]'); 1237 if (!s) { 1238 s = document.createElement('script'); 1239 s.src = SUPABASE_UMD; 1240 s.setAttribute('data-re-supabase', '1'); 1241 document.head.appendChild(s); 1242 } 1243 s.addEventListener('load', hecho); 1244 s.addEventListener('error', fallo); 1245 } 1246 1247 function cargar() { 1248 var cliente = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); 1249 cliente.rpc('buscar_entrenadores_listado', { 1250 p_objetivo: null, 1251 p_modalidad: null, 1252 p_ciudad: ciudad || null, 1253 p_perfil: null, 1254 p_precio_min: null, 1255 p_precio_max: null, 1256 p_orden: 'destacados', 1257 p_limite: limite, 1258 p_offset: 0, 1259 p_solo_contactables: false 1260 }).then(function (res) {
1261 if (res && res.error) return ocultar(); 1262 var filas = (res && res.data) || []; 1263 if (!filas.length) return ocultar(); 1264 rejilla.innerHTML = filas.map(tarjetaHTML).join(''); 1265 }).catch(ocultar); 1266 } 1267 1268 function arrancar() { 1269 rejilla.removeAttribute('hidden'); 1270 esqueleto(); 1271 conSupabase(cargar, ocultar); 1272 } 1273 1274 if (document.readyState === 'loading') { 1275 document.addEventListener('DOMContentLoaded', arrancar); 1276 } else { 1277 arrancar(); 1278 } 1279})(); 1280</script>
1280</div></section><div class="w-embed w-script"><!-- ============================================================ 1281 RedEntrenadores â slider de opiniones ("Entrenadores para 1282 personas como tú") 1283 1284 Sustituye al componente de Webflow "Slider_opiniones", donde las 1285 cinco tarjetas estaban escritas a mano desde febrero de 2026: las 1286 mismas cinco opiniones, los mismos cinco entrenadores, con los 1287 enlaces al perfil puestos a dedo. Aquà salen de Supabase y son 1288 SIEMPRE las cinco últimas recibidas, asà que el bloque se pone al 1289 dÃa solo cada vez que un cliente deja una opinión. 1290 1291 Las medidas no son de un prototipo: están leÃdas del propio 1292 componente de Webflow (clases y variables del sitio) para que el 1293 cambio no se note. Equivalencias, por si hay que volver a 1294 comparar: 1295 1296 seccion .general-seccion.slider â padding 0 2% (4% en 1297 móvil), overflow hidden 1298 cabecera .flex-center + .heading-h2 â Barlow 34/28/24px, 1299 color Black_RE, margin-top 40px 1300 carrusel .slider-reviews â max-width 1260px, 1301 margin 36px auto 140px 1302 ancho tarjeta .reviews-mask â 30% / 45% / 50% / 90% 1303 tarjeta .wrap_reviews â fondo Green_RE, texto 1304 BG-white_RE, padding 24px, radio 8px, 1305 line-height 135%, margin-right 24px 1306 nombre .name-review â 600, line-height 120% 1307 fecha no existÃa en el componente (ver abajo) 1308 cita .decor-curs â cursiva 1309 pie .reviews-entrenador â pastilla blanca, padding 1310 6px 8px, radio 4px, gap 11px, peso 500 1311 "ver perfil" .link-green â 12px, mayúsculas, subrayado, 1312 Green_RE 1313 estrellas asset "5 stars.svg" (93Ã23, #FEE649), redibujado 1314 aquà como SVG en lÃnea 1315 1316 Los colores y las dos familias tipográficas se piden con las 1317 variables del sitio (--_apps---colors--green_re, etc.) y llevan el 1318 valor actual como respaldo: si mañana se cambia la variable en 1319 Webflow, el embed cambia con ella. Ojo: son las del sitio 1320 (#009F3C), un pelo distintas del verde del design system 1321 (#089F3B); manda el componente que se está sustituyendo. 1322 1323 --------------------------------------------------------------- 1324 De dónde salen las opiniones 1325 1326 De la RPC `ultimas_opiniones` (migración 1327 20260823200000_ultimas_opiniones.sql), no de un select a `reviews`: 1328 hace falta el nombre, el slug y la foto del entrenador para 1329 enlazar, y eso vive en `entrenadores_publico`. 1330 1331 La RPC ya decide qué entra (moderadas, con texto de 40+ caracteres, 1332 con nombre y con slug) y sirve una por entrenador, porque las 1333 opiniones no llegan repartidas: once de las quince últimas eran del 1334 mismo entrenador y sin ese tope el slider de la home habrÃa sido 1335 cinco veces la misma persona. Aquà solo se pinta. 1336 1337 Se puede ajustar sin tocar código, metiendo la instancia dentro de 1338 un div con: 1339 1340 data-titulo="..." el H2 (por defecto, el del componente) 1341 data-limite="5" cuántas tarjetas 1342 data-min-estrellas="4" deja fuera las notas bajas (por defecto 1343 no filtra: son "las últimas") 1344 data-por-entrenador="0" quita el tope de una por entrenador 1345 1346 --------------------------------------------------------------- 1347 Tres decisiones propias 1348 1349 - Las estrellas son las de verdad. El componente pinta siempre el 1350 SVG de cinco estrellas; aquÃ, con una opinión de 4, se pintan 1351 cuatro amarillas y una apagada. Con datos reales, mentir en la 1352 nota no es una opción. 1353 - La cita se recorta a 8 lÃneas con line-clamp. Las opiniones 1354 reales van de 40 a 900 caracteres y una sola larga estiraba la 1355 tarjeta (y con ella toda la fila). El texto entero sigue en el 1356 DOM. 1357 - Sin foto del entrenador se pinta el cÃrculo con iniciales del 1358 design system (§16), no un hueco.
1359 - Cada tarjeta lleva la fecha de la opinión, a la derecha del 1360 nombre y en <time> con datetime. El componente no la tenÃa, y 1361 sin ella el bloque dice "las últimas" sin manera de 1362 comprobarlo: media sección se sostiene sobre que las opiniones 1363 sean recientes. Formato "23 ago 2026", el mismo que las fichas 1364 de entrenador y el panel de opiniones. 1365 Va en blanco entero, no en blanco translúcido: sobre el verde 1366 de marca el texto ya está en 3,4:1 (§1.3 del design system) y 1367 bajarle la opacidad lo dejarÃa por debajo de 2,5:1. La 1368 jerarquÃa la marcan el tamaño (12px) y el peso (500). 1369 1370 Y dos cosas que cambian respecto del componente, a propósito: 1371 1372 - El H2 va centrado también por encima de 1260px. En el 1373 componente el tÃtulo cuelga de .wrap_general (alineado a la 1374 izquierda) mientras el slider va centrado, asà que en pantallas 1375 anchas quedaban desalineados unos 60px. 1376 - Las flechas del slider estaban ocultas (.none) y los puntos 1377 eran los de Webflow, blancos sobre fondo blanco. Aquà los 1378 puntos son gris borde / verde el activo, y el carrusel se puede 1379 arrastrar, girar con el teclado y hacer scroll lateral. 1380 1381 NO lleva marcado Review de schema.org, por lo mismo que el bloque 1382 de testimonios de ciudad: son opiniones "self-serving" para Google 1383 y el itemReviewed serÃa cada entrenador, que ya lo lleva en su 1384 ficha. 1385 1386 --------------------------------------------------------------- 1387 Ojo con los tags: el sitio los tiene estilados 1388 1389 La primera versión de este embed usaba <blockquote>, <figure> y 1390 <figcaption> por semántica y salió reventada en producción, porque 1391 la plantilla de Webflow trae estilos de TAG que se cuelan dentro 1392 del embed: 1393 1394 blockquote padding 64px 118px, fondo verde, 28px/700 centrado 1395 figcaption margin-top 16px, text-align center 1396 figure margin 24px 0 48px 1397 body 16px, line-height 1.625em, Plus Jakarta Sans 1398 1399 Por eso aquà solo hay <div>, <a>, <span>, <img>, un <h2> y un 1400 <time> âcasi los mismos tags que usaba el componenteâ y encima va 1401 un reset de los tags que sà se usan. Regla para el futuro: dentro 1402 de un embed, todo lo que se herede del sitio se declara a mano. 1403 Nada de "esto ya lo hereda bien": lo que hoy hereda bien, mañana lo 1404 pisa un estilo de tag. La semántica se conserva con role/aria, que 1405 no la puede pisar ningún CSS. 1406 1407 El código vive en widgets/widget-opiniones-recientes.html del repo. 1408 ============================================================ --> 1409 1410<section id="re-opiniones" class="re-op" aria-label="Opiniones de clientes" hidden> 1411 <div class="re-op-cabecera"> 1412 <h2 class="re-op-titulo" id="re-op-titulo">Entrenadores para personas como tú</h2> 1413 </div> 1414 1415 <div class="re-op-carrusel"> 1416 <div class="re-op-pista" id="re-op-pista" tabindex="0" role="list" 1417 aria-labelledby="re-op-titulo"></div> 1418 <div class="re-op-puntos" id="re-op-puntos" role="tablist" 1419 aria-label="Ir a la opinión" hidden></div> 1420 </div> 1421</section> 1422 1423<style> 1424 #re-opiniones { 1425 display: flex; 1426 flex-direction: column; 1427 padding: 0 2%; 1428 overflow: hidden; 1429 font-family: var(--_apps---typography--body-font, 'Plus Jakarta Sans'), system-ui, -apple-system, 'Segoe UI', sans-serif; 1430 color: var(--_apps---colors--black_re, #1C1F23); 1431 } 1432 #re-opiniones[hidden] { display: none; } 1433 1434 /* ---------------------------------------------------------- reset */ 1435 1436 /* El embed vive dentro de la página, asà que le llegan los estilos de 1437 tag de la plantilla (ver la nota de arriba). Se neutralizan los 1438 tags que se usan aquÃ; lo que venga después, con clase, gana por 1439 especificidad. */ 1440 #re-opiniones, #re-opiniones *, #re-opiniones *::before, #re-opiniones *::after { 1441 box-sizing: border-box; 1442 } 1443 #re-opiniones div, 1444 #re-opiniones span, 1445 #re-opiniones time, 1446 #re-opiniones a, 1447 #re-opiniones h2, 1448 #re-opiniones button, 1449 #re-opiniones img, 1450 #re-opiniones svg { 1451 margin: 0; 1452 padding: 0; 1453 border: 0; 1454 background: none; 1455 box-shadow: none; 1456 font: inherit; 1457 text-align: left; 1458 text-transform: none; 1459 letter-spacing: normal; 1460 color: inherit; 1461 } 1462 #re-opiniones img, #re-opiniones svg { display: block; max-width: 100%; } 1463 #re-opiniones button { cursor: pointer; -webkit-appearance: none; appearance: none; } 1464 1465 /* ------------------------------------------------------- cabecera */ 1466 1467 #re-opiniones .re-op-cabecera { 1468 width: 100%; 1469 max-width: 1260px; 1470 margin: 40px auto 0; 1471 display: flex; 1472 flex-direction: column; 1473 align-items: center; 1474 } 1475 #re-opiniones .re-op-titulo { 1476 margin: 0; 1477 font-family: var(--_apps---typography--heading-font, Barlow), system-ui, -apple-system, 'Segoe UI', sans-serif; 1478 font-size: 34px; 1479 font-weight: 700; 1480 line-height: 120%; 1481 letter-spacing: 1px; 1482 color: var(--_apps---colors--black_re, #1C1F23); 1483 text-align: center; 1484 } 1485 1486 /* ------------------------------------------------------- carrusel */ 1487 1488 #re-opiniones .re-op-carrusel { 1489 position: relative; 1490 width: 100%; 1491 max-width: 1260px; 1492 margin: 36px auto 140px; 1493 } 1494 1495 /* La pista hace de .reviews-mask: la tarjeta ocupa el 30% del ancho 1496 y el resto asoma por la derecha, igual que el slider con la 1497 máscara al 30% y overflow visible. El paso (tarjeta + hueco) es 1498 exactamente ese 30%. */ 1499 #re-opiniones .re-op-pista { 1500 display: flex; 1501 align-items: stretch; 1502 gap: 24px; 1503 overflow-x: auto; 1504 overscroll-behavior-x: contain; 1505 scroll-snap-type: x mandatory; 1506 scroll-behavior: smooth; 1507 scrollbar-width: none; 1508 -ms-overflow-style: none; 1509 /* sitio para que la sombra del foco no se corte */ 1510 padding: 4px; 1511 margin: -4px; 1512 } 1513 #re-opiniones .re-op-pista::-webkit-scrollbar { display: none; } 1514 #re-opiniones .re-op-pista:focus-visible { 1515 outline: 2px solid var(--_apps---colors--black_re, #1C1F23); 1516 outline-offset: 2px; 1517 } 1518 1519 /* --------------------------------------------------------- tarjeta */ 1520 1521 #re-opiniones .re-op-tarjeta { 1522 flex: 0 0 calc(30% - 24px); 1523 scroll-snap-align: start; 1524 display: flex; 1525 flex-direction: column; 1526 margin: 0; 1527 padding: 24px; 1528 border-radius: 8px; 1529 background: var(--_apps---colors--green_re, #009F3C); 1530 color: var(--_apps---colors--bg-white_re, #FFFFFE); 1531 /* 16px es lo que hereda hoy la tarjeta del <body> del sitio; aquà va 1532 escrito porque dentro de un embed no se hereda nada a ciegas. */ 1533 font-size: 16px; 1534 font-weight: 400; 1535 font-style: normal; 1536 line-height: 135%; 1537 text-align: left; 1538 } 1539 1540 #re-opiniones .re-op-estrellas { display: block; width: 93px; height: 23px; } 1541 #re-opiniones .re-op-estrellas svg { display: block; width: 93px; height: 23px; } 1542 1543 /* Nombre y fecha en la misma lÃnea, la fecha pegada a la derecha: el 1544 mismo patrón (y el mismo margin-left:auto) que la cabecera de las 1545 opiniones de la ficha de entrenador. */ 1546 #re-opiniones .re-op-meta { 1547 display: flex; 1548 align-items: baseline; 1549 gap: 8px; 1550 margin: 4px 0; 1551 } 1552 #re-opiniones .re-op-autor { 1553 min-width: 0; 1554 margin: 0; 1555 font-size: 16px; 1556 line-height: 120%; 1557 font-weight: 600; 1558 font-style: normal; 1559 text-align: left; 1560 } 1561 #re-opiniones .re-op-fecha { 1562 margin-left: auto; 1563 font-size: 12px; 1564 font-weight: 500; 1565 font-style: normal; 1566 line-height: 120%; 1567 white-space: nowrap; 1568 } 1569 1570 #re-opiniones .re-op-cita { 1571 /* flex:1 empuja la pastilla del entrenador al fondo de la tarjeta e 1572 iguala el alto de todas; el line-clamp evita que una opinión de 1573 900 caracteres decida ese alto. Los dos display conviven: el 1574 elemento sigue siendo item flex del padre. */ 1575 flex: 1 1 auto; 1576 min-height: 0; 1577 display: -webkit-box; 1578 -webkit-box-orient: vertical; 1579 -webkit-line-clamp: 8; 1580 overflow: hidden; 1581 margin: 0; 1582 padding: 0; 1583 background: none; 1584 border-radius: 0; 1585 font-size: 16px; 1586 font-weight: 400; 1587 font-style: italic; 1588 line-height: 135%; 1589 text-align: left; 1590 } 1591 1592 /* ------------------------------------------------------------- pie */ 1593 1594 #re-opiniones .re-op-entrenador { 1595 display: flex; 1596 align-items: center; 1597 gap: 11px; 1598 margin-top: 28px; 1599 padding: 6px 8px; 1600 border-radius: 4px; 1601 background: var(--_apps---colors--bg-white_re, #FFFFFE); 1602 color: var(--_apps---colors--black_re, #1C1F23); 1603 font-size: 16px; 1604 font-weight: 500; 1605 font-style: normal; 1606 line-height: 135%; 1607 text-align: left; 1608 text-decoration: none; 1609 } 1610 #re-opiniones .re-op-entrenador:hover .re-op-ver { text-decoration: none; } 1611 #re-opiniones .re-op-entrenador:focus-visible { 1612 outline: 2px solid var(--_apps---colors--bg-white_re, #FFFFFE); 1613 outline-offset: 2px; 1614 } 1615 1616 #re-opiniones .re-op-foto { 1617 flex: 0 0 auto; 1618 width: 44px; 1619 height: 44px; 1620 border-radius: 50%; 1621 object-fit: cover; 1622 background: #F3F3F3; 1623 } 1624 /* §16 del design system: sin foto, iniciales â nunca un hueco. */ 1625 #re-opiniones .re-op-iniciales { 1626 flex: 0 0 auto; 1627 display: flex; 1628 align-items: center; 1629 justify-content: center; 1630 width: 44px; 1631 height: 44px; 1632 border-radius: 50%; 1633 background: #F3F3F3; 1634 font-size: 14px; 1635 font-weight: 600; 1636 line-height: 1; 1637 color: #1B1E22; 1638 } 1639 1640 #re-opiniones .re-op-datos { display: flex; flex-direction: column; min-width: 0; } 1641 #re-opiniones .re-op-nombre { font-size: 16px; font-weight: 500; line-height: 120%; } 1642 #re-opiniones .re-op-ver { 1643 margin-top: 2px; 1644 font-size: 12px; 1645 font-weight: 500; 1646 line-height: 120%; 1647 letter-spacing: 0.5px; 1648 text-transform: uppercase; 1649 text-decoration: underline; 1650 color: var(--_apps---colors--green_re, #009F3C); 1651 } 1652 1653 /* --------------------------------------------------------- puntos */ 1654 1655 #re-opiniones .re-op-puntos { 1656 position: absolute; 1657 left: 0; 1658 right: 0; 1659 bottom: -32px; 1660 display: flex; 1661 justify-content: center; 1662 gap: 8px; 1663 } 1664 #re-opiniones .re-op-puntos[hidden] { display: none; } 1665 #re-opiniones .re-op-punto { 1666 width: 10px; 1667 height: 10px; 1668 padding: 0; 1669 border: 0; 1670 border-radius: 50%; 1671 background: #d9dde2; 1672 cursor: pointer; 1673 transition: background .2s ease; 1674 } 1675 #re-opiniones .re-op-punto[aria-selected="true"] { 1676 background: var(--_apps---colors--green_re, #009F3C); 1677 } 1678 #re-opiniones .re-op-punto:focus-visible { 1679 outline: 2px solid var(--_apps---colors--black_re, #1C1F23); 1680 outline-offset: 2px; 1681 } 1682 1683 /* ------------------------------------------------------- esqueleto */ 1684 1685 #re-opiniones .re-op-skel { background: #F3F4F6; } 1686 #re-opiniones .re-op-skel .re-sk { 1687 background: rgba(27, 30, 34, .06); 1688 border-radius: 6px; 1689 animation: re-op-latido 1.2s ease-in-out infinite; 1690 } 1691 #re-opiniones .re-op-skel .re-sk-estrellas { width: 93px; height: 23px; } 1692 #re-opiniones .re-op-skel .re-sk-autor { width: 90px; height: 20px; margin: 4px 0; } 1693 #re-opiniones .re-op-skel .re-sk-cita { flex: 1 1 auto; min-height: 132px; } 1694 #re-opiniones .re-op-skel .re-sk-pie { height: 56px; margin-top: 28px; border-radius: 4px; } 1695 @keyframes re-op-latido { 0%, 100% { opacity: 1; } 50% { opacity: .55; } } 1696 1697 @media (prefers-reduced-motion: reduce) { 1698 #re-opiniones .re-op-pista { scroll-behavior: auto; } 1699 #re-opiniones .re-op-skel .re-sk { animation: none; } 1700 } 1701 1702 /* ------------------------------------------------------ responsive */ 1703 /* Los cortes son los de Webflow (991 / 767 / 479), que son los que 1704 usa .reviews-mask en el componente. */ 1705 1706 @media screen and (max-width: 991px) { 1707 #re-opiniones .re-op-tarjeta { flex-basis: calc(45% - 24px); } 1708 #re-opiniones .re-op-titulo { font-size: 28px; } 1709 } 1710 @media screen and (max-width: 767px) { 1711 #re-opiniones .re-op-tarjeta { flex-basis: calc(50% - 24px); } 1712 #re-opiniones .re-op-carrusel { margin-bottom: 100px; } 1713 } 1714 @media screen and (max-width: 479px) { 1715 #re-opiniones { padding: 0 4%; } 1716 #re-opiniones .re-op-titulo { font-size: 24px; } 1717 #re-opiniones .re-op-tarjeta { 1718 flex-basis: calc(90% - 12px); 1719 padding: 18px 16px; 1720 } 1721 #re-opiniones .re-op-pista { gap: 12px; } 1722 #re-opiniones .re-op-carrusel { margin-bottom: 80px; } 1723 } 1724</style> 1725
1726<script> 1727(function () { 1728 var SUPABASE_URL = "https://yrzanatzshdpvypazgeb.supabase.co"; 1729 var SUPABASE_ANON_KEY = "sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD"; 1730 var SUPABASE_UMD = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js"; 1731 var PERFIL_BASE = "/entrenador-personal/"; 1732 1733 var raiz = document.getElementById('re-opiniones'); 1734 if (!raiz) return; 1735 1736 var pista = document.getElementById('re-op-pista'); 1737 var puntos = document.getElementById('re-op-puntos'); 1738 var titulo = document.getElementById('re-op-titulo'); 1739 1740 function esc(s) { 1741 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 1742 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 1743 }); 1744 } 1745 1746 function ocultar() { raiz.setAttribute('hidden', ''); } 1747 1748 // ------------------------------------------------------- ajustes 1749 1750 // Igual que en el resto de bloques: el dato se pone en un data-* de 1751 // la instancia o de cualquier padre suyo, porque Webflow no deja 1752 // bindear una propiedad de componente a un atributo. 1753 function opcion(nombre) { 1754 var host = raiz.closest ? raiz.closest('[data-' + nombre + ']') : null; 1755 return host ? (host.getAttribute('data-' + nombre) || '').trim() : ''; 1756 } 1757 1758 function numero(nombre, porDefecto) { 1759 var v = parseInt(opcion(nombre), 10); 1760 return isNaN(v) ? porDefecto : v; 1761 } 1762 1763 var LIMITE = Math.max(1, Math.min(12, numero('limite', 5))); 1764 var POR_ENTRENADOR = Math.max(0, numero('por-entrenador', 1)); 1765 var MIN_ESTRELLAS = Math.max(1, Math.min(5, numero('min-estrellas', 1))); 1766 1767 var tituloPropio = opcion('titulo'); 1768 if (tituloPropio) titulo.textContent = tituloPropio; 1769 1770 // ----------------------------------------------------- estrellas 1771 1772 // Los cinco trazos del asset "5 stars.svg" (93Ã23). Se redibujan 1773 // aquà en lÃnea para poder apagar las que la opinión no tiene. 1774 var ESTRELLAS = [ 1775 'M10.3261 0L12.6444 7.13513L20.1467 7.13513L14.0772 11.5449L16.3956 18.68L10.3261 14.2703L4.25656 18.68L6.5749 11.5449L0.505393 7.13513L8.00772 7.13513L10.3261 0Z', 1776 'M27.9003 1.13965L30.2186 8.27478L37.721 8.27478L31.6514 12.6845L33.9698 19.8197L27.9003 15.4099L21.8308 19.8197L24.1491 12.6845L18.0796 8.27478L25.5819 8.27478L27.9003 1.13965Z', 1777 'M46.4999 1.13965L48.8182 8.27478L56.3206 8.27478L50.2511 12.6845L52.5694 19.8197L46.4999 15.4099L40.4304 19.8197L42.7487 12.6845L36.6792 8.27478L44.1815 8.27478L46.4999 1.13965Z', 1778 'M65.0995 1.13965L67.4178 8.27478L74.9202 8.27478L68.8507 12.6845L71.169 19.8197L65.0995 15.4099L59.03 19.8197L61.3483 12.6845L55.2788 8.27478L62.7812 8.27478L65.0995 1.13965Z', 1779 'M82.6737 2.2793L84.9921 9.41443L92.4944 9.41443L86.4249 13.8242L88.7432 20.9593L82.6737 16.5496L76.6042 20.9593L78.9226 13.8242L72.853 9.41443L80.3554 9.41443L82.6737 2.2793Z' 1780 ]; 1781 1782 function estrellasHTML(n) { 1783 var llenas = Math.max(0, Math.min(5, Math.round(Number(n) || 0))); 1784 var trazos = ''; 1785 for (var i = 0; i < 5; i++) { 1786 trazos += '<path d="' + ESTRELLAS[i] + '" fill="' + 1787 (i < llenas ? '#FEE649' : 'rgba(255,255,255,.32)') + '"/>'; 1788 } 1789 return '<div class="re-op-estrellas" role="img" aria-label="' + llenas + ' de 5 estrellas">' + 1790 '<svg viewBox="0 0 93 23" fill="none" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">' + 1791 trazos + '</svg></div>'; 1792 } 1793 1794 // ------------------------------------------------------- pintado 1795 1796 function iniciales(nombre) { 1797 var partes = String(nombre || '').trim().split(/\s+/).slice(0, 2); 1798 return partes.map(function (p) { return p.charAt(0).toUpperCase(); }).join('') || '·'; 1799 } 1800 1801 // "23 ago 2026", el formato de opiniones del resto del sitio 1802 // (widget-perfil-publico-1-resenas.html, widget-panel-opiniones.html). 1803 // Si la fecha no viene o no se puede leer, la tarjeta se queda sin ella 1804 // en vez de enseñar "Invalid Date". 1805 function fechaHTML(iso) { 1806 if (!iso) return ''; 1807 var d = new Date(iso); 1808 if (isNaN(d.getTime())) return ''; 1809 var texto = d.toLocaleDateString('es-ES', { day: 'numeric', month: 'short', year: 'numeric' }); 1810 return '<time class="re-op-fecha" datetime="' + esc(String(iso).slice(0, 10)) + '">' + 1811 esc(texto) + '</time>'; 1812 } 1813 1814 function fotoHTML(o) { 1815 var url = String(o.entrenador_foto || ''); 1816 if (!/^https?:\/\//i.test(url)) { 1817 return '<span class="re-op-iniciales" aria-hidden="true">' + esc(iniciales(o.entrenador_nombre)) + '</span>'; 1818 } 1819 return '<img class="re-op-foto" src="' + esc(url) + '" alt="" loading="lazy" decoding="async" width="44" height="44">'; 1820 } 1821 1822 function tarjetaHTML(o) { 1823 var perfil = PERFIL_BASE + encodeURIComponent(o.entrenador_slug); 1824 // Todo <div>: los tags con estilo propio en la plantilla 1825 // (blockquote, figure, figcaption) revientan dentro del embed. La
1826 // semántica la ponen role y aria-*, que el CSS no puede pisar. 1827 return '<div class="re-op-tarjeta" role="listitem">' + 1828 estrellasHTML(o.puntuacion) + 1829 '<div class="re-op-meta">' + 1830 '<div class="re-op-autor">' + esc(o.autor) + '</div>' + 1831 fechaHTML(o.creada_el) + 1832 '</div>' + 1833 '<div class="re-op-cita">' + esc(String(o.texto || '').trim()) + '</div>' + 1834 '<a class="re-op-entrenador" href="' + perfil + '">' + 1835 fotoHTML(o) + 1836 '<span class="re-op-datos">' + 1837 '<span class="re-op-nombre">' + esc(o.entrenador_nombre) + '</span>' + 1838 '<span class="re-op-ver">ver perfil</span>' + 1839 '</span>' + 1840 '</a>' + 1841 '</div>'; 1842 } 1843 1844 function esqueleto() { 1845 var una = '<div class="re-op-tarjeta re-op-skel" aria-hidden="true">' + 1846 '<div class="re-sk re-sk-estrellas"></div>' + 1847 '<div class="re-sk re-sk-autor"></div>' + 1848 '<div class="re-sk re-sk-cita"></div>' + 1849 '<div class="re-sk re-sk-pie"></div>' + 1850 '</div>'; 1851 pista.innerHTML = una + una + una + una; 1852 } 1853 1854 // -------------------------------------------------------- puntos 1855 1856 var tarjetas = []; 1857 1858 function pintarPuntos() { 1859 if (tarjetas.length < 2) { puntos.setAttribute('hidden', ''); return; } 1860 var html = ''; 1861 for (var i = 0; i < tarjetas.length; i++) { 1862 html += '<button type="button" class="re-op-punto" role="tab" data-i="' + i + '"' + 1863 ' aria-selected="' + (i === 0 ? 'true' : 'false') + '"' + 1864 ' aria-label="Opinión ' + (i + 1) + ' de ' + tarjetas.length + '"></button>'; 1865 } 1866 puntos.innerHTML = html; 1867 puntos.removeAttribute('hidden'); 1868 1869 puntos.addEventListener('click', function (ev) { 1870 var boton = ev.target.closest ? ev.target.closest('.re-op-punto') : null; 1871 if (!boton) return; 1872 var t = tarjetas[Number(boton.getAttribute('data-i'))]; 1873 if (t) pista.scrollTo({ left: t.offsetLeft - pista.offsetLeft, behavior: 'smooth' }); 1874 }); 1875 } 1876 1877 // La tarjeta activa es la que empieza más cerca del borde izquierdo 1878 // de la pista. Con la última pegada al final del scroll, se marca esa. 1879 function marcarPunto() { 1880 if (!tarjetas.length || puntos.hasAttribute('hidden')) return; 1881 var x = pista.scrollLeft; 1882 var fin = pista.scrollWidth - pista.clientWidth - 2; 1883 var activa = x >= fin ? tarjetas.length - 1 : 0; 1884 if (x < fin) { 1885 var mejor = Infinity; 1886 for (var i = 0; i < tarjetas.length; i++) { 1887 var d = Math.abs((tarjetas[i].offsetLeft - pista.offsetLeft) - x); 1888 if (d < mejor) { mejor = d; activa = i; } 1889 } 1890 } 1891 var botones = puntos.children; 1892 for (var j = 0; j < botones.length; j++) { 1893 botones[j].setAttribute('aria-selected', j === activa ? 'true' : 'false'); 1894 } 1895 } 1896 1897 function pintar(lista) { 1898 pista.innerHTML = lista.map(tarjetaHTML).join(''); 1899 tarjetas = Array.prototype.slice.call(pista.querySelectorAll('.re-op-tarjeta')); 1900 pintarPuntos(); 1901 1902 var pendiente = false; 1903 pista.addEventListener('scroll', function () { 1904 if (pendiente) return; 1905 pendiente = true; 1906 window.requestAnimationFrame(function () { pendiente = false; marcarPunto(); }); 1907 }, { passive: true }); 1908 } 1909 1910 // --------------------------------------------------------- datos 1911 1912 function conSupabase(hecho, fallo) { 1913 if (window.supabase && window.supabase.createClient) return hecho(); 1914 var s = document.querySelector('script[data-re-supabase]'); 1915 if (!s) { 1916 s = document.createElement('script'); 1917 s.src = SUPABASE_UMD; 1918 s.setAttribute('data-re-supabase', '1'); 1919 document.head.appendChild(s); 1920 } 1921 s.addEventListener('load', hecho); 1922 s.addEventListener('error', fallo); 1923 } 1924 1925 function cargar() { 1926 window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY) 1927 .rpc('ultimas_opiniones', { 1928 p_limite: LIMITE, 1929 p_por_entrenador: POR_ENTRENADOR, 1930 p_min_puntuacion: MIN_ESTRELLAS 1931 }) 1932 .then(function (res) { 1933 if (!res || res.error) return ocultar(); 1934 var lista = res.data || []; 1935 // Sin ninguna opinión no hay bloque: un carrusel vacÃo bajo un 1936 // titular es peor que no enseñar nada.
1937 if (!lista.length) return ocultar(); 1938 pintar(lista); 1939 }) 1940 .catch(ocultar); 1941 } 1942 1943 function arrancar() { 1944 raiz.removeAttribute('hidden'); 1945 esqueleto(); 1946 conSupabase(cargar, ocultar); 1947 } 1948 1949 if (document.readyState === 'loading') { 1950 document.addEventListener('DOMContentLoaded', arrancar); 1951 } else { 1952 arrancar(); 1953 } 1954})(); 1955</script>
1955 1956</div><section class="general-seccion gim"><div class="wrap_general special-section_home"><div>A tu medida y sin desplazamientos, lleva el gimnasio a tu terreno.</div><a href="https://www.redentrenadores.com/entrenamiento/a-domicilio" class="btn-green white_version w-button">ENTRENA Donde estés</a></div><h2 class="text-diffetence h2">A DOMICILIO</h2></section><a href="/donde-entrenar" class="banner-mapa-tarjeta w-inline-block"><div class="banner-mapa-tarjeta_mapa"><div class="w-embed"><style> 1957/* Pulso del pin del mapa. Vive aqui y no en una clase porque el editor de 1958 estilos de Webflow no crea @keyframes; duplicarlo en otro banner es 1959 inofensivo (mismo nombre, misma definicion). */ 1960@keyframes rePulse { 1961 0% { transform: scale(.6); opacity: .5 } 1962 70% { transform: scale(1.9); opacity: 0 } 1963 100% { transform: scale(1.9); opacity: 0 } 1964} 1965</style></div><svg fill="none" preserveAspectRatio="xMidYMid slice" viewBox="0 0 440 300" class="banner-mapa-tarjeta_trazado banner-mapa-tarjeta_trazado"><g stroke-width="1" stroke="#d9dde2"><path d="M0 40H440M0 100H440M0 160H440M0 220H440M0 280H440"></path><path d="M50 0V300M110 0V300M170 0V300M230 0V300M290 0V300M350 0V300M410 0V300"></path></g><path fill="none" stroke-width="10" stroke="#e8ebee" d="M-20 200 C 90 170, 160 250, 280 205 S 430 130, 500 165"></path><circle fill="#EAF3DE" r="58" cy="235" cx="330"></circle></svg><div class="banner-mapa-tarjeta_pin"><div class="banner-mapa-tarjeta_pulso"></div><div class="banner-mapa-tarjeta_punto"></div></div><div class="banner-mapa-tarjeta_marca-a"></div><div class="banner-mapa-tarjeta_marca-b"></div><div class="banner-mapa-tarjeta_marca-c"></div></div><div class="banner-mapa-tarjeta_texto"><div class="banner-mapa-tarjeta_eyebrow">Dónde entrenar</div><h2 class="banner-mapa-tarjeta_titulo">Encuentra el entrenador que trabaja más cerca de ti</h2><p class="banner-mapa-tarjeta_cuerpo">Abre el mapa, mira los gimnasios, parques y pistas donde se entrena de verdad y contacta con el profesional que ya está allÃ.</p><div class="banner-mapa-tarjeta_chips"><div class="banner-mapa-tarjeta_chip">Madrid</div><div class="banner-mapa-tarjeta_chip">Barcelona</div><div class="banner-mapa-tarjeta_chip">Valencia</div><div class="banner-mapa-tarjeta_chip">Y más ciudades</div><div class="banner-mapa-tarjeta_chip-verde">Al aire libre</div></div><div class="banner-mapa-tarjeta_cta">Ver el mapa<svg fill="none" viewBox="0 0 18 14" height="12" width="16" class="banner-mapa-tarjeta_flecha"><path stroke-linecap="round" stroke-width="2" stroke="#ffffff" d="M1 7h15m0 0-5.5-5.5M16 7l-5.5 5.5"></path></svg></div></div></a><section class="general-seccion presencial"><div class="wrap_general special-section_home"><div>Entrena cara a cara con la motivación y energÃa que solo se vive en persona </div><a href="/entrenamiento/presencial" class="btn-green white_version w-button">ENCUENTRA TU ENTRENADOR PRESENCIAL</a></div><h2 class="text-diffetence h2">Presencial</h2></section><section class="general-seccion online"><div class="wrap_general special-section_home"><div>Conéctate desde cualquier lugar y entrena sin excusas.</div><a href="https://www.redentrenadores.com/entrenamiento/online" class="btn-green white_version w-button">ENCUENTRA TU ENTRENADOR ONLINE</a></div><h2 class="text-diffetence h2">online</h2></section><section class="banner-recientes"><div class="banner-recientes_interior"><div class="banner-recientes_cabecera"><div class="banner-recientes_textos"><div class="banner-recientes_eyebrow">Recién publicados</div><h2 class="banner-recientes_titulo">Nuevos entrenadores en la red</h2><p class="banner-recientes_intro">Perfiles que acaban de incorporarse. TodavÃa con hueco en su agenda.</p></div><a href="/entrenador-personal" class="banner-recientes_cta w-inline-block">Ver todos los entrenadores<svg fill="none" viewBox="0 0 18 14" height="12" width="16" class="banner-recientes_flecha-cta"><path stroke-linecap="round" stroke-width="2" stroke="#ffffff" d="M1 7h15m0 0-5.5-5.5M16 7l-5.5 5.5"></path></svg></a></div><div class="w-embed w-script"><!-- ============================================================ 1966 RedEntrenadores â lista del banner "Recién publicados" 1967 1968 Los últimos entrenadores que han entrado en la red, en dos columnas 1969 (RPC entrenadores_recien_publicados, añadida en 1970 supabase/migrations/20260824083104_entrenadores_recien_publicados.sql). 1971
1972 Vive DENTRO del componente de Webflow "Banner Recién publicados" 1973 (grupo Banners). La cabecera âeyebrow, H2, párrafo y CTAâ NO está 1974 aquÃ: son elementos nativos con clases banner-recientes_*, para que el 1975 copy se edite en el Designer. Aquà solo va lo que sale de la base de 1976 datos, y su CSS, porque Webflow no publica el CSS de clases que no usa 1977 ningún elemento y estas filas las pinta el JS. 1978 1979 Qué es "recién publicado": el momento en que `webflow_item_id` dejó de 1980 ser null, que es lo que da ficha pública. Antes del 24/08/2026 eso no 1981 se guardaba, asà que para los anteriores la vista cae a `created_at`. 1982 Ojo con leer esas fechas como altas reales: 95 de los 121 entrenadores 1983 comparten la ventana de tres horas de la carga masiva del 21/08. 1984 1985 Ajustes opcionales, en un data-* del propio div o de cualquier padre 1986 (sirve envolver la instancia del componente en un div): 1987 data-ciudad="Valencia" solo entrenadores de esa ciudad 1988 data-limite="6" cuántas filas pedir (por defecto 8) 1989 1990 La segunda lÃnea de cada fila es la ciudad. Si el entrenador no tiene 1991 ciudad pero atiende online pone "Online" (es el caso del diseño), y si 1992 no hay ninguna de las dos cosas la lÃnea no se pinta: mejor un hueco 1993 que un guion suelto. 1994 1995 Si Supabase falla o no devuelve a nadie se esconde el banner ENTERO, 1996 no solo la lista: si no, la cabecera se queda con un "Ver todos los 1997 entrenadores" colgando de una sección vacÃa. 1998 ============================================================ --> 1999 2000<div id="re-recien" class="re-recien" hidden></div> 2001 2002<style> 2003 #re-recien { display: grid; grid-template-columns: minmax(0, 1fr); } 2004 2005 /* 720px es el corte del diseño, no uno de Webflow: esto es CSS suelto 2006 dentro de un embed, asà que aquà manda el diseño (mismo criterio que 2007 widget-entrenadores-online.html). */ 2008 @media screen and (min-width: 720px) { 2009 #re-recien { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 40px; } 2010 } 2011 2012 #re-recien .re-recien-fila { 2013 display: flex; 2014 align-items: center; 2015 gap: 14px; 2016 min-width: 0; 2017 padding: 14px 4px; 2018 border-bottom: 1px solid #d9dde2; 2019 text-decoration: none; 2020 color: inherit; 2021 } 2022 #re-recien a.re-recien-fila:hover .re-recien-nombre { color: #089F3B; } 2023 2024 #re-recien .re-recien-foto { 2025 flex-shrink: 0; 2026 width: 48px; 2027 height: 48px; 2028 border-radius: 8px; 2029 background: #F3F3F3; 2030 border: 1px solid #d9dde2; 2031 object-fit: cover; 2032 display: block; 2033 } 2034 #re-recien .re-recien-iniciales { 2035 display: flex; 2036 align-items: center; 2037 justify-content: center; 2038 font-family: 'Barlow', sans-serif; 2039 font-size: 15px; 2040 font-weight: 700; 2041 color: #9ca3af; 2042 } 2043 2044 /* El envoltorio existe solo para anclar el badge: la foto es un <img> y no 2045 puede tener hijos. */ 2046 #re-recien .re-recien-foto-wrap { 2047 position: relative; 2048 flex-shrink: 0; 2049 display: block; 2050 line-height: 0; 2051 } 2052 2053 /* Badge "Solo online" encima de la foto, arriba a la izquierda, como en el 2054 resto de tarjetas. El avatar son 48px y el literal no cabe, asà que aquà es 2055 solo el icono: el texto lo llevan title y aria-label. Blanco con borde y 2056 sombra, no verde: informa de la modalidad, no premia nada (§50). */ 2057 #re-recien .re-recien-online { 2058 position: absolute; 2059 top: 3px; 2060 left: 3px; 2061 z-index: 2; 2062 display: inline-flex; 2063 align-items: center; 2064 justify-content: center; 2065 width: 20px; 2066 height: 20px; 2067 box-sizing: border-box; 2068 background: #fff; 2069 border: 1px solid #d9dde2; 2070 border-radius: 999px; 2071 box-shadow: 0 2px 6px rgba(0,0,0,.18); 2072 color: #4B5563; 2073 } 2074 #re-recien .re-recien-online svg { width: 12px; height: 12px; display: block; } 2075 2076 #re-recien .re-recien-datos { flex: 1; display: flex; flex-direction: column; min-width: 0; } 2077 #re-recien .re-recien-nombre { 2078 font-family: 'Barlow', sans-serif; 2079 font-size: 18px; 2080 line-height: 24px; 2081 font-weight: 700; 2082 color: #1B1E22; 2083 overflow: hidden; 2084 text-overflow: ellipsis; 2085 white-space: nowrap; 2086 } 2087 /* #6b7280 y no #9ca3af: el gris claro no llega a AA como texto (§1.3). */ 2088 #re-recien .re-recien-lugar { font-size: 13px; color: #6b7280; } 2089 #re-recien .re-recien-flecha { flex-shrink: 0; } 2090 2091 /* ---------------------------------------------------------- esqueleto */ 2092 2093 #re-recien .re-recien-skel .re-recien-foto, 2094 #re-recien .re-recien-sk { 2095 background: #F3F3F3; 2096 animation: re-recien-latido 1.4s ease-in-out infinite; 2097 } 2098 #re-recien .re-recien-sk { border-radius: 6px; height: 12px; } 2099 #re-recien .re-recien-sk-nombre { width: 55%; height: 18px; margin-bottom: 6px; } 2100 #re-recien .re-recien-sk-lugar { width: 30%; } 2101 @keyframes re-recien-latido { 0%, 100% { opacity: 1; } 50% { opacity: .55; } } 2102 @media (prefers-reduced-motion: reduce) { 2103 #re-recien .re-recien-skel .re-recien-foto, 2104 #re-recien .re-recien-sk { animation: none; } 2105 } 2106</style> 2107
2108<script> 2109(function () { 2110 var SUPABASE_URL = "https://yrzanatzshdpvypazgeb.supabase.co"; 2111 var SUPABASE_ANON_KEY = "sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD"; 2112 var SUPABASE_UMD = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js"; 2113 var PERFIL_BASE = "/entrenador-personal/"; 2114 var POR_DEFECTO = 8; 2115 2116 var lista = document.getElementById('re-recien'); 2117 if (!lista) return; 2118 2119 // La cabecera es nativa de Webflow y envuelve a este embed: si no hay a 2120 // quién enseñar hay que esconder la sección entera, no solo la lista. 2121 var banner = lista.closest ? lista.closest('.banner-recientes') : null; 2122 2123 function esc(s) { 2124 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 2125 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 2126 }); 2127 } 2128 2129 function ocultar() { (banner || lista).setAttribute('hidden', ''); } 2130 2131 // closest() se incluye a sà mismo, asà que el data-* vale tanto en este 2132 // div como en cualquier padre que envuelva la instancia del componente. 2133 function ajuste(nombre) { 2134 var host = lista.closest ? lista.closest('[data-' + nombre + ']') : null; 2135 return host ? (host.getAttribute('data-' + nombre) || '').trim() : ''; 2136 } 2137 2138 var ciudad = ajuste('ciudad'); 2139 var limite = parseInt(ajuste('limite'), 10); 2140 if (!(limite > 0)) limite = POR_DEFECTO; 2141 2142 // ---------------------------------------------------------- esqueleto 2143 2144 function esqueleto() { 2145 var una = '<div class="re-recien-fila re-recien-skel" aria-hidden="true">' + 2146 '<span class="re-recien-foto-wrap"><span class="re-recien-foto"></span></span>' + 2147 '<div class="re-recien-datos">' + 2148 '<div class="re-recien-sk re-recien-sk-nombre"></div>' + 2149 '<div class="re-recien-sk re-recien-sk-lugar"></div>' + 2150 '</div>' + 2151 '</div>'; 2152 lista.innerHTML = new Array(limite + 1).join(una); 2153 } 2154 2155 // ------------------------------------------------------------ pintado 2156 2157 var FLECHA = '<svg class="re-recien-flecha" width="16" height="12" viewBox="0 0 18 14" fill="none" aria-hidden="true" focusable="false">' + 2158 '<path d="M1 7h15m0 0-5.5-5.5M16 7l-5.5 5.5" stroke="#089F3B" stroke-width="2" stroke-linecap="round"></path></svg>'; 2159 2160 function iniciales(nombre) { 2161 return (nombre || '?').trim().split(/\s+/).slice(0, 2).map(function (p) { 2162 return p.charAt(0); 2163 }).join('').toUpperCase(); 2164 } 2165 2166 var ICONO_PANTALLA = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' + 2167 '<rect x="2.6" y="4" width="18.8" height="12.8" rx="2"></rect><path d="M9 20.4h6"></path></svg>'; 2168 2169 // "Solo online" lo marca la vista `entrenadores_publico` en `badges` 2170 // (`online_total`); esta RPC lo devuelve desde la migración 20260830152518. 2171 function soloOnline(t) { 2172 return !!t.badges && t.badges.indexOf('online_total') !== -1; 2173 } 2174 2175 // Ciudad, o "Solo online" para quien no atiende presencial. El orden importa: 2176 // antes mandaba `ciudad_nombre` y quien solo entrena online, pero tiene ciudad 2177 // rellena (la mayorÃa: es de donde son, no donde entrenan), salÃa en esta tira 2178 // como si atendiera allÃ. Manda la modalidad, que es el dato que decide. 2179 function lugar(t) { 2180 if (soloOnline(t)) return 'Solo online'; 2181 if (t.ciudad_nombre) return t.ciudad_nombre; 2182 var modos = t.tipo_atencion || []; 2183 return modos.indexOf('Online') !== -1 ? 'Online' : ''; 2184 } 2185 2186 function badgeOnlineHTML(t) { 2187 if (!soloOnline(t)) return ''; 2188 return '<span class="re-recien-online" title="Solo entrena online: no atiende presencial"' + 2189 ' aria-label="Solo entrena online">' + ICONO_PANTALLA + '</span>'; 2190 } 2191 2192 function fotoHTML(t) { 2193 var dentro; 2194 if (!t.foto_url) { 2195 dentro = '<span class="re-recien-foto re-recien-iniciales" aria-hidden="true">' + 2196 esc(iniciales(t.nombre)) + '</span>'; 2197 } else { 2198 var donde = soloOnline(t) ? '' : lugar(t); 2199 dentro = '<img class="re-recien-foto" src="' + esc(t.foto_url) + '" alt="' + esc(t.nombre) + 2200 ', entrenador personal' + (donde && donde !== 'Online' ? ' en ' + esc(donde) : '') + 2201 '" loading="lazy" width="48" height="48" />'; 2202 } 2203 return '<span class="re-recien-foto-wrap">' + dentro + badgeOnlineHTML(t) + '</span>'; 2204 } 2205 2206 function filaHTML(t) { 2207 var donde = lugar(t); 2208 return '<a class="re-recien-fila" href="' + PERFIL_BASE + encodeURIComponent(t.slug) + '">' + 2209 fotoHTML(t) + 2210 '<span class="re-recien-datos">' + 2211 '<span class="re-recien-nombre">' + esc(t.nombre) + '</span>' + 2212 (donde ? '<span class="re-recien-lugar">' + esc(donde) + '</span>' : '') + 2213 '</span>' + 2214 FLECHA + 2215 '</a>'; 2216 } 2217 2218 // -------------------------------------------------------------- datos 2219 2220 function conSupabase(hecho, fallo) { 2221 if (window.supabase && window.supabase.createClient) return hecho(); 2222 var s = document.querySelector('script[data-re-supabase]'); 2223 if (!s) { 2224 s = document.createElement('script'); 2225 s.src = SUPABASE_UMD; 2226 s.setAttribute('data-re-supabase', '1'); 2227 document.head.appendChild(s); 2228 } 2229 s.addEventListener('load', hecho); 2230 s.addEventListener('error', fallo); 2231 } 2232
2233 function cargar() { 2234 var cliente = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); 2235 cliente.rpc('entrenadores_recien_publicados', { 2236 p_limite: limite, 2237 p_ciudad: ciudad || null 2238 }).then(function (res) { 2239 if (res && res.error) return ocultar(); 2240 var filas = (res && res.data) || []; 2241 if (!filas.length) return ocultar(); 2242 lista.innerHTML = filas.map(filaHTML).join(''); 2243 }).catch(ocultar); 2244 } 2245 2246 function arrancar() { 2247 lista.removeAttribute('hidden'); 2248 esqueleto(); 2249 conSupabase(cargar, ocultar); 2250 } 2251 2252 if (document.readyState === 'loading') { 2253 document.addEventListener('DOMContentLoaded', arrancar); 2254 } else { 2255 arrancar(); 2256 } 2257})(); 2258</script>
2258</div></div></section><section class="general-seccion"><div class="wrap_general"><h2 class="heading-h2 abs">Un entrenador para cada tipo de entrenamiento</h2><div data-current="Tab 1" data-easing="ease" data-duration-in="300" data-duration-out="100" class="tipo-entrenamiento w-tabs"><div id="w-node-c0a0f80b-01d3-1d85-f0e1-5676de70795e-7858fcdc" class="tab-menu_entrenamiento w-tab-menu"><a data-w-tab="Tab 1" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de70795f" class="tabs-ind w-inline-block w-tab-link w--current"><h3 class="title-tab">Mejora de forma fÃsica</h3><div style="height:0px;opacity:0" class="content-tab"><div>Ponerte en forma y ganar energÃa es posible con la ayuda de un entrenador personal. Con entrenamientos variados y adaptados a tu nivel, podrás aumentar tu resistencia, fuerza y salud cardiovascular, consiguiendo una mejor calidad de vida.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/mejora-forma-fisica'">Ver entrenadores para mejora de forma fÃsica</div></div></div></a><a data-w-tab="Tab 4" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de707966" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Aumento de masa muscular</h3><div style="height:0px;opacity:0" class="content-tab"><div>Ganar músculo requiere constancia, técnica y un plan bien diseñado. Un entrenador personal te guiará en tu entrenamiento de hipertrofia, combinando rutinas efectivas con seguimiento individualizado para ayudarte a aumentar masa muscular de manera segura y progresiva.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/aumento-masa-muscular'">Ver entrenadores para aumento de masa muscular</div></div></div></a><a data-w-tab="Tab 5" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de70796d" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Pérdida de peso</h3><div style="height:0px;opacity:0" class="content-tab"><div>Perder peso de forma saludable es más sencillo con el apoyo de un entrenador personal. A través de un plan de entrenamiento personalizado y progresivo, podrás quemar grasa, mejorar tu resistencia y tonificar tu cuerpo, consiguiendo resultados duraderos.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/perdida-peso'">Ver entrenadores para pérdida de peso</div></div></div></a><a data-w-tab="Tab 6" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de707974" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Entrenamiento funcional</h3><div style="height:0px;opacity:0" class="content-tab"><div>
2258El entrenamiento funcional está diseñado para mejorar los movimientos que realizas en tu dÃa a dÃa, aumentando fuerza, equilibrio y coordinación. Con un programa personalizado podrás trabajar de forma segura y eficaz, guiado por un entrenador personal que adaptará los ejercicios a tu nivel y objetivos.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/entrenamiento-funcional'">Ver entrenadores para entrenamiento funcional</div></div></div></a><a data-w-tab="Tab 7" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de70797b" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Corrección postural</h3><div style="height:0px;opacity:0" class="content-tab"><div>Un entrenador personal especializado en corrección postural te ayudará a mejorar tu alineación corporal, reducir dolores de espalda y prevenir futuras molestias. Con un entrenamiento personalizado podrás fortalecer tu core y adquirir hábitos que favorecen una postura correcta tanto en el trabajo como en la vida diaria.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/correccion-postural'">Ver entrenadores para corrección postural</div></div></div></a><a data-w-tab="Tab 8" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de707982" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Recuperación postparto</h3><div style="height:0px;opacity:0" class="content-tab"><div>Tras el embarazo, contar con un entrenador personal para la recuperación posparto es clave para volver a sentirte fuerte y activa. Con ejercicios adaptados a cada etapa, podrás recuperar tu forma fÃsica, fortalecer el suelo pélvico y mejorar tu bienestar con total seguridad.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/recuperacion-posparto'">Ver entrenadores para recuperación postparto</div></div></div></a><a data-w-tab="Tab 9" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de707989" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Movilidad y flexibilidad</h3><div style="height:0px;opacity:0" class="content-tab"><div>Mejorar la movilidad y la flexibilidad es fundamental para entrenar sin dolor y evitar lesiones. Un entrenador personal puede guiarte con ejercicios de estiramiento y movilidad articular que aumentan tu rango de movimiento y favorecen una vida más activa y cómoda.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/movilidad-flexibilidad'">Ver entrenadores para movilidad y flexibilidad</div></div></div></a><a data-w-tab="Tab 10" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de707990" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Preparación de oposiciones</h3><div style="height:0px;opacity:0" class="content-tab"><div>Superar las pruebas fÃsicas de unas oposiciones requiere un entrenamiento estructurado y eficaz. Con un entrenador personal especializado, podrás preparar resistencia, fuerza y velocidad con rutinas adaptadas a los requisitos de tu oposición, maximizando tus posibilidades de éxito.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/preparacion-oposiciones'">Ver entrenadores para preparaciones y oposiciones</div></div></div></a><a data-w-tab="Tab 11" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de707997" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Prevención de lesiones</h3><div style="height:0px;opacity:0" class="content-tab"><div>Prevenir lesiones es tan importante como recuperarse de ellas. Con un plan de entrenamiento personalizado, tu entrenador personal te enseñará ejercicios de fortalecimiento y movilidad que protegen músculos y articulaciones, evitando sobrecargas y mejorando tu rendimiento fÃsico.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/prevencion-lesiones'">Ver entrenadores para prevención de lesiones</div></div></div></a><a data-w-tab="Tab 12" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de70799e" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Recuperación de lesiones</h3><div style="height:0px;opacity:0" class="content-tab"><div>La recuperación de lesiones requiere de un entrenamiento progresivo y adaptado para evitar recaÃdas. Un entrenador personal especializado te guiará en cada paso, diseñando un plan que refuerce las zonas afectadas y te ayude a volver a entrenar con confianza.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/recuperacion-lesiones'">Ver entrenadores para recuperación de lesiones</div></div></div></a><a data-w-tab="Tab 13" data-w-id="c0a0f80b-01d3-1d85-f0e1-5676de7079a5" class="tabs-ind w-inline-block w-tab-link"><h3 class="title-tab">Preparación para deportistas</h3><div style="height:0px;opacity:0" class="content-tab"><div>Si buscas mejorar tu rendimiento en un deporte concreto, un entrenador personal diseñará un programa especÃfico para tus necesidades. Desde la fuerza hasta la resistencia, trabajarás las capacidades que más influyen en tu disciplina para alcanzar tu mejor versión.</div><div class="w-embed"><div style="width: 100%; height: 100%;" class="link-green_small" onclick="window.location.href='https://www.redentrenadores.com/objetivo/preparacion-deportistas'">Ver entrenadores para preparación de deportistas</div></div></div></a></div><div class="tab-content_entrenamiento w-tab-content"><div data-w-tab="Tab 1" class="w-tab-pane w--tab-active"><div class="img-tabs"></div></div><div data-w-tab="Tab 4" class="w-tab-pane"><div class="img-tabs _02"></div></div><div data-w-tab="Tab 5" class="w-tab-pane"><div class="img-tabs _03"></div></div><div data-w-tab="Tab 6" class="w-tab-pane"><div class="img-tabs _04"></div></div><div data-w-tab="Tab 7" class="w-tab-pane"><div class="img-tabs _05"></div></div><div data-w-tab="Tab 8" class="w-tab-pane"><div class="img-tabs _06"></div></div><div data-w-tab="Tab 9" class="w-tab-pane"><div class="img-tabs _07"></div></div><div data-w-tab="Tab 10" class="w-tab-pane"><div class="img-tabs _08"></div></div><div data-w-tab="Tab 11" class="w-tab-pane"><div class="img-tabs _09"></div></div><div data-w-tab="Tab 12" class="w-tab-pane"><div class="img-tabs _10"></div></div><div data-w-tab="Tab 13" class="w-tab-pane"><div class="img-tabs _11"></div></div></div></div></div></section><section class="general-seccion"><div class="wrap_general"><div class="w-layout-vflex flex-center"><h2 class="heading-h2">ArtÃculos recientes</h2></div><div class="w-dyn-list"><div role="list" class="blog-grid w-dyn-items"><div role="listitem" class="w-dyn-item"><a href="/blog/circuito-de-agilidad-de-policia-nacional" class="no-decor w-inline-block"><div class="bg-img_blog"><img alt="Circuito de agilidad de PolicÃa Nacional y Guardia Civil: cómo entrenarlo" src="https://cdn.prod.website-files.com/68c5a72ac71fa3f37858fc42/6abf4e682d7f5c5d9a0ac547_photo-1786025065736-c57c95cd4369.jpeg" class="img-blog_bg"/><div class="date-blog">2 oct 2026</div></div><div><h3 class="h3_blog">Circuito de agilidad de PolicÃa Nacional y Guardia Civil: cómo entrenarlo</h3><p class="text-blog limit-text">La primera prueba fÃsica elimina por nulo, aunque el tiempo sea bueno. Cómo se entrena el recorrido de banderolas y vallas.</p><div class="tag-leermas">Leer más</div></div></a></div><div role="listitem" class="w-dyn-item"><a href="/blog/recomposicion-corporal-en-mujeres-perder-grasa" class="no-decor w-inline-block"><div class="bg-img_blog">
2258<img alt="Recomposición corporal en mujeres: perder grasa y ganar músculo a la vez" src="https://cdn.prod.website-files.com/68c5a72ac71fa3f37858fc42/6ab0cb1aca5f528a582c947f_photo-1541534741688-6078c6bfb5c5.jpeg" class="img-blog_bg"/><div class="date-blog">30 sep 2026</div></div><div><h3 class="h3_blog">Recomposición corporal en mujeres: perder grasa y ganar músculo a la vez</h3><p class="text-blog limit-text">El peso no baja y la ropa sienta distinto. Es la señal de una recomposición corporal: pierdes grasa y ganas músculo a la vez.</p><div class="tag-leermas">Leer más</div></div></a></div><div role="listitem" class="w-dyn-item"><a href="/blog/cuerpo-ectomorfo-como-ganar-masa-muscular" class="no-decor w-inline-block"><div class="bg-img_blog"><img alt="Cuerpo ectomorfo: cómo ganar volumen muscular si eres delgado" src="https://cdn.prod.website-files.com/68c5a72ac71fa3f37858fc42/6aba05ad397af2ce09300567_photo-1534438327276-14e5300c3a48.jpeg" class="img-blog_bg"/><div class="date-blog">28 sep 2026</div></div><div><h3 class="h3_blog">Cuerpo ectomorfo: cómo ganar volumen muscular si eres delgado</h3><p class="text-blog limit-text">Con volumen suficiente, superávit sostenido y meses de constancia, un ectomorfo gana músculo igual que cualquiera.</p><div class="tag-leermas">Leer más</div></div></a></div></div></div><div class="w-layout-vflex flex-center"><a href="/blog" class="btn-green w-button">ver todos los artÃculos</a></div></div></section><section class="general-seccion"><div class="wrap_general"><div class="home_faqs"><div class="w-layout-vflex"><h2 class="heading-h2">Preguntas frecuentes</h2><div class="space_8"></div><div>Si tienes cualquier duda, puedes escribirnos a <a href="mailto:[email protected]" class="link-green home">[email protected]</a></div><div data-poster-url="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F69a07ccb1e9bd657dced8e6f_5319754-uhd_2160_3840_25fps_poster.0000000.jpg" data-video-urls="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F69a07ccb1e9bd657dced8e6f_5319754-uhd_2160_3840_25fps_mp4.mp4,https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F69a07ccb1e9bd657dced8e6f_5319754-uhd_2160_3840_25fps_webm.webm" data-autoplay="true" data-loop="true" data-wf-ignore="true" class="bg-video_faqs w-background-video w-background-video-atom"><video id="49c103d0-7f1a-f897-d5b8-bc59985aab98-video" autoplay="" loop="" style="background-image:url("https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F69a07ccb1e9bd657dced8e6f_5319754-uhd_2160_3840_25fps_poster.0000000.jpg")" muted="" playsinline="" data-wf-ignore="true" data-object-fit="cover"><source src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F69a07ccb1e9bd657dced8e6f_5319754-uhd_2160_3840_25fps_mp4.mp4" data-wf-ignore="true"/><source src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F69a07ccb1e9bd657dced8e6f_5319754-uhd_2160_3840_25fps_webm.webm" data-wf-ignore="true"/></video></div></div><div class="wrap-faqs"><div><div data-delay="0" data-hover="false" class="f-accordian-dropdown w-dropdown"><div class="f-accordian-toggle w-dropdown-toggle"><div class="f-accordian-title-wrapper"><div>¿Cómo encuentro un entrenador personal cerca de mÃ?</div><div class="f-accordian-icon w-embed"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 2259<path d="M11.9998 15L7.75684 10.757L9.17184 9.34302L11.9998 12.172L14.8278 9.34302L16.2428 10.757L11.9998 15Z" fill="currentColor"/> 2260</svg></div></div></div><nav class="f-accordian-list w-dropdown-list"><div class="f-accordian-content"><p class="f-paragraph-small">En RedEntrenadores puedes buscar entrenadores personales por ciudad o especialidad. Solo tienes que elegir tu ubicación y explorar perfiles de entrenadores verificados con información sobre su experiencia, servicios y formas de contacto.</p></div></nav></div><div data-delay="0" data-hover="false" class="f-accordian-dropdown w-dropdown"><div class="f-accordian-toggle w-dropdown-toggle"><div class="f-accordian-title-wrapper"><div>¿Cuánto cuesta contratar a un entrenador personal?<br/></div><div class="f-accordian-icon w-embed"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 2261<path d="M11.9998 15L7.75684 10.757L9.17184 9.34302L11.9998 12.172L14.8278 9.34302L16.2428 10.757L11.9998 15Z" fill="currentColor"/> 2262</svg></div></div></div><nav class="f-accordian-list w-dropdown-list"><div class="f-accordian-content"><p class="f-paragraph-small">El precio depende del entrenador, el tipo de entrenamiento y si las sesiones son presenciales u online. En cada perfil puedes ver la información del entrenador y contactar directamente para conocer disponibilidad y tarifas.</p></div></nav></div><div data-delay="0" data-hover="false" class="f-accordian-dropdown w-dropdown"><div class="f-accordian-toggle w-dropdown-toggle"><div class="f-accordian-title-wrapper"><div>¿Puedo entrenar online o solo presencial?</div><div class="f-accordian-icon w-embed"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 2263<path d="M11.9998 15L7.75684 10.757L9.17184 9.34302L11.9998 12.172L14.8278 9.34302L16.2428 10.757L11.9998 15Z" fill="currentColor"/> 2264</svg></div></div></div><nav class="f-accordian-list w-dropdown-list"><div class="f-accordian-content"><p class="f-paragraph-small">Muchos entrenadores ofrecen entrenamiento online, además de sesiones presenciales. En el perfil de ca
2264da entrenador verás si ofrece programas online, sesiones por videollamada o planes personalizados.</p></div></nav></div><div data-delay="0" data-hover="false" class="f-accordian-dropdown w-dropdown"><div class="f-accordian-toggle w-dropdown-toggle"><div class="f-accordian-title-wrapper"><div>¿Cómo contacto con un entrenador?</div><div class="f-accordian-icon w-embed"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 2265<path d="M11.9998 15L7.75684 10.757L9.17184 9.34302L11.9998 12.172L14.8278 9.34302L16.2428 10.757L11.9998 15Z" fill="currentColor"/> 2266</svg></div></div></div><nav class="f-accordian-list w-dropdown-list"><div class="f-accordian-content"><p class="f-paragraph-small">Desde el perfil del entrenador puedes enviar una solicitud de contacto directamente. El entrenador recibirá tu mensaje y podrá responderte para resolver dudas o coordinar tu primera sesión.<br/></p></div></nav></div><div data-delay="0" data-hover="false" class="f-accordian-dropdown w-dropdown"><div class="f-accordian-toggle w-dropdown-toggle"><div class="f-accordian-title-wrapper"><div>¿Los entrenadores están verificados?</div><div class="f-accordian-icon w-embed"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 2267<path d="M11.9998 15L7.75684 10.757L9.17184 9.34302L11.9998 12.172L14.8278 9.34302L16.2428 10.757L11.9998 15Z" fill="currentColor"/> 2268</svg></div></div></div><nav class="f-accordian-list w-dropdown-list"><div class="f-accordian-content"><p class="f-paragraph-small">En RedEntrenadores revisamos los perfiles antes de publicarlos para asegurar que la información sea real y útil para los usuarios. Además, cada perfil incluye especialidades, experiencia y descripción del servicio.</p></div></nav></div></div><div class="space_36"></div><a href="/entrenador-personal" class="btn-green w-button">ver todos los ENTRENADORES</a></div></div><div class="big-green_text">sin excusas para ser tu mejor versión</div><div class="w-layout-vflex flex-center"><a href="/entrenador-personal" class="btn-green w-button">ver todos los ENTRENADORES</a></div></div></section><footer class="footer"><div class="footer-middle footer-v1"><div class="footer-grid"><a id="w-node-_5855e537-ce68-c8ac-151a-51c4bee40e4e-1e51f0f5" href="#" class="w-inline-block"><img src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif" alt="Logotipo de RedEntrenadores" sizes="100vw" srcset="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-500.avif 500w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px-p-800.avif 800w, https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/68c5a7f45b0c2bd091e87d5c_RedEntrenadores_Cropped_400px.avif 2246w" class="footer-logo"/></a><div class="w-layout-vflex"><div>Encuentra entrenador personal para alcanzar tus objetivos en cualquier ciudad de España</div><div class="space_36"></div><div><strong>SÃguenos</strong></div><div class="social-grid"><a href="https://twitter.com/redentrenadores" target="_blank" class="social-link-icon w-inline-block"><div class="social-icon-font">î ©</div></a><a href="https://www.instagram.com/redentrenadores" target="_blank" class="social-link-icon w-inline-block"><div class="social-icon-font">î </div></a><a href="https://facebook.com/redentrenadores" target="_blank" class="social-link-icon w-inline-block"><div class="social-icon-font">î </div></a></div><div><strong>EscrÃbenos si necesitas ayuda.</strong></div><a href="mailto:[email protected]" class="footer-link">[email protected]</a></div><div class="w-layout-vflex"><h4 class="h4-footer">Páginas</h4><a href="/sobre-nosotros" class="footer-link">Nosotros</a><a href="/blog" class="footer-link">Blog</a><a href="/donde-entrenar" class="footer-link">Dónde entrenar</a><a href="/calculadoras/precio-entrenador-personal" class="footer-link">Cuánto cuesta un entrenador</a>
2268<a href="/calculadoras/donde-te-conviene-entrenar" class="footer-link">¿Dónde te conviene entrenar?</a><a href="/calculadoras/plan-primer-mes" class="footer-link">Tu primer mes de entrenamiento</a><a href="/calculadoras/calculadora-calorias" class="footer-link">Calculadora de calorÃas</a><a href="/calculadoras/calculadora-proteina" class="footer-link">Calculadora de proteÃna</a><a href="/calculadoras/calculadora-imc" class="footer-link">Calculadora de IMC</a><a href="/calculadoras/test-nivel-fitness" class="footer-link">Test de nivel de fitness</a><a href="/calculadoras/pruebas-fisicas-oposiciones" class="footer-link">Pruebas fÃsicas de oposición</a><a href="/herramientas" class="footer-link">Todas las herramientas</a><a href="https://www.redentrenadores.com/entrenamiento/online" class="footer-link">Entrenador personal online</a><a href="https://www.redentrenadores.com/entrenamiento/a-domicilio" class="footer-link">Entrenador personal a domicilio</a><a href="https://www.redentrenadores.com/entrenamiento/en-un-gimnasio" class="footer-link">Entrenador personal en gimnasio</a><a href="https://www.redentrenadores.com/entrenamiento/en-exteriores" class="footer-link">Entrenador personal al aire libre</a><a href="https://www.redentrenadores.com/entrenamiento/presencial" class="footer-link">Entrenador personal presencial</a><a href="/usuarios/mi-cuenta" class="footer-link">Ãrea de usuario</a><a href="/legal/politica-de-privacidad" class="footer-link">PolÃtica de privacidad</a><a href="/legal/terminos-y-condiciones" class="footer-link">Términos y condiciones</a><a href="/legal/aviso-legal" class="footer-link">Aviso legal</a></div><div class="w-layout-vflex"><h4 class="h4-footer">Entrenadores por ciudad</h4><a href="https://www.redentrenadores.com/entrenadores-personales/barcelona" class="footer-link">Entrenadores personales Barcelona</a><a href="https://www.redentrenadores.com/entrenadores-personales/madrid" class="footer-link">Entrenadores personales Madrid</a><a href="https://www.redentrenadores.com/entrenadores-personales/valencia" class="footer-link">Entrenadores personales Valencia</a><a href="https://www.redentrenadores.com/entrenadores-personales/malaga" class="footer-link">Entrenadores personales Málaga</a><a href="https://www.redentrenadores.com/entrenadores-personales/zaragoza" class="footer-link">Entrenadores personales Zaragoza</a><a href="https://www.redentrenadores.com/entrenadores-personales/sevilla" class="footer-link">Entrenadores personales Sevilla</a><a href="https://www.redentrenadores.com/entrenadores-personales/granada" class="footer-link">Entrenadores personales Granada</a><a href="https://www.redentrenadores.com/entrenadores-personales/alicante" class="footer-link">Entrenadores personales Alicante</a><a href="https://www.redentrenadores.com/entrenadores-personales/getafe" class="footer-link">Entrenadores personales Getafe</a><a href="https://www.redentrenadores.com/entrenadores-personales/alcala-de-henares" class="footer-link">Entrenadores personales Alcalá de Henares</a><a href="https://www.redentrenadores.com/entrenadores-personales/pozuelo-de-alarcon" class="footer-link">Entrenadores personales Pozuelo de Alarcón</a><a href="https://www.redentrenadores.com/entrenadores-personales/terrassa" class="footer-link">Entrenadores personales Terrassa</a><a href="https://www.redentrenadores.com/entrenadores-personales/badalona" class="footer-link">Entrenadores personales Badalona</a></div><div class="w-layout-vflex"><h4 class="h4-footer">Entrenadores por objetivo</h4><a href="https://www.redentrenadores.com/objetivo/movilidad-flexibilidad" class="footer-link">Movilidad y flexibilidad</a><a href="https://www.redentrenadores.com/objetivo/prevencion-lesiones" class="footer-link">Prevención de lesiones</a><a href="https://www.redentrenadores.com/objetivo/preparacion-oposiciones" class="footer-link">Preparación de oposiciones</a><a href="https://www.redentrenadores.com/objetivo/mejora-forma-fisica" class="footer-link">Mejora de forma fÃsica</a><a href="https://www.redentrenadores.com/objetivo/aumento-masa-muscular" class="footer-link">Aumento de masa muscular</a><a href="https://www.redentrenadores.com/objetivo/entrenamiento-funcional" class="footer-link">Entrenamiento funcional</a><a href="https://www.redentrenadores.com/objetivo/recuperacion-lesiones" class="footer-link">Recuperación de lesiones</a><a href="https://www.redentrenadores.com/objetivo/preparacion-deportistas" class="footer-link">Preparación para deportistas</a><a href="https://www.redentrenadores.com/objetivo/correccion-postural" class="footer-link">Corrección postural</a><a href="https://www.redentrenadores.com/objetivo/perdida-peso" class="footer-link">Pérdida de peso</a><a href="https://www.redentrenadores.com/objetivo/recuperacion-posparto" class="footer-link">Recuperación posparto</a></div></div></div><div class="footer-bottom-copy"><div id="w-node-_2744af48-cdd0-c61e-6264-4fdb1e51f15a-1e51f0f5" class="text-block-14">© 2026 RedEntrenadores. Todos los derechos reservados.</div></div><div class="footer-bottom"><a href="/legal/politica-de-privacidad" class="footer-link xs">PolÃtica de Privacidad</a><a href="/legal/aviso-legal" class="footer-link xs">Aviso Legal</a><a href="/legal/terminos-y-condiciones" class="footer-link xs">Términos y condiciones</a></div><div class="w-embed"><style> 2269 /* Namespace propio `re-rec-*` en TODO. El sitio ya tiene `.re-modal`, 2270 `.re-cta` y `.re-field` en las landings y en el listado, y este bloque
2271 carga en las mismas páginas: reutilizar esos nombres es garantizarse un 2272 choque de estilos el dÃa que alguien toque cualquiera de los dos. */ 2273 #re-rec-overlay { 2274 position: fixed; 2275 inset: 0; 2276 /* En móvil `inset: 0` mide el viewport GRANDE â el de la barra del 2277 navegador escondida â, asà que el borde de abajo del overlay cae por 2278 debajo de lo que se ve. Como en móvil el modal se ancla justo ahà 2279 (bottom sheet), el último campo y el botón de enviar quedaban tapados 2280 por la barra del navegador, sin forma de llegar a ellos: el modal ya 2281 está en su tope de scroll interno y lo que sobra queda fuera de 2282 pantalla. `dvh` mide el viewport visible de verdad; el `vh` de arriba 2283 es el respaldo para navegadores sin `dvh`, que se quedan como estaban. 2284 El teclado no lo arregla ninguna unidad CSS â de eso se encarga 2285 `ajustarAlViewport()` en el bloque 3. */ 2286 height: 100vh; 2287 height: 100dvh; 2288 z-index: 10000; 2289 display: none; 2290 align-items: center; 2291 justify-content: center; 2292 padding: 20px; 2293 background: rgba(27, 30, 34, 0.55); 2294 font-family: 'Plus Jakarta Sans', sans-serif; 2295 box-sizing: border-box; 2296 } 2297 #re-rec-overlay.re-rec-abierto { display: flex; } 2298 2299 #re-rec-overlay .re-rec-modal { 2300 position: relative; 2301 width: 100%; 2302 max-width: 520px; 2303 max-height: 90vh; 2304 overflow-y: auto; 2305 /* Que el scroll del modal no arrastre la página de detrás al llegar al 2306 final: en iOS el encadenado hace que el sheet parezca que "se va". */ 2307 overscroll-behavior: contain; 2308 background: #fff; 2309 border-radius: 12px; 2310 box-shadow: 0 4px 14px rgba(0, 0, 0, 0.09); 2311 padding: 28px; 2312 box-sizing: border-box; 2313 /* El paso de confirmación es bastante más ancho que las preguntas (tres 2314 fichas en fila); se ensancha con una clase en vez de dejar el modal 2315 ancho todo el rato, que harÃa que las 4 preguntas naden en el hueco. */ 2316 } 2317 #re-rec-overlay .re-rec-modal.re-rec-ancho { max-width: 640px; } 2318 2319 #re-rec-overlay .re-rec-cerrar { 2320 position: absolute; 2321 top: 10px; 2322 right: 10px; 2323 width: 44px; 2324 height: 44px; 2325 border: 0; 2326 background: none; 2327 color: #6b7280; 2328 font-size: 20px; 2329 line-height: 1; 2330 cursor: pointer; 2331 } 2332 #re-rec-overlay .re-rec-cerrar:hover { color: #1B1E22; } 2333 2334 #re-rec-overlay .re-rec-dots { display: flex; gap: 6px; margin-bottom: 18px; } 2335 #re-rec-overlay .re-rec-dots span { 2336 width: 26px; 2337 height: 4px; 2338 border-radius: 999px; 2339 background: #d9dde2; 2340 } 2341 #re-rec-overlay .re-rec-dots span.re-rec-hecho { background: #089F3B; } 2342 2343 #re-rec-overlay .re-rec-eyebrow { 2344 font-size: 12px; 2345 font-weight: 700; 2346 letter-spacing: 0.48px; 2347 text-transform: uppercase; 2348 color: #089F3B; 2349 margin: 0 0 6px; 2350 } 2351 #re-rec-overlay h3 { 2352 font-family: 'Barlow', sans-serif; 2353 font-size: 24px; 2354 line-height: 1.25; 2355 font-weight: 700; 2356 color: #1B1E22; 2357 margin: 0 0 6px; 2358 padding-right: 34px; 2359 } 2360 #re-rec-overlay .re-rec-sub { 2361 font-size: 16px; 2362 line-height: 1.6; 2363 color: #4B5563; 2364 margin: 0 0 18px; 2365 } 2366 2367 #re-rec-overlay .re-rec-pregunta { 2368 font-size: 14px; 2369 font-weight: 600; 2370 color: #1B1E22; 2371 margin: 0 0 8px; 2372 } 2373 2374 /* Opciones: botón grande de fila completa. Objetivo táctil holgado (48px) 2375 por el punto 13 del Apéndice A del design system â el código nuevo no 2376 deberÃa nacer ya con deuda de accesibilidad. */ 2377 #re-rec-overlay .re-rec-opciones { display: grid; gap: 8px; } 2378 #re-rec-overlay .re-rec-op { 2379 display: flex; 2380 align-items: center; 2381 gap: 12px; 2382 width: 100%; 2383 min-height: 48px; 2384 padding: 10px 14px; 2385 text-align: left; 2386 background: #fff; 2387 border: 1px solid #d9dde2; 2388 border-radius: 8px; 2389 font-family: inherit; 2390 font-size: 16px; 2391 font-weight: 500; 2392 color: #1B1E22; 2393 cursor: pointer; 2394 box-sizing: border-box; 2395 } 2396 #re-rec-overlay .re-rec-op:hover { border-color: #089F3B; background: #EAF3DE; } 2397 #re-rec-overlay .re-rec-op.re-rec-op-on { border-color: #089F3B; background: #EAF3DE; color: #067A2D; font-weight: 600; } 2398 #re-rec-overlay .re-rec-op-txt { flex: 1; min-width: 0; } 2399 #re-rec-overlay .re-rec-op-nota { display: block; font-size: 13px; font-weight: 400; color: #6b7280; margin-top: 2px; } 2400 2401 /* El icono va en su propia pastilla verde clara y no suelto: suelto se 2402 pierde contra el blanco de la fila, y es lo que hace que la lista se lea 2403 de un vistazo en vez de leerse renglón a renglón. Al pasar por encima el 2404 fondo de la fila se vuelve `#EAF3DE`, el mismo de la pastilla, asà que 2405 ahà la pastilla invierte a verde macizo para no desaparecer. */ 2406 #re-rec-overlay .re-rec-op-ico { 2407 display: flex; 2408 align-items: center; 2409 justify-content: center; 2410 width: 38px; 2411 height: 38px; 2412 flex-shrink: 0; 2413 border-radius: 10px; 2414 background: #EAF3DE; 2415 color: #089F3B; 2416 } 2417 #re-rec-overlay .re-rec-op-ico svg { width: 21px; height: 21px; display: block; } 2418 #re-rec-overlay .re-rec-op:hover .re-rec-op-ico, 2419 #re-rec-overlay .re-rec-op.re-rec-op-on .re-rec-op-ico { background: #089F3B; color: #fff; } 2420 2421 #re-rec-overlay .re-rec-op-flecha { 2422 width: 17px; 2423 height: 17px; 2424 flex-shrink: 0; 2425 color: #C9CED4; 2426 } 2427 #re-rec-overlay .re-rec-op:hover .re-rec-op-flecha, 2428 #re-rec-overlay .re-rec-op.re-rec-op-on .re-rec-op-flecha { color: #089F3B; } 2429 2430 /* "Ver más opciones" no es una opción más: va sin caja para que no compita 2431 con las dos que sà son elegibles. */ 2432 #re-rec-overlay .re-rec-vermas { 2433 justify-self: start; 2434 min-height: 44px; 2435 padding: 4px 0; 2436 background: none; 2437 border: 0; 2438 cursor: pointer; 2439 font-family: inherit; 2440 font-size: 14px; 2441 font-weight: 600; 2442 color: #089F3B; 2443 text-decoration: underline; 2444 } 2445 #re-rec-overlay .re-rec-vermas:hover { color: #067A2D; } 2446 2447 #re-rec-overlay .re-rec-campo { margin-bottom: 14px; } 2448 /* La ciudad aparece debajo de las opciones de modalidad, no en su propia 2449 pantalla: necesita aire por arriba para no leerse como una quinta opción. */ 2450 #re-rec-overlay .re-rec-campo-ciudad { margin-top: 18px; margin-bottom: 0; } 2451 #re-rec-overlay .re-rec-campo label { 2452 display: block; 2453 font-size: 14px; 2454 font-weight: 600; 2455 color: #1B1E22; 2456 margin-bottom: 6px; 2457 } 2458 #re-rec-overlay .re-rec-campo input { 2459 width: 100%; 2460 min-height: 48px; 2461 padding: 12px 14px; 2462 border: 1px solid #d9dde2; 2463 border-radius: 8px; 2464 font-family: inherit; 2465 font-size: 16px; 2466 color: #1B1E22; 2467 background: #fff; 2468 box-sizing: border-box; 2469 } 2470 /* 16px en los inputs y no 14: por debajo de 16 Safari en iPhone hace zoom 2471 al enfocar el campo y descoloca el modal entero. */ 2472 #re-rec-overlay .re-rec-campo input:focus { 2473 outline: none; 2474 border-color: #089F3B; 2475 box-shadow: 0 0 0 3px rgba(8, 159, 59, 0.15); 2476 } 2477 #re-rec-overlay .re-rec-opcional { font-weight: 400; color: #6b7280; } 2478 2479 #re-rec-overlay .re-rec-btn { 2480 display: inline-block; 2481 border: 0; 2482 border-radius: 8px; 2483 background: #089F3B; 2484 color: #fff; 2485 font-family: inherit; 2486 font-size: 16px; 2487 font-weight: 600; 2488 padding: 13px 22px; 2489 min-height: 48px; 2490 cursor: pointer; 2491 } 2492 #re-rec-overlay .re-rec-btn:hover { background: #067A2D; } 2493 #re-rec-overlay .re-rec-btn:disabled { opacity: 0.5;
2493 cursor: not-allowed; } 2494 2495 #re-rec-overlay .re-rec-nav { display: flex; align-items: center; gap: 14px; margin-top: 12px; } 2496 #re-rec-overlay .re-rec-atras { 2497 background: none; 2498 border: 0; 2499 padding: 12px 0; 2500 cursor: pointer; 2501 font-family: inherit; 2502 font-size: 14px; 2503 font-weight: 600; 2504 color: #4B5563; 2505 text-decoration: underline; 2506 } 2507 #re-rec-overlay .re-rec-atras:hover { color: #067A2D; } 2508 2509 #re-rec-overlay .re-rec-error { 2510 display: none; 2511 margin: 12px 0 0; 2512 font-size: 14px; 2513 color: #791F1F; 2514 background: #FCEBEB; 2515 border-radius: 8px; 2516 padding: 10px 12px; 2517 } 2518 #re-rec-overlay .re-rec-legal { 2519 margin: 12px 0 0; 2520 font-size: 12px; 2521 line-height: 1.45; 2522 color: #6b7280; 2523 } 2524 #re-rec-overlay .re-rec-legal a { color: #6b7280; text-decoration: underline; } 2525 2526 /* El pie: lo que antes era el subtÃtulo de arriba. Va separado por un filete 2527 y en cuerpo pequeño â es contexto para quien duda, no la instrucción de la 2528 pantalla, que ahora es el tÃtulo más las propias opciones. */ 2529 #re-rec-overlay .re-rec-pie { 2530 display: flex; 2531 align-items: flex-start; 2532 gap: 9px; 2533 margin: 18px 0 0; 2534 padding-top: 14px; 2535 border-top: 1px solid #E5E6E8; 2536 font-size: 13px; 2537 line-height: 1.5; 2538 color: #6b7280; 2539 } 2540 #re-rec-overlay .re-rec-pie-ico { color: #089F3B; flex-shrink: 0; line-height: 0; } 2541 #re-rec-overlay .re-rec-pie-ico svg { width: 17px; height: 17px; display: block; } 2542 2543 /* ---------------------------------------------------------- confirmación */ 2544 2545 #re-rec-overlay .re-rec-fichas { 2546 display: grid; 2547 grid-template-columns: repeat(3, minmax(0, 1fr)); 2548 gap: 12px; 2549 margin: 18px 0 0; 2550 } 2551 /* Sin `:hover` ni cursor de mano: la ficha no es clicable (ver fichaHTML), y 2552 un borde que reacciona al pasar por encima promete un clic que no existe. */ 2553 #re-rec-overlay .re-rec-ficha { 2554 display: block; 2555 text-align: center; 2556 border: 1px solid #d9dde2; 2557 border-radius: 12px; 2558 padding: 14px 10px; 2559 color: inherit; 2560 } 2561 /* El envoltorio existe solo para anclar el badge: la foto es un <img> y no 2562 puede tener hijos. Se lleva el margen que antes tenÃa la foto. */ 2563 #re-rec-overlay .re-rec-foto-wrap { 2564 position: relative; 2565 display: block; 2566 width: 96px; 2567 margin: 0 auto 10px; 2568 } 2569 /* La foto crece dentro de la misma tarjeta: es lo único de la ficha que se 2570 reconoce de un vistazo, y a 64px competÃa con el texto en vez de mandar.
2571 Cuadrada y no circular: a igual ancho, el cÃrculo recorta los laterales y 2572 se come parte de la cara. Las esquinas van a 8px, el mismo radio que los 2573 botones â no a 12px, que es el de la tarjeta que la contiene. */ 2574 #re-rec-overlay .re-rec-foto { 2575 width: 100%; 2576 height: 96px; 2577 border-radius: 8px; 2578 object-fit: cover; 2579 display: block; 2580 background: #F3F3F3; 2581 } 2582 2583 /* Badge "Solo online" encima de la foto, arriba a la izquierda, como en el 2584 resto de tarjetas del sitio. Solo el icono: a 96px ây a 64px en móvilâ el 2585 literal no cabe, y ya sale escrito como primer dato de la ficha, justo 2586 debajo. El texto lo llevan title y aria-label. Blanco con borde y sombra, 2587 no verde: informa de la modalidad, no premia nada (§50). */ 2588 #re-rec-overlay .re-rec-online { 2589 position: absolute; 2590 top: 6px; 2591 left: 6px; 2592 z-index: 2; 2593 display: inline-flex; 2594 align-items: center; 2595 justify-content: center; 2596 width: 22px; 2597 height: 22px; 2598 box-sizing: border-box; 2599 background: #fff; 2600 border: 1px solid #d9dde2; 2601 border-radius: 999px; 2602 box-shadow: 0 2px 6px rgba(0,0,0,.18); 2603 color: #4B5563; 2604 } 2605 #re-rec-overlay .re-rec-online svg { width: 13px; height: 13px; display: block; } 2606 #re-rec-overlay .re-rec-ficha-nombre { 2607 font-family: 'Barlow', sans-serif; 2608 font-size: 15px; 2609 font-weight: 700; 2610 color: #1B1E22; 2611 display: block; 2612 line-height: 1.25; 2613 } 2614 /* PÃldora pública: radio 999px, nunca los 12px de la pÃldora de panel. */ 2615 #re-rec-overlay .re-rec-pill { 2616 display: inline-block; 2617 margin-top: 6px; 2618 padding: 4px 10px; 2619 border-radius: 999px; 2620 background: #F3F4F6; 2621 color: #4B5563; 2622 font-size: 11px; 2623 font-weight: 600; 2624 line-height: 1.3; 2625 } 2626 #re-rec-overlay .re-rec-pill-match { background: #EAF3DE; color: #089F3B; } 2627 #re-rec-overlay .re-rec-ficha-meta { 2628 font-size: 12px; 2629 line-height: 1.4; 2630 color: #4B5563; 2631 display: block; 2632 margin-top: 5px; 2633 } 2634 #re-rec-overlay .re-rec-estrella { vertical-align: -1px; } 2635 2636 #re-rec-overlay .re-rec-p2 { margin-top: 22px; padding-top: 18px; border-top: 1px solid #d9dde2; } 2637 #re-rec-overlay .re-rec-p2 h4 { 2638 font-family: 'Barlow', sans-serif; 2639 font-size: 18px; 2640 font-weight: 700; 2641 color: #1B1E22; 2642 margin: 0 0 4px; 2643 } 2644 /* minmax(0, 1fr) y no 1fr: el mÃnimo "auto" de un <select> es el ancho de su 2645 opción más larga, asà que con 1fr las columnas no encogen y desbordan. */ 2646 #re-rec-overlay .re-rec-p2-grid { 2647 display: grid; 2648 grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); 2649 gap: 0 14px; 2650 } 2651 #re-rec-overlay .re-rec-p2-grid > div { min-width: 0; } 2652 #re-rec-overlay .re-rec-p2-full { grid-column: 1 / -1; } 2653 #re-rec-overlay .re-rec-campo select, 2654 #re-rec-overlay .re-rec-campo textarea { 2655 width: 100%; 2656 min-height: 48px; 2657 padding: 12px 14px; 2658 border: 1px solid #d9dde2; 2659 border-radius: 8px; 2660 font-family: inherit; 2661 font-size: 16px; 2662 color: #1B1E22; 2663 background: #fff; 2664 box-sizing: border-box; 2665 } 2666 #re-rec-overlay .re-rec-campo textarea { min-height: 70px; resize: vertical; } 2667 #re-rec-overlay .re-rec-acciones { display: flex; align-items: center; gap: 18px; margin-top: 8px; } 2668 #re-rec-overlay .re-rec-saltar { 2669 background: none; 2670 border: 0; 2671 padding: 12px 0; 2672 cursor: pointer; 2673 font-family: inherit; 2674 font-size: 14px; 2675 font-weight: 600; 2676 color: #4B5563; 2677 text-decoration: underline; 2678 } 2679 2680 @media (max-width: 560px) { 2681 #re-rec-overlay { 2682 padding: 12px; 2683 /* La barra de gestos del iPhone se come los últimos ~34px: sin esto el 2684 pie del modal queda por debajo de ella. */ 2685 padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); 2686 align-items: flex-end; 2687 } 2688 /* `100%` es el alto del overlay ya descontado su padding, o sea lo que de 2689 verdad se ve. Antes era `92vh`, que se mide contra el viewport grande y 2690 dejaba el pie del modal fuera de pantalla. */ 2691 #re-rec-overlay .re-rec-modal { padding: 22px 18px; max-height: 100%; } 2692 2693 /* Con el teclado abierto el modal se queda en ~350px de alto y el 2694 contenido no cabe de ninguna manera: al escribir la ciudad, el botón 2695 quedaba por debajo del borde del modal. Se veÃa cortado y no habÃa nada 2696 que indicara que el modal tiene scroll propio, asà que el paso parecÃa 2697 roto. Pegado abajo, el botón está siempre a la vista y lo que scrolla es 2698 lo de arriba. Solo en móvil: en escritorio no hay teclado que robe media 2699 pantalla y el modal casi nunca desborda. 2700 2701 El fondo blanco es obligatorio âel contenido pasa por detrásâ y el 2702 padding de abajo replica el del modal, porque `bottom: 0` ancla contra 2703 el borde del scrollport y no contra su padding; el margen negativo lo 2704 descuenta para que en reposo, sin pegar, nada se mueva de sitio. */ 2705 #re-rec-overlay .re-rec-nav { 2706 position: sticky; 2707 bottom: 0; 2708 background: #fff; 2709 padding-bottom: 22px; 2710 margin-bottom: -22px; 2711 /* El padding de arriba se descuenta del margen para que en reposo la 2712 separación siga siendo la misma. La sombra es lo que hace que, cuando 2713 hay contenido pasando por detrás, la barra se lea como barra y no como 2714 un campo cortado a medias; a esta opacidad no se nota cuando la barra 2715 está en su sitio y no tapa nada. */ 2716 padding-top: 10px; 2717 margin-top: 2px; 2718 box-shadow: 0 -6px 10px -8px rgba(27, 30, 34, 0.16); 2719 } 2720 #re-rec-overlay h3 { font-size: 21px; } 2721 #re-rec-overlay .re-rec-fichas { grid-template-columns: minmax(0, 1fr); } 2722 #re-rec-overlay .re-rec-ficha { display: flex; align-items: center; gap: 14px; text-align: left; } 2723 #re-rec-overlay .re-rec-foto-wrap { width: 64px; margin: 0; flex-shrink: 0; } 2724 #re-rec-overlay .re-rec-foto { height: 64px; } 2725 #re-rec-overlay .re-rec-online { top: 4px; left: 4px;
2725 width: 20px; height: 20px; } 2726 #re-rec-overlay .re-rec-ficha-datos { min-width: 0; } 2727 #re-rec-overlay .re-rec-p2-grid { grid-template-columns: minmax(0, 1fr); } 2728 } 2729</style></div><div class="w-embed w-script">
2729<script> 2730(function () { 2731 "use strict"; 2732 2733 window.RE_RECOMENDADOR_VERSION = "1.0.0"; 2734 2735 var SUPABASE_URL = "https://yrzanatzshdpvypazgeb.supabase.co"; 2736 var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlyemFuYXR6c2hkcHZ5cGF6Z2ViIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ0NjczNzcsImV4cCI6MjEwMDA0MzM3N30.WNRs7Bc8lA1JUw-KbOHYDBlEjTIPjwxx3cvlBmdpgHU"; 2737 var PRIVACIDAD_URL = "https://www.redentrenadores.com/legal/politica-de-privacidad"; 2738 2739 // Rutas donde el pop-up no debe salir nunca. El panel y el área de usuario 2740 // son zonas privadas de alguien que ya está dentro; /lp/ son las landings de 2741 // paid, que ya llevan este mismo flujo dentro de su propio widget y verÃan 2742 // dos modales peleándose; /crear-cuenta y /crea-tu-perfil son el alta y el 2743 // onboarding del entrenador, a quien ofrecerle buscar entrenador no le sirve 2744 // de nada y encima le tapa el formulario a medio rellenar; en 2745 // /pregunta-al-experto es donde contesta preguntas. /donde-entrenar queda 2746 // fuera por decisión de Esteve (20/08/2026). 2747 var RUTAS_EXCLUIDAS = [ 2748 "/panel-entrenador", "/usuarios/", "/mi-cuenta", "/admin", "/lp/", 2749 "/crear-cuenta", "/crea-tu-perfil", "/pregunta-al-experto", "/donde-entrenar" 2750 ]; 2751 2752 var SEGUNDOS_ESPERA = 25; 2753 var SCROLL_DISPARO = 0.5; 2754 2755 var CLAVE_APAGADO = "re_recomendador_off"; // localStorage: no volver a salir 2756 var CLAVE_SESION = "re_recomendador_visto"; // sessionStorage: ya salió hoy 2757 2758 // Los `value` son las etiquetas exactas de `entrenadores.tipo_atencion`, no 2759 // slugs, y tienen que seguir siéndolo: es lo que ya hay guardado en 2760 // `solicitudes_contacto.modalidad` y lo que el panel del entrenador pinta 2761 // sin traducir. El label es lo único redactable para el usuario. 2762 // 2763 // El orden importa: se enseñan solo las dos primeras y el resto se destapa 2764 // con "Ver más", igual que hace el wizard de /encuentra-entrenador. Online y
2765 // gimnasio son las dos que cubren a casi todo el mundo, y una pantalla con 2766 // cuatro opciones grandes pesa más de lo que aporta como primera impresión. 2767 var MODALIDADES = [ 2768 { value: "Online", label: "Online, por videollamada", nota: "Desde donde quieras" }, 2769 { value: "En un gimnasio", label: "En un gimnasio", nota: "En su centro o en el tuyo" }, 2770 { value: "A domicilio", label: "En mi casa", nota: "El entrenador se desplaza hasta ti" }, 2771 { value: "En exteriores", label: "Al aire libre", nota: "Parques y espacios abiertos" } 2772 ]; 2773 // Cuántas opciones se enseñan de entrada en cada paso; el resto va tras 2774 // "Ver más opciones". Las listas están ordenadas para que las visibles sean 2775 // las que cubren a más gente: en objetivos, las tres primeras son las que 2776 // más entrenadores trabajan (53, 47 y 45 de 95). 2777 var VISIBLES = { donde: 2, objetivo: 3 }; 2778 2779 // Son las especialidades tal cual están en `entrenadores.especialidades`, 2780 // porque la RPC filtra con `especialidades @> ARRAY[p_objetivo]`: cualquier 2781 // slug inventado aquà no casarÃa con nadie y devolverÃa lista vacÃa. Se 2782 // ofrecen las 6 con cobertura real (11 y 5 entrenadores las menos vistas de 2783 // esta lista); las de cola larga caen en "Aún no lo sé", que no filtra. 2784 var OBJETIVOS = [ 2785 { value: "Pérdida de peso", label: "Perder peso" }, 2786 { value: "Ganar masa muscular", label: "Ganar masa muscular" }, 2787 { value: "Mejora de forma fÃsica", label: "Estar en forma" }, 2788 { value: "Entrenamiento funcional", label: "Entrenamiento funcional" }, 2789 { value: "Recuperación de lesiones", label: "Recuperarme de una lesión" }, 2790 { value: "Preparación para deportistas", label: "Preparar una prueba o competición" }, 2791 { value: "", label: "Aún no lo tengo claro", nota: "Te ayudarán a definirlo" } 2792 ]; 2793 2794 // `cuando_empieza` no tiene hoy ni una fila rellena en toda la tabla (el 2795 // campo existe en el formulario de la ficha pero no llega), asà que estos 2796 // valores no chocan con nada previo. Son texto legible a propósito: el 2797 // entrenador los lee tal cual en su panel, sin traducción. 2798 var CUANDOS = [ 2799 { value: "Lo antes posible", label: "Lo antes posible" }, 2800 { value: "En 2-4 semanas", label: "En 2-4 semanas" }, 2801 { value: "En 1-2 meses", label: "En 1 o 2 meses" }, 2802 { value: "Solo me estoy informando", label: "Solo me estoy informando" } 2803 ]; 2804 2805 var NIVELES = [ 2806 { value: "sedentario", label: "Sedentario, casi no me muevo" }, 2807 { value: "poco-activo", label: "Poco activo, 1-2 dÃas" }, 2808 { value: "activo", label: "Activo, 3-4 dÃas" }, 2809 { value: "muy-activo", label: "Muy activo, 5 dÃas o más" } 2810 ]; 2811 2812 var estado = { 2813 indice: 0, // posición dentro de pasos() 2814 fase: "preguntas", // "preguntas" | "confirmacion" | "cierre" 2815 modalidad: "", 2816 abiertas: {}, // pasos cuyo "Ver más" ya se ha pulsado 2817 ciudad: "", 2818 ciudadDePagina: false, // en las 60 páginas de ciudad tampoco se pregunta 2819 objetivo: "", 2820 objetivoDePagina: false, // en /objetivo/* no se pregunta, ya lo sabemos 2821 objetivoRespondido: false, 2822 cuando: "", 2823 nombre: "", 2824 email: "", 2825 telefono: "", 2826 enviando: false, 2827 error: "", 2828 top3: [], 2829 top3Clave: null, // respuestas con las que se busco ese reparto 2830 claimToken: null, 2831 2832 // Solo para la medición: qué pantallas se han contado ya en esta apertura, 2833 // cuándo se abrió y si esta apertura cuenta. Las previsualizaciones y el 2834 // ?recomendador=1 no cuentan: si no, cada vez que se revisa el copy se 2835 // ensucia el embudo con una sesión que no es de nadie. 2836 vistas: {}, 2837 abiertoEn: 0, 2838 midiendo: true 2839 }; 2840 2841 // La secuencia de pantallas no es fija: quien entrena online no tiene que 2842 // decir su ciudad, y en una página de ciudad o de objetivo esa respuesta ya 2843 // la da la propia página. Se calcula en cada render en vez de encadenar 2844 // condiciones dentro de cada paso â un contador fijo de "paso N de 4" con 2845 // ramas de distinta longitud acaba pintando 5 pantallas bajo 4 puntos. 2846 //
2847 // Nunca se pregunta algo que ya sabemos: quien entra por 2848 // /entrenadores-personales/valencia ya ha dicho dónde busca, y volver a 2849 // preguntárselo se lee como que no le hemos escuchado. 2850 function pasos() { 2851 var lista = ["donde"]; 2852 if (!estado.objetivoDePagina) lista.push("objetivo"); 2853 lista.push("cuando", "datos"); 2854 return lista; 2855 } 2856 2857 function pasoActual() { 2858 return pasos()[estado.indice]; 2859 } 2860 2861 var ciudades = []; 2862 2863 // ------------------------------------------------------------------ utils 2864 2865 function esc(str) { 2866 var d = document.createElement("div"); 2867 d.textContent = str == null ? "" : String(str); 2868 return d.innerHTML; 2869 } 2870 2871 function leer(clave, almacen) { 2872 try { return almacen.getItem(clave); } catch (e) { return null; } 2873 } 2874 2875 function escribir(clave, valor, almacen) { 2876 try { almacen.setItem(clave, valor); } catch (e) {} 2877 } 2878 2879 // La ciudad de las 60 páginas de ciudad sale de #re-ciudad-nombre, el 2880 // elemento oculto bindeado al campo Nombre de la colección Ciudades que ya 2881 // usa el glue del listado. Se usa el nombre y no el slug porque la RPC casa 2882 // por `ciudad_nombre ILIKE` y hay ciudades cuyo slug no coincide con su 2883 // nombre ("la-sagra-toledo" contra "La sagra Toledo"). 2884 function ciudadDeLaPagina() { 2885 var el = document.getElementById("re-ciudad-nombre"); 2886 if (el && el.textContent.trim()) return el.textContent.trim(); 2887 var param = new URLSearchParams(location.search).get("ciudad"); 2888 return param ? param.trim() : ""; 2889 } 2890 2891 // Las marcas que deja NFD al separar las tildes (U+0300-U+036F). Va con 2892 // fromCharCode y no con un escape unicode porque este bloque se sube al 2893 // embed dentro del JSON de una llamada MCP y esa secuencia no sobrevive: 2894 // queda el carácter combinante suelto, invisible en el editor. El rango es 2895 // exactamente el mismo. La propiedad Diacritic no vale como atajo: deja 2896 // fuera 19 puntos de este bloque. 2897 var MARCAS_COMBINANTES = new RegExp( 2898 "[" + String.fromCharCode(0x300) + "-" + String.fromCharCode(0x36f) + "]", "g"); 2899 2900 // En /objetivo/* el objetivo ya lo dice la propia página: preguntarlo otra 2901 // vez es hacerle repetir al usuario lo que acaba de elegir al entrar. 2902 function aSlug(texto) { 2903 return texto.toLowerCase()
2904 .normalize("NFD").replace(MARCAS_COMBINANTES, "") 2905 .replace(/[^a-z0-9]+/g, "-") 2906 .replace(/^-|-$/g, ""); 2907 } 2908 2909 function objetivoDeLaPagina() { 2910 var m = location.pathname.match(/^\/objetivo\/([^/]+)/); 2911 if (!m) return null; 2912 var encontrado = OBJETIVOS.filter(function (o) { return o.value; }) 2913 .find(function (o) { return aSlug(o.value) === m[1]; }); 2914 return encontrado ? encontrado.value : null; 2915 } 2916 2917 // ------------------------------------------------------------------ datos 2918 2919 function rest(ruta, opciones) { 2920 var o = opciones || {}; 2921 return fetch(SUPABASE_URL + ruta, { 2922 method: o.method || "GET", 2923 headers: { 2924 apikey: SUPABASE_ANON_KEY, 2925 Authorization: "Bearer " + SUPABASE_ANON_KEY, 2926 "Content-Type": "application/json" 2927 }, 2928 body: o.body ? JSON.stringify(o.body) : undefined 2929 }).then(function (res) { 2930 if (!res.ok) throw new Error(ruta + " -> " + res.status); 2931 return res.json(); 2932 }); 2933 } 2934 2935 function cargarCiudades() { 2936 if (ciudades.length) return Promise.resolve(ciudades); 2937 return rest("/rest/v1/entrenadores_publico?select=ciudad_nombre&tipo_entrenador=eq.personal&ciudad_nombre=not.is.null") 2938 .then(function (filas) { 2939 ciudades = Array.from(new Set(filas.map(function (f) { return f.ciudad_nombre; }))) 2940 .sort(function (a, b) { return a.localeCompare(b, "es"); }); 2941 var dl = document.getElementById("re-rec-ciudades"); 2942 if (dl) dl.innerHTML = ciudades.map(function (c) { return '<option value="' + esc(c) + '">'; }).join(""); 2943 return ciudades; 2944 }) 2945 .catch(function (err) { 2946 // Sin la lista el campo sigue siendo un input de texto libre y la RPC 2947 // casa por ILIKE, asà que se puede enviar igual: solo se pierde el 2948 // autocompletado. No es motivo para bloquear el formulario. 2949 console.warn("[re-recomendador] no se pudieron cargar las ciudades", err); 2950 return []; 2951 }); 2952 } 2953 2954 // A quién se le manda. Se pide a la MISMA RPC que pinta el listado público, 2955 // asà que el orden es el mismo que ve el usuario si navega él solo â no un 2956 // ranking paralelo que dijera otra cosa. 2957 // 2958 // NO se fuerza a que salgan tres. Si con lo que ha pedido solo encaja uno, se 2959 // le manda a ese uno y ya: rellenar el hueco con gente que no encaja es un 2960 // mal reparto para el usuario y un contacto quemado para el entrenador. 2961 // Valladolid es el caso real: un único entrenador en toda la ciudad, y la 2962 // versión vieja acababa soltando la ciudad para llegar a tres, asà que 2963 // devolvÃa tres de cualquier sitio menos el de Valladolid. 2964 // 2965 // Los filtros solo se sueltan cuando no encaja NADIE, de menos a más 2966 // importante: primero el objetivo (casi cualquier entrenador sirve para un 2967 // objetivo que no tenga marcado), luego la modalidad, y la ciudad la última 2968 // porque quien no cubre la zona no puede atender a este cliente y punto. 2969 function buscarTop3() { 2970 var intentos = [ 2971 { objetivo: estado.objetivo, modalidad: estado.modalidad, ciudad: estado.ciudad }, 2972 { objetivo: "", modalidad: estado.modalidad, ciudad: estado.ciudad }, 2973 { objetivo: "", modalidad: "", ciudad: estado.ciudad }, 2974 { objetivo: "", modalidad: estado.modalidad, ciudad: "" } 2975 ]; 2976 2977 function intentar(i) { 2978 if (i >= intentos.length) return Promise.resolve([]); 2979 var f = intentos[i]; 2980 return rest("/rest/v1/rpc/buscar_entrenadores_listado", { 2981 method: "POST", 2982 body: { 2983 p_objetivo: f.objetivo || null, 2984 p_modalidad: f.modalidad || null, 2985 p_ciudad: f.ciudad || null, 2986 p_perfil: null, 2987 p_precio_min: null, 2988 p_precio_max: null, 2989 p_orden: "relevancia", 2990 p_limite: 3, 2991 p_offset: 0 2992 } 2993 }).then(function (filas) { 2994 // Basta con uno: el siguiente intento no serÃa "mejor", serÃa menos 2995 // fiel a lo que ha pedido. 2996 if (filas && filas.length) return filas; 2997 return intentar(i + 1); 2998 }); 2999 } 3000 3001 return intentar(0); 3002 } 3003 3004 // ------------------------------------------------------------------ vistas 3005 3006 function dotsHTML() { 3007 var total = pasos().length; 3008 var html = ""; 3009 for (var i = 0; i < total; i++) { 3010 html += '<span class="' + (i <= estado.indice ? "re-rec-hecho" : "") + '"></span>'; 3011 } 3012 return '<div class="re-rec-dots">' + html + "</div>"; 3013 } 3014 3015 function cabeceraHTML(eyebrow, titulo, sub) { 3016 return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">✕</button>' + 3017 dotsHTML() + 3018 (eyebrow ? '<p class="re-rec-eyebrow">' + esc(eyebrow) + "</p>" : "") + 3019 "<h3>" + esc(titulo) + "</h3>" + 3020 (sub ? '<p class="re-rec-sub">' + esc(sub) + "</p>" : ""); 3021 } 3022 3023 // Las opciones se identifican por su valor y no por su posición: con la 3024 // lista de modalidades plegada, un Ãndice apuntarÃa a la posición dentro de 3025 // lo que se está pintando y no dentro de la lista real. 3026 // Pinta las opciones de un paso ya recortadas a las visibles, y decide sola 3027 // si hace falta el "Ver más": asà ningún paso puede quedarse con el enlace 3028 // puesto pero sin nada que destapar. 3029 function opcionesDelPaso(paso, lista, seleccionado) { 3030 var tope = VISIBLES[paso]; 3031 var plegado = tope && !estado.abiertas[paso] && lista.length > tope; 3032 return opcionesHTML(plegado ? lista.slice(0, tope) : lista, seleccionado, plegado); 3033 } 3034 3035 function opcionesHTML(lista, seleccionado, verMas) { 3036 return '<div class="re-rec-opciones">' + lista.map(function (o) { 3037 var on = o.value === seleccionado; 3038 return '<button type="button" class="re-rec-op' + (on ? " re-rec-op-on" : "") + '"' + 3039 ' data-accion="opcion" data-valor="' + esc(o.value) + '">' + 3040 "<span>" + esc(o.label) + 3041 (o.nota ? '<span class="re-rec-op-nota">' + esc(o.nota) + "</span>" : "") + 3042 "</span></button>"; 3043 }).join("") + 3044 (verMas ? '<button type="button" class="re-rec-vermas" data-accion="ver-mas">Ver más opciones</button>' : "") + 3045 "</div>"; 3046 } 3047 3048 function navHTML(conAtras, textoBoton) { 3049 return '<div class="re-rec-nav">' + 3050 (conAtras ? '<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>' : "") + 3051 (textoBoton ? '<button type="button" class="re-rec-btn" data-accion="siguiente">' + esc(textoBoton) + "</button>" : "") + 3052 "</div>"; 3053 } 3054 3055 function errorHTML() { 3056 return '<div class="re-rec-error"' + (estado.error ? ' style="display:block"' : "") + ">" + esc(estado.error) + "</div>"; 3057 } 3058 3059 // ¿Hace falta preguntar la ciudad? Solo si ya ha dicho que quiere entrenar 3060 // presencialmente y la página no nos la ha dado hecha. Entrenar por
3061 // videollamada no depende de dónde viva, y filtrar por su ciudad ahà solo 3062 // servirÃa para dejar fuera a entrenadores que le valdrÃan igual. 3063 function pideCiudad() { 3064 return !!estado.modalidad && estado.modalidad !== "Online" && !estado.ciudadDePagina; 3065 } 3066 3067 // Modalidad y ciudad comparten pantalla: son la misma pregunta ("¿dónde?") 3068 // partida en dos, y separarlas metÃa un paso entero para un solo campo. El 3069 // campo de ciudad no está desde el principio â aparece al elegir una 3070 // modalidad presencial, porque hasta entonces no se sabe si hace falta. 3071 function pasoDondeHTML() { 3072 // El número va escrito y se cuenta solo. Dos motivos: el recorrido no 3073 // siempre tiene los mismos pasos (el objetivo se salta en /objetivo/*), y 3074 // la última pantalla no es una pregunta sino el formulario de contacto â 3075 // por eso se descuenta uno y no se anuncian cuatro preguntas. 3076 var PALABRAS = { 1: "Una", 2: "Dos", 3: "Tres" }; 3077 return cabeceraHTML( 3078 "Te ayudamos a decidir", 3079 "¿TodavÃa no lo tienes claro? Te recomendamos un entrenador.", 3080 (PALABRAS[pasos().length - 1] || "Unas") + " preguntas rápidas" + 3081 " y te ponemos en contacto con los entrenadores" + 3082 (estado.ciudad ? " de " + estado.ciudad : "") + 3083 " que mejor encajan contigo. Gratis y sin compromiso." 3084 ) + 3085 // Al dejar de ser el tÃtulo, la primera pregunta necesita su propia 3086 // etiqueta: sin ella las cuatro opciones quedan colgando del subtÃtulo 3087 // y no se entiende qué se está eligiendo. 3088 '<p class="re-rec-pregunta">¿Dónde te gustarÃa entrenar?</p>' + 3089 opcionesDelPaso("donde", MODALIDADES, estado.modalidad) + 3090 (pideCiudad() 3091 ? '<div class="re-rec-campo re-rec-campo-ciudad">' + 3092 '<label for="re-rec-ciudad">¿En qué ciudad?</label>' + 3093 '<input type="text" id="re-rec-ciudad" list="re-rec-ciudades" autocomplete="address-level2" placeholder="Empieza a escribirâ¦" value="' + esc(estado.ciudad) + '">' + 3094 '<datalist id="re-rec-ciudades"></datalist>' + 3095 "</div>" 3096 : "") + 3097 errorHTML() + 3098 (pideCiudad() ? navHTML(false, "Siguiente") : ""); 3099 } 3100 3101 function pasoObjetivoHTML() { 3102 return cabeceraHTML("", "¿Cuál es tu objetivo?", "Con esto sabemos qué entrenadores están especializados en lo que buscas.") + 3103 // El centinela es `null` y no "": "" es una respuesta valida ("aun no lo 3104 // tengo claro"), y sin distinguirla esa opcion saldria ya marcada antes 3105 // de que el usuario hubiera contestado nada. 3106 opcionesDelPaso("objetivo", OBJETIVOS, estado.objetivoRespondido ? estado.objetivo : null) + 3107 errorHTML() + 3108 navHTML(true, ""); 3109 } 3110 3111 function pasoCuandoHTML() { 3112 return cabeceraHTML("", "¿Cuándo te gustarÃa empezar?", "Asà te proponen un plan realista desde el primer mensaje.") + 3113 opcionesHTML(CUANDOS, estado.cuando) + 3114 errorHTML() + 3115 navHTML(true, ""); 3116 } 3117 3118 // "Ana, Marcos y Juanma" â coma entre todos menos el último, que va con "y". 3119 function enumerar(nombres) { 3120 if (nombres.length === 1) return nombres[0]; 3121 return nombres.slice(0, -1).join(", ") + " y " + nombres[nombres.length - 1]; 3122 } 3123 3124 // El subtÃtulo de la última pantalla nombra a los tres entrenadores en 3125 // cuanto se sabe quiénes son. Mientras la búsqueda va en camino (o si 3126 // falla) se cae a la versión sin nombres: pedir el email es el momento de 3127 // menos paciencia del recorrido y no puede quedarse esperando a nada. 3128 function subtituloDatosHTML() { 3129 if (!estado.top3.length) { 3130 return "Enviaremos tu solicitud a los entrenadores que mejor encajan contigo. Te escribirán ellos directamente."; 3131 } 3132 var nombres = estado.top3.map(function (e) { return "<strong>" + esc(e.nombre) + "</strong>"; }); 3133 return enumerar(nombres) + 3134 (estado.top3.length === 1 ? " es el que mejor encaja" : " son los que mejor encajan") + 3135 " contigo. Te " + (estado.top3.length === 1 ? "escribirá" : "escribirán") + " directamente."; 3136 } 3137 3138 // El aviso legal tiene que decir con cuánta gente se comparte el dato, y 3139 // ya no siempre son tres: si en la ciudad solo encaja uno, es uno. Si la 3140 // búsqueda aún no ha vuelto no se inventa un número. 3141 function conQuienHTML() { 3142 var cuantos = estado.top3.length; 3143 if (cuantos === 1) return "este entrenador"; 3144 if (cuantos) return "estos " + cuantos + " entrenadores"; 3145 return "los entrenadores que mejor encajen contigo"; 3146 } 3147 3148 function pasoDatosHTML() { 3149 return cabeceraHTML("", "Ãltimo paso: ¿cómo te contactamos?", null) + 3150 '<p class="re-rec-sub">' + subtituloDatosHTML() + "</p>" + 3151 '<div class="re-rec-campo">' + 3152 '<label for="re-rec-nombre">Nombre</label>' + 3153 '<input type="text" id="re-rec-nombre" autocomplete="given-name" value="' + esc(estado.nombre) + '">' + 3154 "</div>" + 3155 // El email vuelve al paso 1 y es obligatorio: sin él no hay chat, porque 3156 // no hay a quién avisar de un mensaje nuevo ni con qué entrar al área 3157 // (la cuenta es passwordless por email). Ver specs/spec-chat-plataforma.md. 3158 '<div class="re-rec-campo">' + 3159 '<label for="re-rec-email">Email</label>' + 3160 '<input type="email" id="re-rec-email" autocomplete="email" inputmode="email" placeholder="[email protected]" value="' + esc(estado.email) + '">' + 3161 "</div>" + 3162 // El teléfono se queda, pero pasa a opcional. No se quita: es el dato con 3163 // el que el entrenador llama, y en los 90 dÃas previos las solicitudes 3164 // que solo lo traÃan se gestionaron casi todas, frente a dos tercios de 3165 // las que solo traÃan email. 3166 '<div class="re-rec-campo">' + 3167 '<label for="re-rec-telefono">Teléfono <span class="re-rec-opcional">(opcional)</span></label>' + 3168 '<input type="tel" id="re-rec-telefono" autocomplete="tel" inputmode="tel" placeholder="600 11 22 33" value="' + esc(estado.telefono) + '">' + 3169 "</div>" + 3170 errorHTML() + 3171 '<div class="re-rec-nav">' + 3172 '<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>' + 3173 '<button type="button" class="re-rec-btn" data-accion="enviar"' + (estado.enviando ? " disabled" : "") + ">" +
3174 (estado.enviando ? "Enviandoâ¦" : "Enviar mi solicitud") + 3175 "</button>" + 3176 "</div>" + 3177 '<p class="re-rec-legal">Al enviar aceptas nuestra <a href="' + PRIVACIDAD_URL + '" target="_blank" rel="noopener">polÃtica de privacidad</a>. Compartiremos tu nombre y tus datos de contacto con ' + conQuienHTML() + ' para que puedan responderte.</p>'; 3178 } 3179 3180 // Estrella siempre en el verde de marca, con independencia de la nota: es la 3181 // convención del listado público (§48 del design system), no un color de 3182 // estado que deba variar con el resultado. 3183 var ESTRELLA_SVG = '<svg class="re-rec-estrella" width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0l2.47 5.01 5.53.8-4 3.9.94 5.5L8 12.6l-4.94 2.6.94-5.5-4-3.9 5.53-.8L8 0z" fill="#089F3B"/></svg>'; 3184 3185 // La pÃldora solo lleva el check cuando ese entrenador trabaja de verdad el 3186 // objetivo que ha pedido el usuario. Cuando el reparto ha tenido que soltar 3187 // el filtro de objetivo para completar los tres (o cuando ha contestado "aún 3188 // no lo tengo claro"), se enseña su especialidad principal sin check: decir 3189 // "coincide con lo que buscas" de alguien que no lo trabaja es mentir en la 3190 // pantalla donde más confianza hace falta. 3191 function especialidadHTML(e) { 3192 var suyas = e.especialidades || []; 3193 if (!suyas.length) return ""; 3194 3195 var coincide = estado.objetivo && suyas.indexOf(estado.objetivo) !== -1; 3196 var texto = coincide ? etiquetaObjetivo(estado.objetivo) : suyas[0]; 3197 return '<span class="re-rec-pill' + (coincide ? " re-rec-pill-match" : "") + '">' + 3198 (coincide ? "â " : "") + esc(texto) + 3199 "</span>"; 3200 } 3201 3202 // "Pérdida de peso" es como está guardado; "Perder peso" es como se lo hemos 3203 // preguntado. Se le devuelve su propia palabra. 3204 function etiquetaObjetivo(valor) { 3205 var op = OBJETIVOS.find(function (o) { return o.value === valor; }); 3206 return op ? op.label : valor; 3207 } 3208 3209 function fichaHTML(e) { 3210 // Sin `loading="lazy"`: son tres fotos que se pintan ya visibles y en el 3211 // momento en que se muestra el modal. Diferirlas solo consigue que la 3212 // confirmación aparezca un instante con tres huecos grises. 3213 var foto = e.foto_url 3214 ? '<img class="re-rec-foto" src="' + esc(e.foto_url) + '" alt="' + esc(e.nombre) + '">' 3215 : '<div class="re-rec-foto"></div>'; 3216 3217 // Nota y experiencia, y nada más: el precio se probó como relleno y se 3218 // descartó (decisión de Esteve del 17/08/2026). Esta pantalla va de si el 3219 // entrenador encaja, y meter una cifra en euros justo cuando el usuario 3220 // acaba de dar sus datos convierte una confirmación en un escaparate. 3221 // 3222 // La consecuencia asumida es que muchas fichas se quedan solo con nombre y 3223 // especialidad: de los 93 entrenadores personales, 22 tienen años de 3224 // experiencia y 15 tienen opiniones. Eso no se arregla aquà sino haciendo 3225 // que rellenen esos campos. 3226 // 3227 // Nunca se pinta un cero: un "0 opiniones" o un "0 años" en la pantalla 3228 // que tiene que dar confianza juega en contra del entrenador. 3229 var datos = []; 3230 if (e.total_reviews > 0) { 3231 datos.push(ESTRELLA_SVG + " " + e.rating_promedio + " (" + e.total_reviews + 3232 (e.total_reviews === 1 ? " opinión)" : " opiniones)")); 3233 } 3234 if (e.anos_experiencia > 0) { 3235 // "de experiencia" completo y no solo "2 años": suelto, se lee como la 3236 // edad del entrenador. 3237 datos.push(e.anos_experiencia + (e.anos_experiencia === 1 ? " año" : " años") + " de experiencia"); 3238 } 3239 3240 // La ficha NO es un enlace, a propósito. Debajo de estas tres tarjetas 3241 // está el paso de completar el perfil, que es lo único que queda por 3242 // hacer y lo que da valor a la solicitud recién enviada; un enlace al 3243 // perfil aquà arriba se lleva al usuario a otra pestaña justo antes de 3244 // llegar a él. Las fotos y los datos ya cumplen su función, que es 3245 // demostrar que la solicitud ha ido a gente real. 3246 return '<div class="re-rec-ficha">' + 3247 foto + 3248 '<span class="re-rec-ficha-datos">' + 3249 '<span class="re-rec-ficha-nombre">' + esc(e.nombre) + "</span>" + 3250 especialidadHTML(e) + 3251 // Cada dato en su lÃnea y no separados por "·": en una tarjeta de 3252 // 180px, "5 (12 opiniones) · 8 años de experiencia" se parte por donde 3253 // le toca y deja huérfano medio dato en la lÃnea siguiente. 3254 datos.map(function (d) { return '<span class="re-rec-ficha-meta">' + d + "</span>"; }).join("") + 3255 "</span>" + 3256 "</div>"; 3257 } 3258 3259 function opcionesSelectHTML(lista, placeholder) { 3260 return '<option value="">' + esc(placeholder) + "</option>" + lista.map(function (o) { 3261 return '<option value="' + esc(o.value) + '">
3261' + esc(o.label) + "</option>"; 3262 }).join(""); 3263 } 3264 3265 // Paso 2 del journey de usuario. Va SIEMPRE después de la solicitud, nunca 3266 // antes: es opcional y no puede convertirse en un peaje para escribir a un 3267 // entrenador. `claim_token` es lo que devuelve solicitar-contacto-entrenador 3268 // y lo único que prueba que quien rellena esto es quien acaba de enviar. 3269 function confirmacionHTML() { 3270 var cuantos = estado.top3.length; 3271 var p2 = estado.claimToken 3272 ? '<div class="re-rec-p2">' + 3273 "<h4>Cuéntanos algo más</h4>" + 3274 '<p class="re-rec-sub" style="font-size:14px;margin-bottom:14px">Opcional y son 20 segundos, pero con esto te preparan una propuesta a tu medida.</p>' + 3275 // El nivel de actividad va a fila completa porque su etiqueta no 3276 // cabe a media columna; edad y peso comparten fila, que son dos 3277 // campos de tres caracteres. 3278 '<div class="re-rec-p2-grid">' + 3279 // El email ya no se pide aquÃ: lo trae el paso 1, obligatorio. 3280 // Pedirlo dos veces es peor que pedirlo una. 3281 '<div class="re-rec-campo re-rec-p2-full"><label for="re-rec-actividad">¿Cuánto ejercicio haces ahora?</label><select id="re-rec-actividad">' + opcionesSelectHTML(NIVELES, "Selecciona tu nivel") + "</select></div>" + 3282 '<div class="re-rec-campo"><label for="re-rec-edad">Edad</label><input type="number" id="re-rec-edad" min="10" max="120" inputmode="numeric" placeholder="Años"></div>' + 3283 '<div class="re-rec-campo"><label for="re-rec-peso">Peso</label><input type="number" id="re-rec-peso" min="20" max="400" step="0.1" inputmode="decimal" placeholder="Kg"></div>' + 3284 // Altura y disponibilidad los pide la tarjeta de "Solicitud inicial" 3285 // del panel del entrenador, que enseña ocho datos del cliente. 3286 // Están aquà y no en el paso 1 por lo mismo que el resto: el paso 1 3287 // pide lo mÃnimo para poder responder, esto es lo que ayuda a 3288 // preparar la propuesta y se pregunta cuando ya no cuesta conversión. 3289 '<div class="re-rec-campo"><label for="re-rec-altura">Altura</label><input type="number" id="re-rec-altura" min="100" max="250" inputmode="numeric" placeholder="Cm"></div>' + 3290 '<div class="re-rec-campo"><label for="re-rec-disponibilidad">¿Cuándo puedes entrenar?</label><input type="text" id="re-rec-disponibilidad" placeholder="Tardes entre semana"></div>' + 3291 '<div class="re-rec-campo re-rec-p2-full"><label for="re-rec-notas">¿Algo más que debamos saber?</label><textarea id="re-rec-notas" placeholder="Lesiones, horarios que te van bien, experiencia previaâ¦"></textarea></div>' + 3292 "</div>" + 3293 '<div class="re-rec-error" id="re-rec-p2-error"></div>' + 3294 '<div class="re-rec-acciones">' + 3295 '<button type="button" class="re-rec-btn" data-accion="guardar-perfil">Enviar mis datos</button>' + 3296 '<button type="button" class="re-rec-saltar" data-accion="cerrar">Ahora no</button>' + 3297 "</div>" + 3298 "</div>" 3299 : ""; 3300 3301 return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">✕</button>' + 3302 '<p class="re-rec-eyebrow">Solicitud enviada</p>' + 3303 "<h3>" + (cuantos === 1 ? "Este es tu entrenador" : "Estos son tus " + cuantos + " entrenadores") + "</h3>" + 3304 '<p class="re-rec-sub">Ya tienen tu solicitud y te escribirán directamente.</p>' + 3305 '<div class="re-rec-fichas">' + estado.top3.map(fichaHTML).join("") + "</div>" + 3306 p2; 3307 } 3308 3309 // `ofreceCuenta` sigue siendo un parámetro y no un `true` fijo aunque el 3310 // email vuelva a ser obligatorio en el paso 1: este cierre también lo pinta 3311 // el camino en el que el paso 2 falla, y ahà no se da nada por hecho. 3312 function cierreHTML(guardado, ofreceCuenta) { 3313 var corto = estado.nombre ? estado.nombre.trim().split(" ")[0] : ""; 3314 return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">✕</button>' + 3315 "<h3>" + esc(guardado ? "¡Listo!" : (corto ? "¡Gracias, " + corto + "!" : "¡Gracias!")) + "</h3>" + 3316 '<p class="re-rec-sub">' + 3317 (guardado 3318 ? "Tus datos ya van con la solicitud. Se pondrán en contacto contigo muy pronto." 3319 : "Tu solicitud está enviada. Se pondrán en contacto contigo muy pronto.") + 3320 "</p>" + 3321 (estado.claimToken && ofreceCuenta 3322 ? '<div class="re-rec-p2">' + 3323 "<h4>¿Quieres seguir tus solicitudes?</h4>" + 3324 '<p class="re-rec-sub" style="font-size:14px;margin-bottom:14px">Crea tu cuenta y consulta cuándo las ve cada entrenador. Sin contraseña y sin salir de aquÃ.</p>' + 3325 '<div class="re-rec-error" id="re-rec-cuenta-error"></div>' + 3326 '<button type="button" class="re-rec-btn" data-accion="crear-cuenta">Crear mi cuenta</button>' + 3327 '<p class="re-rec-legal">Al crear la cuenta aceptas nuestra <a href="' + PRIVACIDAD_URL + '" target="_blank" rel="noopener">polÃtica de privacidad</a>.</p>' + 3328 "</div>" 3329 : ""); 3330 } 3331 3332 // ------------------------------------------------------------------ puente 3333 3334 // El widget no cabe en un solo Embed de Webflow (tope de 50.000 caracteres), 3335 // asi que va partido en dos bloques. Esto es TODO lo que cruza de este 3336 // bloque al siguiente; es interno y no es la API publica, que sigue siendo 3337 // window.reRecomendador y la monta el bloque de comportamiento. 3338 // 3339 // `estado` cruza como referencia, no como copia: los dos bloques escriben 3340 // sobre el mismo objeto y esa es justo la idea. 3341 window.__reRecNucleo = { 3342 RUTAS_EXCLUIDAS: RUTAS_EXCLUIDAS, 3343 SEGUNDOS_ESPERA: SEGUNDOS_ESPERA, 3344 SCROLL_DISPARO: SCROLL_DISPARO, 3345 CLAVE_APAGADO: CLAVE_APAGADO, 3346 CLAVE_SESION: CLAVE_SESION,
3347 MODALIDADES: MODALIDADES, 3348 OBJETIVOS: OBJETIVOS, 3349 CUANDOS: CUANDOS, 3350 estado: estado, 3351 pasos: pasos, 3352 pasoActual: pasoActual, 3353 pideCiudad: pideCiudad, 3354 leer: leer, 3355 escribir: escribir, 3356 ciudadDeLaPagina: ciudadDeLaPagina, 3357 objetivoDeLaPagina: objetivoDeLaPagina, 3358 rest: rest, 3359 cargarCiudades: cargarCiudades, 3360 buscarTop3: buscarTop3, 3361 pasoDondeHTML: pasoDondeHTML, 3362 pasoObjetivoHTML: pasoObjetivoHTML, 3363 pasoCuandoHTML: pasoCuandoHTML, 3364 pasoDatosHTML: pasoDatosHTML, 3365 subtituloDatosHTML: subtituloDatosHTML, 3366 confirmacionHTML: confirmacionHTML, 3367 cierreHTML: cierreHTML 3368 }; 3369})(); 3370</script>
3370 3371</div><div class="w-embed w-script"><!-- RedEntrenadores - Parche del pop-up recomendador: a quien se reparte --> 3372<!-- Va en el Footer V1, ENTRE el bloque 2 (JS nucleo) y el bloque 3 (JS 3373 comportamiento), y tiene que seguir ahi: el bloque 3 se lleva `buscarTop3` 3374 a una variable local nada mas cargar, asi que quien quiera cambiarla tiene 3375 que hacerlo antes. 3376 3377 Que cambia. El nucleo reparte con `buscar_entrenadores_listado`, que es la 3378 RPC del listado publico y ordena por un score de cinco criterios (plan 35, 3379 puja 25, recomendaciones 20, calidad 12, valoracion 8). Eso vale para 3380 ordenar un listado, pero repartir no es ordenar: con 180 entrenadores el 3381 score le daba casi todos los contactos a los mismos seis. 3382 3383 Desde el 12/09/2026 el reparto son dos criterios y ni uno mas 3384 (`buscar_entrenadores_reparto`, migracion 20260913030000): 3385 3386 1. primero los de plan de pago, que pagan por eso; 3387 2. despues todos los demas, empezando por quien menos leads ha recibido. 3388 3389 Y sigue haciendo lo de antes: `p_solo_contactables` deja fuera del reparto 3390 a los perfiles de escaparate (`entrenadores.recibe_contactos = false`), que 3391 tienen que seguir saliendo en el listado y no pueden salir aqui, porque 3392 aqui no se enseña, se reparte el contacto. El servidor ya los descartaba 3393 (`solicitar-contacto-entrenador` no les manda nada), pero el pop-up 3394 enseñaba su tarjeta diciendo "ya tienen tu solicitud", que de ellos no era 3395 verdad. 3396 3397 Por que un parche y no una linea en el bloque 2. Escribir ese embed por 3398 MCP obliga a reproducir sus 34 KB enteros a mano, sobre el flujo que trae 3399 los leads. Esto son 30 lineas y se puede tirar entero el dia que se toque 3400 el nucleo: entonces, cambia alli la RPC y borra este embed. 3401 3402 La cascada de intentos es copia literal del nucleo, con el corte en cuanto 3403 hay UNO (23/08/2026, cambiado en los dos sitios a la vez). Si algun dia 3404 cambia la del nucleo, el numero de version de abajo apaga el parche en vez 3405 de imponer la version vieja en silencio. 3406 3407 ID del embed: e4e348ca-b183-eafe-444a-cd8dbed588ae (componente Footer V1). -->
3408<script> 3409(function () { 3410 "use strict"; 3411 3412 var N = window.__reRecNucleo; 3413 if (!N || !N.buscarTop3) return; 3414 3415 // Copiado del bloque 2 con esta version. Si el nucleo cambia de version, el 3416 // parche se aparta: mejor que el pop-up reparta con el score viejo (el 3417 // servidor sigue filtrando) a que se quede congelado con una busqueda vieja. 3418 if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") { 3419 console.warn("[recomendador] parche de reparto desactivado: el nucleo es la version", window.RE_RECOMENDADOR_VERSION, "y el parche se escribio para la 1.0.0. Cambia la RPC a buscar_entrenadores_reparto en buscarTop3 y borra este embed."); 3420 return; 3421 } 3422 3423 var estado = N.estado; 3424 var rest = N.rest; 3425 3426 N.buscarTop3 = function buscarTop3() { 3427 var intentos = [ 3428 { objetivo: estado.objetivo, modalidad: estado.modalidad, ciudad: estado.ciudad }, 3429 { objetivo: "", modalidad: estado.modalidad, ciudad: estado.ciudad }, 3430 { objetivo: "", modalidad: "", ciudad: estado.ciudad }, 3431 { objetivo: "", modalidad: estado.modalidad, ciudad: "" } 3432 ]; 3433 3434 function intentar(i) { 3435 if (i >= intentos.length) return Promise.resolve([]); 3436 var f = intentos[i]; 3437 return rest("/rest/v1/rpc/buscar_entrenadores_reparto", { 3438 method: "POST", 3439 body: { 3440 p_objetivo: f.objetivo || null, 3441 p_modalidad: f.modalidad || null, 3442 p_ciudad: f.ciudad || null, 3443 p_limite: 3, 3444 // Va dentro de la busqueda y no filtrando el resultado porque el 3445 // LIMIT ya se ha aplicado: quitar a uno aqui fuera dejaria dos, y un 3446 // lead repartido de menos es un entrenador que no recibe nada. 3447 p_solo_contactables: true 3448 } 3449 }).then(function (filas) { 3450 // Basta con uno: el siguiente intento no seria "mejor", seria menos 3451 // fiel a lo que ha pedido. 3452 if (filas && filas.length) return filas; 3453 return intentar(i + 1); 3454 }); 3455 } 3456 3457 return intentar(0); 3458 }; 3459 3460 window.RE_RECOMENDADOR_ESCAPARATE = "2026-09-12-1"; 3461})(); 3462</script>
3462</div><div class="w-embed w-script"><!-- RedEntrenadores - Parche del pop-up recomendador: no preguntar lo que ya nos han dicho --> 3463<!-- Va en el Footer V1, ENTRE el parche 2b (escaparate) y el bloque 3 (JS 3464 comportamiento), y tiene que seguir ahi: el bloque 3 se lleva `pasos`, 3465 `pasoActual` y las cuatro funciones de pantalla a variables locales nada 3466 mas cargar, asi que quien quiera cambiarlas tiene que hacerlo antes. 3467 3468 Que arregla. El buscador de la home (widget-home-hero-buscar.html) ya le ha 3469 sacado a la persona el objetivo, la modalidad, la ciudad y cuando quiere 3470 empezar. Sin esto, el pop-up se abria igualmente por la primera pantalla 3471 ("¿donde quieres entrenar?") a preguntarle lo que acababa de escribir. 3472 3473 Como se le dice lo que ya sabemos. Quien abra el pop-up con respuestas 3474 puestas marca ademas `estado.sabidos`: 3475 3476 estado.sabidos = { donde: true, objetivo: true, cuando: true } 3477 3478 Solo se salta lo que este marcado Y ademas tenga valor de verdad. "Donde" 3479 es la unica con truco: esa pantalla pregunta modalidad y, si es presencial, 3480 tambien ciudad, asi que saltarla exige tener las dos. 3481 3482 El paso "datos" no se salta nunca: es donde se piden nombre y email, que es 3483 lo unico que no se puede adivinar de una frase. Con las cuatro respuestas 3484 puestas, el pop-up abre directamente ahi. 3485 3486 Sin `estado.sabidos` la lista es exactamente la del nucleo (["donde", 3487 "objetivo", "cuando", "datos"], con "objetivo" fuera en /objetivo/*). Quien 3488 llega por el pop-up automatico de los 25 segundos no nota ningun cambio y 3489 el embudo que ya se esta midiendo sigue igual. 3490 3491 Por que hay que parchear CUATRO cosas y no solo `pasos`: 3492 3493 - `pasoActual` la calcula el nucleo con su `pasos` interna, no con la 3494 exportada. Sin parchearla, el bloque 3 pintaria la pantalla de la lista 3495 vieja: la lista diria "datos" y la pantalla seguiria siendo "donde". 3496 - Los puntos de progreso y el "Tres preguntas rapidas" tambien salen de 3497 esa `pasos` interna, y estan dentro de `cabeceraHTML`, que no se 3498 exporta. Se corrigen sobre el HTML ya generado, que es feo pero cabe 3499 aqui; la alternativa era reescribir los 30 KB del nucleo a mano. 3500 - El boton "Atras" de la pantalla de datos esta escrito a pelo (no pasa 3501 por `navHTML`), asi que cuando datos es la unica pantalla se queda un 3502 boton que no lleva a ningun sitio. Se quita cuando no hay nada detras. 3503 3504 Desde el 23/08/2026 corrige ademas las tres frases del nucleo que daban por 3505 hecho que el reparto siempre son tres entrenadores; ya no lo es. 3506 3507 Todo esto se cae solo el dia que el nucleo incorpore `sabidos`: sube su 3508 RE_RECOMENDADOR_VERSION y este embed se apaga entero. Mismo criterio y 3509 mismo guardia de version que el parche 2b. -->
3510<script> 3511(function () { 3512 "use strict"; 3513 3514 var N = window.__reRecNucleo; 3515 if (!N || !N.pasos || !N.estado) return; 3516 3517 // Igual que en el parche 2b: si el nucleo cambia de version, este parche se 3518 // aparta y vuelve el recorrido completo. Preguntar de mas es recuperable; 3519 // saltarse una pantalla que ha cambiado de contenido, no. 3520 if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") { 3521 console.warn("[recomendador] parche de pasos desactivado: el nucleo es la version", window.RE_RECOMENDADOR_VERSION, "y el parche se escribio para la 1.0.0. Metele `sabidos` a pasos() y borra este embed."); 3522 return; 3523 } 3524 3525 var estado = N.estado; 3526 3527 // La pantalla "donde" son dos preguntas en una. Saltarla solo vale si ninguna 3528 // de las dos queda en el aire: online no necesita ciudad, presencial si. 3529 function sabemosDonde() { 3530 if (!estado.modalidad) return false; 3531 if (estado.modalidad === "Online") return true; 3532 return !!estado.ciudad || !!estado.ciudadDePagina; 3533 } 3534 3535 N.pasos = function pasos() { 3536 var sabidos = estado.sabidos || {}; 3537 var lista = []; 3538 if (!sabidos.donde || !sabemosDonde()) lista.push("donde"); 3539 if (!estado.objetivoDePagina && !(sabidos.objetivo && estado.objetivo)) lista.push("objetivo"); 3540 if (!(sabidos.cuando && estado.cuando)) lista.push("cuando"); 3541 lista.push("datos"); 3542 return lista; 3543 }; 3544 3545 N.pasoActual = function pasoActual() { 3546 return N.pasos()[estado.indice]; 3547 }; 3548 3549 var PALABRAS = { 1: "Una", 2: "Dos", 3: "Tres" }; 3550 3551 function dotsHTML() { 3552 var total = N.pasos().length; 3553 var html = ""; 3554 for (var i = 0; i < total; i++) { 3555 html += '<span class="' + (i <= estado.indice ? "re-rec-hecho" : "") + '"></span>'; 3556 } 3557 return '<div class="re-rec-dots">' + html + "</div>"; 3558 } 3559 3560 // Cuantos entrenadores van a recibir la solicitud de verdad. En la primera 3561 // pintada de la pantalla de datos todavia no se sabe (la busqueda va en 3562 // camino), y ahi no se inventa un numero. 3563 function conQuien() { 3564 var n = (estado.top3 || []).length; 3565 if (n === 1) return "este entrenador"; 3566 if (n) return "estos " + n + " entrenadores"; 3567 return "los entrenadores que mejor encajen contigo"; 3568 } 3569 3570 // Cirugia sobre el HTML que devuelve el nucleo. Los reemplazos son literales 3571 // suyos: si alguno deja de aparecer, no rompe nada (el replace no encuentra 3572 // y devuelve el mismo HTML), solo deja de corregir esa parte. 3573 function corregir(html) { 3574 html = html.replace(/<div class="re-rec-dots">[\s\S]*?<\/div>/, dotsHTML()); 3575 3576 var preguntas = N.pasos().length - 1; // la ultima pantalla no es pregunta 3577 html = html.replace( 3578 /(?:Una|Dos|Tres|Unas) preguntas rápidas/, 3579 preguntas === 1 ? "Una pregunta rápida" : (PALABRAS[preguntas] || "Unas") + " preguntas rápidas" 3580 ); 3581 3582 // El reparto dejo de forzar tres (parche 2b, 23/08/2026): si en la ciudad 3583 // solo encaja uno, se le manda a uno. Estas tres frases del nucleo daban 3584 // por hecho que siempre eran tres, asi que la primera pantalla prometia 3585 // algo que no se iba a cumplir y el aviso legal decia con cuanta gente se 3586 // comparte el dato sin que fuera verdad. 3587 html = html.replace("con los 3 entrenadores", "con los entrenadores"); 3588 html = html.replace("Enviaremos tu solicitud a los 3 entrenadores", "Enviaremos tu solicitud a los entrenadores"); 3589 html = html.replace("con estos 3 entrenadores", "con " + conQuien()); 3590 3591 if (estado.indice === 0) { 3592 html = html.replace('<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>', ""); 3593 } 3594 return html; 3595 } 3596 3597 ["pasoDondeHTML", "pasoObjetivoHTML", "pasoCuandoHTML", "pasoDatosHTML"].forEach(function (nombre) { 3598 var original = N[nombre]; 3599 if (typeof original !== "function") return; 3600 N[nombre] = function () { return corregir(original.apply(null, arguments)); }; 3601 }); 3602})(); 3603</script>
3603</div><div class="w-embed w-script"><!-- ============================================================ 3604 RedEntrenadores â Parche del pop-up recomendador: gancho manual en una ruta excluida 3605 3606 QUà ARREGLA: en /pregunta-al-experto el recomendador está en `RUTAS_EXCLUIDAS`, y 3607 el bloque 3 entiende "excluida" como "ni siquiera arranques": sale de `init()` antes 3608 de montar nada, asà que allà no existe `window.reRecomendador` y no hay forma de 3609 ofrecerlo a mano. Lo queremos ofrecer justo después de enviar la pregunta, que es 3610 cuando quien pregunta tiene el problema fresco y toda la atención puesta. 3611 3612 QUà HACE: en esa ruta, y solo en esa, 3613 3614 1. saca `/pregunta-al-experto` de `RUTAS_EXCLUIDAS` para que el bloque 3 monte el 3615 pop-up y deje el gancho `window.reRecomendador.abrir()` disponible; 3616 2. le hace creer que ya salió en esta sesión, envolviendo `leer` para la clave de 3617 sesión. `puedeSalir()` la consulta antes de cada disparo por tiempo o scroll, 3618 asà que el pop-up no sale por su cuenta; `abrir()` no la mira, asà que el 3619 gancho manual sigue funcionando. 3620 3621 Quien lo abre es el pop-up de confirmación de esa landing: 3622 `widget-pregunta-al-experto-2-popup.html`. 3623 3624 POR QUà UN PARCHE Y NO TOCAR EL BLOQUE 3: aquel embed son 28 KB que habrÃa que 3625 reproducir enteros para cambiarle dos lÃneas. Es el mismo motivo por el que ya 3626 existen `-escaparate` y `-saltar-sabidos`. El dÃa que se reescriba el bloque 3, 3627 el cambio limpio es que `excluida()` apague solo los disparos automáticos y deje 3628 el resto del `init()` en pie; entonces este embed se borra. 3629 3630 DÃNDE VA: cuarto Embed del componente Footer V1, ENTRE el núcleo (bloque 2, del que 3631 lee `window.__reRecNucleo`) y el comportamiento (bloque 3, al que tiene que 3632 adelantarse). Junto a los otros dos parches, delante del bloque 3. 3633 ============================================================ --> 3634
3635<script> 3636(function () { 3637 var N = window.__reRecNucleo; 3638 if (!N) return; // sin el núcleo no hay nada que parchear; el bloque 3 ya avisa por consola 3639 3640 // Guardia de versión, como los otros parches: si el núcleo cambia de versión, este 3641 // bloque se calla en vez de operar sobre algo que ya no es lo que espera. 3642 if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") { 3643 console.warn("[re-recomendador] parche del gancho manual: version distinta, no se aplica"); 3644 return; 3645 } 3646 3647 var RUTA = "/pregunta-al-experto"; 3648 if (location.pathname.indexOf(RUTA) !== 0) return; 3649 3650 N.RUTAS_EXCLUIDAS = N.RUTAS_EXCLUIDAS.filter(function (r) { return r !== RUTA; }); 3651 3652 // El bloque 3 copia `N.leer` a una variable local en cuanto arranca, asà que esto 3653 // tiene que estar puesto antes que él. Solo miente sobre la clave de sesión: todo lo 3654 // demás (el apagado permanente, entre otros) sigue leyéndose de verdad. 3655 var leerOriginal = N.leer; 3656 N.leer = function (clave, almacen) { 3657 if (clave === N.CLAVE_SESION) return "1"; 3658 return leerOriginal(clave, almacen); 3659 }; 3660 3661 window.RE_RECOMENDADOR_GANCHO_MANUAL = true; 3662})(); 3663</script>
3663 3664</div><div class="w-embed w-script">
3664<script> 3665(function () { 3666 "use strict"; 3667 3668 // Segunda mitad del pop-up: cuando se abre, que pasa al pulsar y que se 3669 // mide. Depende del bloque anterior; si por lo que sea no ha cargado, aqui 3670 // no se hace nada en vez de reventar con un TypeError por cada pagina. 3671 var N = window.__reRecNucleo; 3672 if (!N) { 3673 console.error("[re-recomendador] falta el bloque 1, el pop-up no arranca"); 3674 return; 3675 } 3676 3677 // Se sacan a variables locales para que el resto del bloque se lea igual 3678 // que cuando todo esto era un unico fichero. 3679 var RUTAS_EXCLUIDAS = N.RUTAS_EXCLUIDAS; 3680 var SEGUNDOS_ESPERA = N.SEGUNDOS_ESPERA; 3681 var SCROLL_DISPARO = N.SCROLL_DISPARO; 3682 var CLAVE_APAGADO = N.CLAVE_APAGADO; 3683 var CLAVE_SESION = N.CLAVE_SESION; 3684 var MODALIDADES = N.MODALIDADES; 3685 var OBJETIVOS = N.OBJETIVOS; 3686 var CUANDOS = N.CUANDOS; 3687 var estado = N.estado; 3688 var pasos = N.pasos; 3689 var pasoActual = N.pasoActual; 3690 var pideCiudad = N.pideCiudad; 3691 var leer = N.leer; 3692 var escribir = N.escribir; 3693 var ciudadDeLaPagina = N.ciudadDeLaPagina; 3694 var objetivoDeLaPagina = N.objetivoDeLaPagina; 3695 var rest = N.rest; 3696 var cargarCiudades = N.cargarCiudades; 3697 var buscarTop3 = N.buscarTop3; 3698 var pasoDondeHTML = N.pasoDondeHTML; 3699 var pasoObjetivoHTML = N.pasoObjetivoHTML; 3700 var pasoCuandoHTML = N.pasoCuandoHTML; 3701 var pasoDatosHTML = N.pasoDatosHTML; 3702 var subtituloDatosHTML = N.subtituloDatosHTML; 3703 var confirmacionHTML = N.confirmacionHTML; 3704 var cierreHTML = N.cierreHTML; 3705 3706 var overlay = null; 3707 var caja = null; 3708 3709 // ------------------------------------------------ alto real de la pantalla 3710 3711 // Lo que el CSS no puede arreglar solo: ni `100dvh` ni un `position: fixed` 3712 // se enteran de que ha subido el teclado. El modal se sigue dibujando contra 3713 // el viewport de siempre y el teclado le tapa la parte de abajo, que en el 3714 // paso de datos es justo donde están el último campo y el botón de enviar. 3715 // La única medida que sà encoge con el teclado es `visualViewport`, asà que 3716 // mientras el pop-up está abierto el overlay se pega a ella. Hace falta 3717 // colocarlo además de medirlo (`top`/`left`) porque iOS no encoge el 3718 // viewport visible: lo desplaza. 3719 var vv = window.visualViewport || null; 3720 var ultimoEncaje = ""; 3721 3722 function ajustarAlViewport() { 3723 if (!overlay || !vv) return; 3724 var encaje = [vv.height, vv.width, vv.offsetTop, vv.offsetLeft].join("|"); 3725 if (encaje === ultimoEncaje) return; 3726 ultimoEncaje = encaje; 3727 // Con `top` + `height` y `left` + `width` puestos, el `inset: 0` del CSS 3728 // deja de mandar en el borde de abajo y en el de la derecha. 3729 overlay.style.top = vv.offsetTop + "px"; 3730 overlay.style.left = vv.offsetLeft + "px"; 3731 overlay.style.height = vv.height + "px"; 3732 overlay.style.width = vv.width + "px"; 3733 // Si estaba escribiendo, que su campo quede en el centro de lo que queda 3734 // visible. `center` y no `nearest`: `nearest` lo deja pegado al borde de 3735 // abajo, que es justo donde está la barra de botones pegada, y el campo 3736 // acaba detrás de ella. 3737 var foco = document.activeElement; 3738 if (foco && caja && foco !== caja && caja.contains(foco)) { 3739 try { foco.scrollIntoView({ block: "center" }); } catch (e) {} 3740 } 3741 } 3742 3743 function soltarViewport() { 3744 if (!overlay) return; 3745 ultimoEncaje = ""; 3746 overlay.style.top = ""; 3747 overlay.style.left = ""; 3748 overlay.style.height = ""; 3749 overlay.style.width = ""; 3750 } 3751 3752 // ----------------------------------------------------------------- render 3753 3754 var VISTAS = { 3755 donde: pasoDondeHTML, 3756 objetivo: pasoObjetivoHTML, 3757 cuando: pasoCuandoHTML, 3758 datos: pasoDatosHTML 3759 }; 3760 3761 var LISTAS = { 3762 donde: MODALIDADES, 3763 objetivo: OBJETIVOS, 3764 cuando: CUANDOS 3765 }; 3766 3767 function pintar() { 3768 var html; 3769 if (estado.fase === "confirmacion") html = confirmacionHTML(); 3770 else html = VISTAS[pasoActual()](); 3771 3772 caja.innerHTML = html; 3773 caja.classList.toggle("re-rec-ancho", estado.fase !== "preguntas"); 3774 caja.scrollTop = 0; 3775 3776 if (document.getElementById("re-rec-ciudad")) cargarCiudades(); 3777 if (estado.fase === "preguntas" && pasoActual() === "datos") precargarTop3(); 3778 // El foco automático solo en escritorio: en móvil abre el teclado y tapa 3779 // media pantalla justo cuando el usuario aún está leyendo la pregunta. 3780 var primerInput = caja.querySelector("input"); 3781 if (primerInput && window.innerWidth > 560) primerInput.focus(); 3782 3783 medirPantalla(); 3784 } 3785
3786 function claveBusqueda() { 3787 return [estado.ciudad, estado.objetivo, estado.modalidad].join("|"); 3788 } 3789 3790 // Se busca a los tres al llegar a la pantalla de datos, no al enviarla, para 3791 // poder nombrarlos antes de pedir el email. El resultado se guarda con la 3792 // clave de las respuestas que lo produjeron: si vuelve atrás y cambia una, 3793 // la caché deja de valer sola y `enviar` no manda la solicitud a los tres 3794 // entrenadores de una búsqueda que ya no es la suya. 3795 function precargarTop3() { 3796 var clave = claveBusqueda(); 3797 if (estado.top3Clave === clave) return; 3798 estado.top3Clave = clave; 3799 estado.top3 = []; 3800 3801 buscarTop3().then(function (top3) { 3802 if (estado.top3Clave !== clave) return; // llegó tarde: ya cambió algo 3803 estado.top3 = top3; 3804 // Se reescribe solo el subtÃtulo en vez de repintar la pantalla: un 3805 // `pintar()` aquà borrarÃa lo que el usuario ya estuviera escribiendo 3806 // en el nombre o el email. 3807 var sub = caja.querySelector(".re-rec-sub"); 3808 if (sub && estado.fase === "preguntas" && pasoActual() === "datos") { 3809 sub.innerHTML = subtituloDatosHTML(); 3810 } 3811 }).catch(function (err) { 3812 console.warn("[re-recomendador] no se pudo precargar el reparto", err); 3813 estado.top3Clave = null; // que `enviar` lo reintente 3814 }); 3815 } 3816 3817 function avanzar() { 3818 estado.error = ""; 3819 estado.indice += 1; 3820 pintar(); 3821 } 3822 3823 function elegirOpcion(valor) { 3824 var paso = pasoActual(); 3825 var lista = LISTAS[paso]; 3826 if (!lista) return; 3827 var op = lista.find(function (o) { return o.value === valor; }); 3828 if (!op) return; 3829 guardarCampos(); 3830 3831 if (paso === "donde") { 3832 estado.modalidad = op.value; 3833 medir("recomendador_respuesta", { paso: "modalidad", valor: op.value }); 3834 // Elegir modalidad avanza solo, salvo cuando eso destapa el campo de 3835 // ciudad: ahà hay que quedarse en la pantalla para que lo rellene. 3836 if (pideCiudad()) { estado.error = ""; return pintar(); } 3837 } else if (paso === "objetivo") { 3838 estado.objetivo = op.value; 3839 estado.objetivoRespondido = true; 3840 // El objetivo vacÃo es la opción "Aún no lo tengo claro", no un fallo: 3841 // se le pone nombre para poder contarla como una respuesta más. 3842 medir("recomendador_respuesta", { paso: "objetivo", valor: op.value || "Aun no lo tengo claro" }); 3843 } else if (paso === "cuando") { 3844 estado.cuando = op.value; 3845 medir("recomendador_respuesta", { paso: "cuando", valor: op.value }); 3846 } 3847 3848 avanzar(); 3849 } 3850 3851 function validarYAvanzar() { 3852 var input = document.getElementById("re-rec-ciudad"); 3853 if (!input) return; 3854 var ciudad = input.value.trim(); 3855 if (!ciudad) return mostrarError("Dinos en qué ciudad buscas."); 3856 estado.ciudad = ciudad; 3857 // La ciudad va tal cual la escribe: saber que piden Albacete y que ahà no 3858 // hay nadie vale más que el propio embudo. 3859 medir("recomendador_respuesta", { paso: "ciudad", valor: ciudad }); 3860 avanzar(); 3861 } 3862 3863 function mostrarError(texto) { 3864 estado.error = texto; 3865 var el = caja.querySelector(".re-rec-error"); 3866 if (el) { el.textContent = texto; el.style.display = "block"; } 3867 } 3868 3869 // Cualquier repintado tira el contenido de los campos, asà que antes de 3870 // movernos se guarda lo que haya escrito. Sin esto, quien vuelve atrás a 3871 // cambiar el objetivo se encuentra el nombre y el email en blanco al 3872 // regresar, y eso es un abandono servido en bandeja. 3873 function guardarCampos() { 3874 var campos = { nombre: "re-rec-nombre", email: "re-rec-email", telefono: "re-rec-telefono", ciudad: "re-rec-ciudad" }; 3875 Object.keys(campos).forEach(function (clave) { 3876 var el = document.getElementById(campos[clave]); 3877 if (el) estado[clave] = el.value.trim(); 3878 }); 3879 } 3880 3881 function retroceder() { 3882 guardarCampos(); 3883 medir("recomendador_atras", { desde: pasoActual() }); 3884 estado.error = ""; 3885 if (estado.indice > 0) estado.indice -= 1; 3886 pintar(); 3887 } 3888 3889 // ----------------------------------------------------------------- envÃo 3890 3891 // re-validar: copia inline de `_shared/validacion-contacto.ts`. Los embeds de 3892 // Webflow son sueltos y no pueden importar nada, asà que cuando se toque 3893 // aquel fichero hay que tocar también esto. Deja el número en E.164 o 3894 // devuelve null: 9 dÃgitos se asumen españoles, 00 pasa a +, y lo que no 3895 // encaje se rechaza en vez de guardarse en crudo. 3896 function reNormalizarTelefono(valor) { 3897 if (typeof valor !== "string") return null; 3898 var limpio = valor.replace(/[\s\-.()]/g, ""); 3899 if (!limpio) return null; 3900 var candidato; 3901 if (limpio.charAt(0) === "+") candidato = limpio; 3902 else if (limpio.slice(0, 2) === "00") candidato = "+" + limpio.slice(2); 3903 else if (/^\d{9}$/.test(limpio)) candidato = "+34" + limpio; 3904 else return null; 3905 return /^\+\d{6,15}$/.test(candidato) ? candidato : null; 3906 } 3907 3908 function enviar() { 3909 var nombre = (document.getElementById("re-rec-nombre").value || "").trim(); 3910 var email = (document.getElementById("re-rec-email").value || "").trim(); 3911 var telefono = (document.getElementById("re-rec-telefono").value || "").trim(); 3912
3913 estado.nombre = nombre; 3914 estado.email = email; 3915 estado.telefono = telefono; 3916 3917 if (!nombre) { 3918 medir("recomendador_datos_invalidos", { campo: "nombre" }); 3919 return mostrarError("Introduce tu nombre."); 3920 } 3921 // El email es el campo obligatorio desde que existe el chat: es lo que 3922 // permite avisar de un mensaje y lo que abre la cuenta. 3923 if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { 3924 medir("recomendador_datos_invalidos", { campo: "email" }); 3925 return mostrarError(email 3926 ? "Revisa el email, parece que falta algo." 3927 : "Introduce tu email para que puedan escribirte."); 3928 } 3929 // El teléfono es opcional, pero si lo escribe tiene que servir para llamar. 3930 // Se manda ya normalizado para que el backend guarde la misma forma. 3931 var telefonoE164 = telefono ? reNormalizarTelefono(telefono) : null; 3932 if (telefono && !telefonoE164) { 3933 medir("recomendador_datos_invalidos", { campo: "telefono" }); 3934 return mostrarError("Revisa el teléfono: 9 dÃgitos, o con prefijo internacional (+34â¦)."); 3935 } 3936 3937 estado.enviando = true; 3938 estado.error = ""; 3939 pintar(); 3940 3941 // Si la precarga de esta misma búsqueda ya trajo el reparto, se manda a 3942 // esos: son exactamente los que se le acaban de nombrar en pantalla. 3943 var yaEstan = estado.top3.length && estado.top3Clave === claveBusqueda(); 3944 (yaEstan ? Promise.resolve(estado.top3) : buscarTop3()).then(function (top3) { 3945 if (!top3.length) throw new Error("sin_entrenadores"); 3946 estado.top3 = top3; 3947 3948 return rest("/functions/v1/solicitar-contacto-entrenador", { 3949 method: "POST", 3950 body: { 3951 entrenador_slugs: top3.map(function (e) { return e.slug; }), 3952 nombre: nombre, 3953 email: email, 3954 telefono: telefonoE164, 3955 zona: estado.ciudad || null, 3956 objetivo: estado.objetivo || null, 3957 modalidad: estado.modalidad || null, 3958 cuando_empieza: estado.cuando || null, 3959 tipo_solicitud: "multiple", 3960 origen: "popup-recomendador" 3961 } 3962 }); 3963 }).then(function (datos) { 3964 escribir(CLAVE_APAGADO, "enviado", localStorage); 3965 estado.claimToken = (datos && datos.claim_token) || null; 3966 estado.enviando = false; 3967 estado.fase = "confirmacion"; 3968 pintar(); 3969 medir("recomendador_solicitud_enviada", { 3970 entrenadores: estado.top3.length, 3971 ciudad: estado.ciudad || null, 3972 objetivo: estado.objetivo || "Aun no lo tengo claro", 3973 modalidad: estado.modalidad || null, 3974 cuando: estado.cuando || null, 3975 // El email es obligatorio, asà que no se mide: serÃa siempre true. El 3976 // teléfono sÃ, que ahora es el opcional y su tasa es justo lo que dice 3977 // si el cambio ha costado el dato con el que el entrenador llama. 3978 con_telefono: !!telefonoE164, 3979 segundos: estado.abiertoEn ? Math.round((Date.now() - estado.abiertoEn) / 1000) : null 3980 }); 3981 }).catch(function (err) { 3982 console.error("[re-recomendador] envÃo", err); 3983 // "sin_entrenadores" no es una averÃa: es demanda en una ciudad sin 3984 // cobertura. Separarlo del fallo de red es la diferencia entre arreglar 3985 // el widget y salir a captar entrenadores en esa ciudad. 3986 medir("recomendador_envio_fallido", { 3987 motivo: err.message === "sin_entrenadores" ? "sin_entrenadores" : "error", 3988 ciudad: estado.ciudad || null 3989 }); 3990 estado.enviando = false; 3991 estado.error = err.message === "sin_entrenadores" 3992 ? "No hemos encontrado entrenadores para esa búsqueda. Prueba con otra ciudad." 3993 : "No hemos podido enviar tu solicitud. Inténtalo de nuevo en un momento."; 3994 pintar(); 3995 }); 3996 } 3997 3998 function guardarPerfil() { 3999 var btn = caja.querySelector('[data-accion="guardar-perfil"]'); 4000 var errEl = document.getElementById("re-rec-p2-error"); 4001 var numero = function (id) { 4002 var v = (document.getElementById(id).value || "").trim(); 4003 return v === "" ? null : Number(v); 4004 }; 4005 4006 btn.disabled = true; 4007 btn.textContent = "Guardandoâ¦"; 4008 if (errEl) errEl.style.display = "none"; 4009 4010 rest("/functions/v1/completar-perfil-usuario", { 4011 method: "POST", 4012 body: { 4013 claim_token: estado.claimToken, 4014 // El email ya viajó con la solicitud en el paso 1. Mandarlo otra vez 4015 // no aportarÃa nada: el RPC respeta el que la ficha ya tiene. 4016 email: null, 4017 objetivo: estado.objetivo || null, 4018 modalidad: estado.modalidad || null, 4019 zona: estado.ciudad || null, 4020 edad: numero("re-rec-edad"), 4021 peso_kg: numero("re-rec-peso"), 4022 altura_cm: numero("re-rec-altura"), 4023 disponibilidad: (document.getElementById("re-rec-disponibilidad").value || "").trim() || null, 4024 nivel_actividad: document.getElementById("re-rec-actividad").value || null, 4025 comentarios: (document.getElementById("re-rec-notas").value || "").trim() || null 4026 } 4027 }).then(function (datos) { 4028 // La cuenta se ofrece porque la ficha tiene email: lo dio en el paso 1 y 4029 // la solicitud se creó con él. Ya no se mira `email_guardado`, que desde 4030 // este cambio viene siempre a false âsignifica "no he escrito nada nuevo 4031 // porque la ficha ya tenÃa email", que es exactamente lo que pasa ahoraâ
4032 // y usarlo escondÃa la oferta de cuenta a todo el mundo. 4033 var ofreceCuenta = !!estado.email; 4034 medir("recomendador_perfil_guardado", {}); 4035 caja.innerHTML = cierreHTML(true, ofreceCuenta); 4036 }).catch(function (err) { 4037 console.error("[re-recomendador] paso 2", err); 4038 if (errEl) { 4039 errEl.textContent = "No hemos podido guardarlo. Tu solicitud sà se envió, no te preocupes."; 4040 errEl.style.display = "block"; 4041 } 4042 btn.disabled = false; 4043 btn.textContent = "Enviar mis datos"; 4044 }); 4045 } 4046 4047 function crearCuenta() { 4048 var btn = caja.querySelector('[data-accion="crear-cuenta"]'); 4049 var errEl = document.getElementById("re-rec-cuenta-error"); 4050 btn.disabled = true; 4051 btn.textContent = "Creandoâ¦"; 4052 if (errEl) errEl.style.display = "none"; 4053 4054 rest("/functions/v1/crear-cuenta-usuario", { 4055 method: "POST", 4056 body: { claim_token: estado.claimToken } 4057 }).then(function (datos) { 4058 medir("recomendador_cuenta_creada", { requiere_email: !!datos.requiere_email }); 4059 // Cuenta nueva: se entra en el acto. Si ya existÃa, la función manda el 4060 // enlace al buzón, que ahà es lo único que prueba que el email es suyo. 4061 if (datos.requiere_email) { 4062 btn.parentElement.innerHTML = 4063 "<h4>Ya tienes cuenta</h4>" + 4064 '<p class="re-rec-sub" style="font-size:14px;margin:4px 0 0">Te hemos mandado un enlace a tu email para entrar y ver esta solicitud junto a las anteriores.</p>'; 4065 } else { 4066 btn.textContent = "Entrandoâ¦"; 4067 window.location.href = datos.url; 4068 } 4069 }).catch(function (err) { 4070 console.error("[re-recomendador] crear cuenta", err); 4071 if (errEl) { 4072 errEl.textContent = "No hemos podido crear la cuenta. Tu solicitud sà se envió; inténtalo de nuevo en un momento."; 4073 errEl.style.display = "block"; 4074 } 4075 btn.disabled = false; 4076 btn.textContent = "Crear mi cuenta"; 4077 }); 4078 } 4079 4080 // Amplitude no envÃa nada hasta que cookieyes acepte analytics; hasta 4081 // entonces los eventos se acumulan en su cola. Nunca se cuelga una conversión de 4082 // Ads de este pop-up: el activador de GTM de las landings de paid va por el 4083 // selector `.re-modal-success`, y por eso aquà no se usa esa clase. 4084 // 4085 // No hace falta encolar nada aquÃ: el bloque de Amplitude del head del sitio 4086 // deja un stub `{ _q: [], track }` desde el primer momento y vacÃa esa cola 4087 // cuando el módulo arranca, asà que un `track` disparado antes del 4088 // consentimiento no se pierde. Si el consentimiento no llega nunca, la cola 4089 // se descarta sin enviarse, que es justo lo que debe pasar. 4090 function medir(evento, props) { 4091 if (!estado.midiendo) return; 4092 var datos = props || {}; 4093 // Segmentar por página es lo que distingue "el pop-up funciona" de "el 4094 // pop-up funciona en las páginas de ciudad y en la home no". 4095 datos.pagina = location.pathname; 4096 try { 4097 if (window.amplitude && window.amplitude.track) window.amplitude.track(evento, datos); 4098 } catch (e) {} 4099 } 4100 4101 // Una misma pantalla se repinta varias veces âal destapar "Ver más", al 4102 // abrirse el campo de ciudad, al enseñar un errorâ y ninguno de esos 4103 // repintados es una pantalla nueva. Contando solo la primera, el embudo mide 4104 // personas y no renders. 4105 function medirPantalla() { 4106 var lista = pasos(); 4107 var clave = estado.fase === "confirmacion" ? "confirmacion" : pasoActual(); 4108 if (estado.vistas[clave]) return; 4109 estado.vistas[clave] = true; 4110 medir("recomendador_paso_visto", { 4111 paso: clave, 4112 numero: estado.fase === "confirmacion" ? lista.length + 1 : estado.indice + 1, 4113 total: lista.length 4114 }); 4115 } 4116 4117 // ------------------------------------------------------------ abrir/cerrar 4118 4119 function abrir(motivo) { 4120 if (!overlay || overlay.classList.contains("re-rec-abierto")) return; 4121 escribir(CLAVE_SESION, "1", sessionStorage); 4122 estado.indice = 0; 4123 estado.fase = "preguntas"; 4124 estado.vistas = {}; 4125 estado.abiertoEn = Date.now(); 4126 estado.midiendo = motivo !== "preview" && motivo !== "debug"; 4127 // Va antes de `pintar()` para que la apertura quede por delante del primer 4128 // paso en el orden de eventos, y no detrás. 4129 medir("recomendador_popup_abierto", { 4130 motivo: motivo || "manual", 4131 ciudad_de_pagina: estado.ciudadDePagina, 4132 objetivo_de_pagina: estado.objetivoDePagina, 4133 pasos: pasos().length 4134 }); 4135 pintar(); 4136 overlay.classList.add("re-rec-abierto"); 4137 document.body.style.overflow = "hidden"; 4138 ajustarAlViewport(); 4139 if (vv) { 4140 vv.addEventListener("resize", ajustarAlViewport); 4141 vv.addEventListener("scroll", ajustarAlViewport); 4142 } 4143 } 4144 4145 function cerrar() { 4146 if (!overlay) return; 4147 var estabaAbierto = overlay.classList.contains("re-rec-abierto"); 4148 overlay.classList.remove("re-rec-abierto"); 4149 document.body.style.overflow = ""; 4150 if (vv) { 4151 vv.removeEventListener("resize", ajustarAlViewport); 4152 vv.removeEventListener("scroll", ajustarAlViewport); 4153 } 4154 soltarViewport(); 4155 // El evento más útil de todos: dice en qué pantalla exacta se cae la gente. 4156 if (estabaAbierto) { 4157 medir("recomendador_cerrado", { 4158 paso: estado.fase === "preguntas" ? pasoActual() : estado.fase,
4159 habia_enviado: estado.fase !== "preguntas", 4160 segundos: estado.abiertoEn ? Math.round((Date.now() - estado.abiertoEn) / 1000) : null 4161 }); 4162 } 4163 // Solo se apaga para siempre si ya habÃa enviado; cerrar en la primera 4164 // pregunta no deberÃa castigarle a no volver a verlo nunca en el sitio. 4165 if (estado.fase !== "preguntas") escribir(CLAVE_APAGADO, "enviado", localStorage); 4166 } 4167 4168 // Con otro modal abierto no se dispara: en la ficha del entrenador convive 4169 // con el pop-up de contacto ("Nuevo contacto v2"), y dos modales apilados 4170 // es la forma más rápida de perder el lead que ya estaba escribiendo. 4171 function hayOtroModalAbierto() { 4172 // El banner de cookies entra en la lista: a un visitante nuevo le sale 4173 // CookieYes nada más entrar, y taparlo con este pop-up es a la vez una 4174 // mala experiencia y un problema de consentimiento. 4175 var candidatos = [ 4176 ".pop-up-contacto", 4177 "#re-overlay.re-open-modal-overlay", 4178 ".re-modal-overlay", 4179 "[data-re-modal-abierto]", 4180 ".cky-consent-container", 4181 ".cky-modal" 4182 ]; 4183 return candidatos.some(function (sel) { 4184 var el = document.querySelector(sel); 4185 if (!el) return false; 4186 // No basta con mirar `display`: los pop-ups de Webflow los abre y cierra 4187 // IX2, que muchas veces deja el elemento en el flujo y solo le anima la 4188 // opacidad. Dando por abierto todo lo que no sea `display:none`, este 4189 // pop-up no volverÃa a salir nunca en las 95 fichas de entrenador â y 4190 // sin un error en consola que lo delate. Se mide lo que de verdad se ve. 4191 var estilo = window.getComputedStyle(el); 4192 if (estilo.display === "none" || estilo.visibility === "hidden") return false; 4193 if (Number(estilo.opacity) === 0) return false; 4194 var caja = el.getBoundingClientRect(); 4195 return caja.width > 0 && caja.height > 0; 4196 }); 4197 } 4198 4199 function puedeSalir() { 4200 if (leer(CLAVE_APAGADO, localStorage)) return false; 4201 if (leer(CLAVE_SESION, sessionStorage)) return false; 4202 if (hayOtroModalAbierto()) return false; 4203 // Si está escribiendo en cualquier formulario de la página, no se le 4204 // interrumpe: quien ya está rellenando algo no necesita que le recomienden nada. 4205 var activo = document.activeElement; 4206 if (activo && /^(INPUT|TEXTAREA|SELECT)$/.test(activo.tagName)) return false; 4207 return true; 4208 } 4209 4210 // ------------------------------------------------------------------ init 4211 4212 function excluida() { 4213 var ruta = location.pathname; 4214 return RUTAS_EXCLUIDAS.some(function (r) { return ruta.indexOf(r) === 0; }); 4215 } 4216 4217 function init() { 4218 if (excluida()) return; 4219 4220 overlay = document.createElement("div"); 4221 overlay.id = "re-rec-overlay"; 4222 overlay.innerHTML = '<div class="re-rec-modal" role="dialog" aria-modal="true" aria-label="Te ayudamos a decidir"></div>'; 4223 document.body.appendChild(overlay); 4224 caja = overlay.querySelector(".re-rec-modal"); 4225 4226 estado.ciudad = ciudadDeLaPagina(); 4227 estado.ciudadDePagina = !!estado.ciudad; 4228 var obj = objetivoDeLaPagina(); 4229 if (obj) { estado.objetivo = obj; estado.objetivoDePagina = true; } 4230 4231 overlay.addEventListener("click", function (ev) { 4232 if (ev.target === overlay) return cerrar(); 4233 var btn = ev.target.closest("[data-accion]"); 4234 if (!btn) return; 4235 var accion = btn.getAttribute("data-accion"); 4236 if (accion === "cerrar") cerrar(); 4237 else if (accion === "opcion") elegirOpcion(btn.getAttribute("data-valor")); 4238 else if (accion === "ver-mas") { 4239 // Si mucha gente lo pulsa, las opciones visibles que fija `VISIBLES` no 4240 // son las que cubren a la gente que llega de verdad. 4241 medir("recomendador_ver_mas", { paso: pasoActual() }); 4242 estado.abiertas[pasoActual()] = true; 4243 pintar(); 4244 } 4245 else if (accion === "siguiente") validarYAvanzar(); 4246 else if (accion === "atras") retroceder(); 4247 else if (accion === "enviar") enviar(); 4248 else if (accion === "guardar-perfil") guardarPerfil(); 4249 else if (accion === "crear-cuenta") crearCuenta(); 4250 }); 4251 4252 document.addEventListener("keydown", function (ev) { 4253 if (ev.key === "Escape" && overlay.classList.contains("re-rec-abierto")) cerrar
4253(); 4254 }); 4255 4256 // Disparo por tiempo o por scroll, lo que llegue antes. 4257 var disparado = false; 4258 var entrada = Date.now(); 4259 4260 function disparar(motivo) { 4261 if (disparado) return false; 4262 if (!puedeSalir()) return false; 4263 disparado = true; 4264 abrir(motivo); 4265 return true; 4266 } 4267 4268 // 30/08/2026: sale nada más entrar, no a los 25 segundos. Lo único que se 4269 // espera es a que CookieYes pinte y despache su banner â`puedeSalir` ya lo 4270 // cuenta como modal abierto, pero tarda unas décimas en aparecer y sin esa 4271 // espera mÃnima el pop-up se le adelantaba y se quedaba debajoâ, asà que la 4272 // espera baja de 25 segundos a segundo y medio. 4273 // 4274 // El disparo se reintenta en vez de perderse: en el primer intento puede 4275 // haber un motivo temporal para no salir (el banner de cookies sin 4276 // responder, otro modal abierto, el usuario escribiendo en un campo) y con 4277 // un `setTimeout` suelto esa única oportunidad se gastaba ahÃ. El sondeo 4278 // baja a 400 ms âantes 5 sâ para que en cuanto se aceptan las cookies el 4279 // pop-up salga acto seguido y no hasta cinco segundos después. 4280 // 4281 // `SEGUNDOS_ESPERA` del bloque 2 se queda sin usar a propósito: reescribir 4282 // aquel embed de 34 KB solo para bajar una constante no compensa. 4283 var ESPERA_MS = 1500; 4284 var reintento = setInterval(function () { 4285 var transcurrido = Date.now() - entrada; 4286 if (disparado || transcurrido > 180000) return clearInterval(reintento); 4287 if (transcurrido >= ESPERA_MS) disparar("tiempo"); 4288 }, 400); 4289 4290 window.addEventListener("scroll", function () { 4291 var alto = document.documentElement.scrollHeight - window.innerHeight; 4292 if (alto <= 0) return; 4293 if (window.scrollY / alto >= SCROLL_DISPARO) disparar("scroll"); 4294 }, { passive: true }); 4295 4296 // Gancho manual: para un botón "¿No lo tienes claro?" en cualquier sitio 4297 // de la página, y para poder probarlo sin esperar al disparo automático. 4298 window.reRecomendador = { 4299 abrir: function () { abrir("manual"); }, 4300 cerrar: cerrar, 4301 reset: function () { 4302 try { localStorage.removeItem(CLAVE_APAGADO); sessionStorage.removeItem(CLAVE_SESION); } catch (e) {} 4303 }, 4304 // Pinta la confirmación con el reparto real de una búsqueda, pero SIN 4305 // enviar nada: sirve para revisar el copy y las fichas sin avisar a tres 4306 // entrenadores de verdad cada vez que se quiere mirar esa pantalla. 4307 // `tokenFalso` sirve para ver el bloque de perfil y el de crear cuenta, 4308 // que solo se pintan cuando hay `claim_token`. Guardar desde ahà falla 4309 // (el token no existe), que es justo lo que debe pasar. 4310 previsualizarConfirmacion: function (ciudad, objetivo, modalidad, tokenFalso) { 4311 estado.ciudad = ciudad || estado.ciudad; 4312 estado.objetivo = objetivo || ""; 4313 estado.modalidad = modalidad || "En un gimnasio"; 4314 return buscarTop3().then(function (top3) { 4315 // abrir() reinicia el estado al primer paso, asà que va antes de 4316 // dejar la fase en "confirmacion" y no después. 4317 abrir("preview"); 4318 estado.top3 = top3; 4319 estado.claimToken = tokenFalso ? "token-de-prueba" : null; 4320 estado.fase = "confirmacion"; 4321 pintar(); 4322 return top3.map(function (e) { return e.slug; }); 4323 }); 4324 } 4325 }; 4326 4327 if (new URLSearchParams(location.search).get("recomendador") === "1") abrir("debug"); 4328 } 4329 4330 // Si en consola hay VERSION pero no LISTO, es que falta el segundo embed. 4331 window.RE_RECOMENDADOR_LISTO = true; 4332 4333 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); 4334 else init(); 4335})(); 4336</script>
4336 4337</div><div class="w-embed w-script"><!-- RedEntrenadores - Origen de campana (embed en el componente Footer V1) --> 4338<!-- Va en el Footer V1 porque tiene que correr en las 115 paginas: el custom 4339 code de sitio haria lo mismo, pero ese campo son 20 KB y reescribirlo 4340 entero por API es justo el tamano que se corta a medias. 4341 4342 El problema que resuelve: cada formulario leia `document.referrer` en el 4343 momento de enviarse, y para entonces ya era interno. Quien llegaba por un 4344 enlace de campana, aterrizaba en una landing y contactaba desde la ficha 4345 de un entrenador nacia en `usuarios` sin origen ninguno. Y las utm de la 4346 URL de llegada se perdian en el primer salto. 4347 4348 Hace tres cosas: 4349 1. Guarda la primera llegada de la sesion y la sirve en `window.rtOrigen()`. 4350 2. Envuelve `fetch` para que toda solicitud a `solicitar-contacto-entrenador` 4351 lleve el origen. Asi el formulario de la ficha (22 KB) y el de la landing 4352 de paid quedan cubiertos sin tocarles una linea. 4353 3. Arrastra las utm a los enlaces que apuntan a /encuentra-entrenador, que 4354 es el unico sitio del que el wizard las lee. 4355 4356 Por que (3) en vez de envolver tambien la RPC del wizard: supabase-js 4357 resuelve `fetch` al construir el cliente, y ese cliente nace al parsearse 4358 su embed, antes que este. La envoltura no le llegaria. 4359 4360 Fuente versionada: widgets/widget-origen-campana.html -->
4361<script> 4362(function () { 4363 var CLAVE = 're-origen'; 4364 var ENDPOINT_CONTACTO = '/functions/v1/solicitar-contacto-entrenador'; 4365 var DESTINO_WIZARD = '/encuentra-entrenador'; 4366 4367 // Copia en memoria: en navegacion privada o con el almacenamiento capado, 4368 // sessionStorage lanza al escribir. Sin esto el origen se perderia en el 4369 // primer salto interno justo en los navegadores mas restrictivos. 4370 var enMemoria = null; 4371 4372 function leer() { 4373 try { 4374 var crudo = sessionStorage.getItem(CLAVE); 4375 if (crudo) return JSON.parse(crudo); 4376 } catch (e) { /* almacenamiento capado: tiramos de la copia en memoria */ } 4377 return enMemoria; 4378 } 4379 4380 function guardar(o) { 4381 enMemoria = o; 4382 try { sessionStorage.setItem(CLAVE, JSON.stringify(o)); } catch (e) {} 4383 } 4384 4385 function referenteExterno() { 4386 try { 4387 if (!document.referrer) return null; 4388 var r = new URL(document.referrer); 4389 if (r.hostname === location.hostname) return null; 4390 // Sin query: el referente de un buscador lleva la consulta del usuario. 4391 return r.origin + r.pathname; 4392 } catch (e) { return null; } 4393 } 4394 4395 var params = new URLSearchParams(location.search); 4396 var llegada = { 4397 pagina: location.pathname, 4398 referrer: referenteExterno(), 4399 utm_source: params.get('utm_source'), 4400 utm_medium: params.get('utm_medium'), 4401 utm_campaign: params.get('utm_campaign') 4402 }; 4403 4404 // Manda el primer toque de la sesion, con una excepcion: una llegada nueva 4405 // CON utm_source la pisa. Asi quien vuelve desde un enlace de campana se 4406 // atribuye a esa campana y no al referente con el que abrio el navegador 4407 // hace media hora. 4408 if (!leer() || llegada.utm_source) guardar(llegada); 4409 4410 // Devuelve siempre las cinco claves: quien lo consume puede volcarlo en el 4411 // payload sin comprobar nada. 4412 window.rtOrigen = function () { 4413 var o = leer() || {}; 4414 return { 4415 pagina: o.pagina || location.pathname, 4416 referrer: o.referrer || null, 4417 utm_source: o.utm_source || null, 4418 utm_medium: o.utm_medium || null, 4419 utm_campaign: o.utm_campaign || null 4420 }; 4421 }; 4422 4423 // 2. El origen viaja con cada solicitud de contacto 4424 // 4425 // Envolver `fetch` en vez de editar los formularios: son embeds de 22 y 40 4426 // KB y reescribirlos por API no cabe. La envoltura es defensiva de arriba 4427 // abajo: cualquier fallo cae al fetch original con los argumentos tal cual, 4428 // porque aqui pasa el envio del formulario de contacto y romperlo significa 4429 // perder leads. 4430 var fetchOriginal = window.fetch; 4431 4432 if (typeof fetchOriginal === 'function') { 4433 window.fetch = function (entrada, opciones) { 4434 try { 4435 var url = typeof entrada === 'string' ? entrada : (entrada && entrada.url) || ''; 4436 if (url.indexOf(ENDPOINT_CONTACTO) !== -1 && opciones && typeof opciones.body === 'string') { 4437 var cuerpo = JSON.parse(opciones.body); 4438 if (cuerpo && typeof cuerpo === 'object' && !Array.isArray(cuerpo)) { 4439 var o = window.rtOrigen(); 4440 // Solo se rellena lo que falta: un formulario que ya mande su 4441 // origen manda el. 4442 if (!cuerpo.pagina_origen) cuerpo.pagina_origen = location.pathname; 4443 if (!cuerpo.referrer_externo && o.referrer) cuerpo.referrer_externo = o.referrer; 4444 if (!cuerpo.utm_source && o.utm_source) cuerpo.utm_source = o.utm_source; 4445 if (!cuerpo.utm_medium && o.utm_medium) cuerpo.utm_medium = o.utm_medium; 4446 if (!cuerpo.utm_campaign && o.utm_campaign) cuerpo.utm_campaign = o.utm_campaign; 4447 4448 var copia = {}; 4449 for (var k in opciones) { if (Object.prototype.hasOwnProperty.call(opciones, k)) copia[k] = opciones[k]; } 4450 copia.body = JSON.stringify(cuerpo); 4451 return fetchOriginal.call(this, entrada, copia); 4452 } 4453 } 4454 } catch (e) { /* cuerpo ilegible o lo que sea: va el original */ } 4455 return fetchOriginal.apply(this, arguments); 4456 }; 4457 } 4458 4459 // 3. Las utm llegan al wizard 4460 // 4461 // El wizard de /encuentra-entrenador lee las utm de SU url. Quien llega 4462 // desde una landing interna las habria perdido por el camino, asi que se 4463 // las pegamos a los enlaces que apuntan alli. Solo a esos: llenar de utm 4464 // todos los enlaces del sitio ensuciaria las URLs indexables y contaria 4465 // cada pagina interna como una visita de campana. 4466 function propagarAlWizard() { 4467 var o = window.rtOrigen(); 4468 if (!o.utm_source && !o.utm_medium && !o.utm_campaign) return; 4469 4470 var enlaces = document.querySelectorAll('a[href*="' + DESTINO_WIZARD + '"]'); 4471 Array.prototype.forEach.call(enlaces, function (a) {
4472 try { 4473 if (a.getAttribute('data-re-origen')) return; 4474 var u = new URL(a.href, location.origin); 4475 if (u.hostname !== location.hostname) return; 4476 if (u.pathname.indexOf(DESTINO_WIZARD) !== 0) return; 4477 if (o.utm_source && !u.searchParams.get('utm_source')) u.searchParams.set('utm_source', o.utm_source); 4478 if (o.utm_medium && !u.searchParams.get('utm_medium')) u.searchParams.set('utm_medium', o.utm_medium); 4479 if (o.utm_campaign && !u.searchParams.get('utm_campaign')) u.searchParams.set('utm_campaign', o.utm_campaign); 4480 a.setAttribute('href', u.pathname + u.search + u.hash); 4481 a.setAttribute('data-re-origen', '1'); 4482 } catch (e) {} 4483 }); 4484 } 4485 4486 if (document.readyState === 'loading') { 4487 document.addEventListener('DOMContentLoaded', propagarAlWizard); 4488 } else { 4489 propagarAlWizard(); 4490 } 4491 // Los listados y el pop-up recomendador pintan enlaces despues de cargar. 4492 setTimeout(propagarAlWizard, 1500); 4493})(); 4494</script>
4494</div></footer>
4494<script src="https://d3e54v103j8qbb.cloudfront.net/js/jquery-3.5.1.min.dc5e7f18c8.js?site=68c5a729c71fa3f37858fbbe" type="text/javascript" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
4494<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/js/webflow.a6c64a8e.02301a381a1d8876.js" type="text/javascript" integrity="sha384-RxpPBra0owSEHk9KBH3zzcHMN4Aq9zqJytev0dKEK6IjZzn6T6HD8dGHfBURFvZ1" crossorigin="anonymous"></script>
4494<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 4494-4495
4494 4495<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
4495GTM-KBD83QKS
vendor: 83 bytes, lines 4495-4496
4495" 4496height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
4497<!-- End Google Tag Manager (noscript) --> 4498 4499<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4500 PREGUNTA AL EXPERTO â filtro por categoria + contador 4501 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 4502<style> 4503 .re-cats__link.is-active { background:#089F3B !important; border-color:#089F3B !important; color:#fff !important; } 4504 .re-cats__link.is-active [class*="re-cats__icon-"] { filter: brightness(0) invert(1); } 4505 .re-cats__check { position:absolute; top:6px; right:8px; width:18px; height:18px; border-radius:50%; background:#fff; color:#089F3B; font-size:12px; font-weight:800; line-height:1; display:flex; align-items:center; justify-content:center; } 4506 .re-cats__reset { display:block; width:fit-content; margin:16px auto 0; padding:10px 20px; border:1.5px solid #1B1E22; border-radius:999px; font-size:13px; font-weight:700; color:#1B1E22; text-decoration:none; cursor:pointer; } 4507 .re-cats__reset:hover { background:#1B1E22; color:#fff; } 4508 .re-oq-title.is-filtered { font-size:20px !important; text-transform:none !important; line-height:1.25 !important; text-align:center; } 4509 .otras_preguntas-header:has(.re-oq-title.is-filtered) { display:block !important; } 4510 #re-oq-clear { display:block !important; width:100% !important; text-align:center !important; margin-top:8px !important; float:none !important; } 4511 /* Boton de continuidad al final del bloque */ 4512 .re-oq-explorar { display:block; width:fit-content; margin:32px auto 0; padding:14px 28px; border-radius:8px; background:#089F3B; color:#fff !important; font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:.02em; text-decoration:none; text-align:center; } 4513 .re-oq-explorar:hover { background:#067A2D; } 4514</style>
4515<script> 4516(function () { 4517 if (window.location.pathname.indexOf('/pregunta-al-experto') !== 0) return; 4518 4519 var CAT = new URLSearchParams(window.location.search).get('categoria'); 4520 var contadoresPintados = false; 4521 var yaColocado = false; 4522 4523 function textoVer(n) { 4524 if (!n) return 'Leer respuesta del entrenador â'; 4525 if (n === 1) return 'Leer respuesta del entrenador â'; 4526 return 'Leer las ' + n + ' respuestas â'; 4527 } 4528 4529 function quitarFiltro(e) { 4530 if (e) { e.preventDefault(); e.stopPropagation(); } 4531 CAT = null; 4532 try { history.replaceState(null, '', window.location.pathname); } catch (err) {} 4533 4534 Array.prototype.forEach.call(document.querySelectorAll('.re-cats__link'), function (chip) { 4535 chip.classList.remove('is-active'); 4536 var c = chip.querySelector('.re-cats__check'); 4537 if (c) c.parentNode.removeChild(c); 4538 var orig = chip.getAttribute('data-re-href'); 4539 if (orig) chip.setAttribute('href', orig); 4540 }); 4541 4542 Array.prototype.forEach.call(document.querySelectorAll('.re-oq-card'), function (card) { 4543 var wrap = card.closest ? (card.closest('.w-dyn-item') || card) : card; 4544 wrap.style.setProperty('display', '', 'important'); 4545 }); 4546 4547 var header = document.querySelector('.re-oq-title'); 4548 if (header) { 4549 header.removeAttribute('data-re-done'); 4550 header.classList.remove('is-filtered'); 4551 header.textContent = '¿Tienes prisa? Encuentra tu respuesta al instante'; 4552 } 4553 var clear = document.getElementById('re-oq-clear'); 4554 if (clear) clear.parentNode.removeChild(clear); 4555 var reset = document.getElementById('re-cats-reset'); 4556 if (reset) reset.parentNode.removeChild(reset); 4557 var expl = document.getElementById('re-oq-explorar'); 4558 if (expl) expl.style.display = 'none'; 4559 } 4560 4561 function aplicar() { 4562 var chips = document.querySelectorAll('.re-cats__link'); 4563 var cards = document.querySelectorAll('.re-oq-card'); 4564 4565 Array.prototype.forEach.call(chips, function (chip) { 4566 if (!chip.getAttribute('data-re-href')) { 4567 chip.setAttribute('data-re-href', chip.getAttribute('href') || ''); 4568 } 4569 }); 4570 4571 if (CAT) { 4572 Array.prototype.forEach.call(chips, function (chip) { 4573 var orig = chip.getAttribute('data-re-href') || ''; 4574 if (orig.indexOf('categoria=' + CAT) !== -1) { 4575 chip.classList.add('is-active'); 4576 chip.setAttribute('title', 'Quitar filtro'); 4577 if (!chip.getAttribute('data-re-toggle')) { 4578 chip.setAttribute('data-re-toggle', '1'); 4579 chip.addEventListener('click', function (e) { 4580 if (chip.classList.contains('is-active')) quitarFiltro(e); 4581 }); 4582 } 4583 if (!chip.querySelector('.re-cats__check')) { 4584 var c = document.createElement('span'); 4585 c.className = 're-cats__check'; 4586 c.textContent = 'â'; 4587 chip.appendChild(c); 4588 } 4589 } 4590 }); 4591 4592 var listaCats = document.querySelector('.re-cats__list'); 4593 if (listaCats && !document.getElementById('re-cats-reset')) { 4594 var r = document.createElement('a'); 4595 r.id = 're-cats-reset'; 4596 r.className = 're-cats__reset'; 4597 r.href = '#'; 4598 r.textContent = 'Ver todas las categorias'; 4599 r.addEventListener('click', quitarFiltro); 4600 listaCats.insertAdjacentElement('afterend', r); 4601 } 4602 4603 if (!yaColocado) { 4604 yaColocado = true; 4605 var seccionCats = document.querySelector('.re-cats'); 4606 if (seccionCats && window.scrollY < 40) { 4607 var top = seccionCats.getBoundingClientRect().top + window.scrollY - 70; 4608 window.scrollTo(0, top > 0 ? top : 0); 4609 } 4610 } 4611 } 4612 4613 if (!cards.length) return false; 4614 4615 // Boton de continuidad al final del bloque de preguntas 4616 var seccionOQ = document.querySelector('.re-oq-section'); 4617 var listaOQ = document.querySelector('.re-oq-section .w-dyn-list'); 4618 if (seccionOQ && listaOQ && !document.getElementById('re-oq-explorar')) { 4619 var ex = document.createElement('a'); 4620 ex.id = 're-oq-explorar'; 4621 ex.className = 're-oq-explorar'; 4622 ex.href = '/pregunta-al-experto'; 4623 ex.textContent = 'Explorar toda la biblioteca de preguntas'; 4624 ex.addEventListener('click', function (e) { 4625 if (CAT) { quitarFiltro(e); } 4626 }); 4627 listaOQ.insertAdjacentElement('afterend', ex); 4628 } 4629 var explBtn = document.getElementById('re-oq-explorar'); 4630 if (explBtn) explBtn.style.display = CAT ? 'block' : 'none'; 4631 4632 if (CAT) { 4633 var filtro = CAT.trim().toLowerCase(); 4634 var visibles = 0; 4635 Array.prototype.forEach.call(cards, function (card) { 4636 var h = card.querySelector('.re-oq-hidden'); 4637 var cat = h ? (h.textContent || '').trim().toLowerCase() : ''; 4638 var ok = (cat === filtro); 4639 var wrap = card.closest ? (card.closest('.w-dyn-item') || card) : card; 4640 wrap.style.setProperty('display', ok ? '' : 'none', 'important'); 4641 if (ok) visibles++; 4642 }); 4643 4644 var header = document.querySelector('.re-oq-title'); 4645 if (header && !header.getAttribute('data-re-done')) { 4646 header.setAttribute('data-re-done', '1'); 4647 header.classList.add('is-filtered'); 4648 var nombre = filtro.replace(/-/g, ' ');
4649 nombre = nombre.charAt(0).toUpperCase() + nombre.slice(1); 4650 header.textContent = visibles > 0 4651 ? nombre + ' (' + visibles + ')' 4652 : 'Todavia no hay preguntas sobre ' + nombre.toLowerCase(); 4653 4654 if (!document.getElementById('re-oq-clear')) { 4655 var a = document.createElement('a'); 4656 a.id = 're-oq-clear'; 4657 a.href = '#'; 4658 a.textContent = 'Ver todas las preguntas â'; 4659 a.style.cssText = 'display:block;width:100%;text-align:center;margin-top:8px;font-size:13px;font-weight:700;color:#089F3B;text-decoration:none;'; 4660 a.addEventListener('click', quitarFiltro); 4661 var contenedor = header.parentElement || header; 4662 contenedor.insertAdjacentElement('afterend', a); 4663 } 4664 } 4665 } 4666 4667 Array.prototype.forEach.call(cards, function (card) { 4668 var detail = card.querySelector('.re-oq-detail'); 4669 var toggle = card.querySelector('[data-oq-toggle]'); 4670 if (!detail || !toggle || toggle.getAttribute('data-re-bound')) return; 4671 toggle.setAttribute('data-re-bound', '1'); 4672 toggle.style.display = (detail.scrollHeight > detail.clientHeight + 2) ? 'block' : 'none'; 4673 toggle.addEventListener('click', function (e) { 4674 e.preventDefault(); 4675 e.stopPropagation(); 4676 var exp = detail.classList.toggle('re-oq-detail--expanded'); 4677 toggle.textContent = exp ? 'Ver menos' : 'Ver mas'; 4678 }); 4679 }); 4680 4681 if (!contadoresPintados) { 4682 contadoresPintados = true; 4683 cargarSupabase(function (db) { 4684 // Nada de respuestas(count): PostgREST lo traduce a una referencia de 4685 // fila entera y la clave publicable tiene los permisos de `respuestas` 4686 // por columna (migracion 20260819150000), asi que devuelve 403. Se 4687 // piden los id de una columna concedida y se cuentan aqui. 4688 db.from('preguntas').select('slug, respuestas(id)').then(function (res) { 4689 if (res.error) { console.warn('[PAE] contador de respuestas:', res.error.message); return; } 4690 var mapa = {}; 4691 (res.data || []).forEach(function (r) { 4692 mapa[r.slug] = (r.respuestas || []).length; 4693 }); 4694 Array.prototype.forEach.call(document.querySelectorAll('.re-oq-card'), function (card) { 4695 var slug = (card.getAttribute('href') || '').split('/').filter(Boolean).pop(); 4696 var v = card.querySelector('[data-oq-ver]'); 4697 if (!v) return; 4698 v.textContent = textoVer(mapa[slug]); 4699 }); 4700 }); 4701 }); 4702 } 4703 4704 return true; 4705 } 4706 4707 function cargarSupabase(cb) { 4708 function go() { 4709 try { 4710 cb(window.supabase.createClient( 4711 'https://yrzanatzshdpvypazgeb.supabase.co', 4712 'sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD' 4713 )); 4714 } catch (e) {} 4715 } 4716 if (window.supabase && window.supabase.createClient) { go(); return; } 4717 var s = document.createElement('script'); 4718 s.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2'; 4719 s.onload = go; 4720 document.head.appendChild(s); 4721 } 4722 4723 function arrancar() { 4724 aplicar(); 4725 var intentos = 0; 4726 var t = setInterval(function () { 4727 intentos++; 4728 aplicar(); 4729 if (intentos > 20) clearInterval(t); 4730 }, 300); 4731 4732 var lista = document.querySelector('.w-dyn-list'); 4733 if (lista && window.MutationObserver) { 4734 new MutationObserver(function () { aplicar(); }).observe(lista, { childList: true, subtree: true }); 4735 } 4736 } 4737 4738 if (document.readyState === 'loading') { 4739 document.addEventListener('DOMContentLoaded', arrancar); 4740 } else { 4741 arrancar(); 4742 } 4743})(); 4744</script>
4744 4745 4746<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4747 BOTÃN FLOTANTE DE WHATSAPP 4748 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 4749 4750<a 4751 href="https://wa.me/34633590498?text=Hola%2C%20me%20gustar%C3%ADa%20m%C3%A1s%20informaci%C3%B3n" 4752 target="_blank" 4753 rel="noopener noreferrer" 4754 class="wa-float-btn" 4755 aria-label="Contactar por WhatsApp" 4756> 4757 <svg viewBox="0 0 24 24" width="30" height="30" fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg"> 4758 <path d="M12 2a10 10 0 0 0-8.61 15.06L2 22l5.06-1.33A10 10 0 1 0 12 2zm0 18.13a8.1 8.1 0 0 1-4.14-1.14l-.3-.18-3.08.81.82-3-.2-.31A8.13 8.13 0 1 1 12 20.13z"/> 4759 <path d="M16.57 13.72c-.25-.13-1.47-.72-1.7-.81s-.4-.13-.57.13-.65.81-.8.97-.3.19-.55.06a6.76 6.76 0 0 1-2-1.23 7.47 7.47 0 0 1-1.38-1.72c-.14-.25 0-.38.11-.5s.25-.3.37-.44a1.7 1.7 0 0 0 .25-.42.46.46 0 0 0 0-.44c-.06-.13-.57-1.37-.78-1.87s-.42-.43-.57-.44h-.49a.94.94 0 0 0-.68.32 2.87 2.87 0 0 0-.9 2.13 5 5 0 0 0 1.05 2.64 11.4 11.4 0 0 0 4.36 3.85c.61.26 1.08.42 1.45.54a3.48 3.48 0 0 0 1.6.1 2.62 2.62 0 0 0 1.72-1.21 2.13 2.13 0 0 0 .15-1.21c-.06-.11-.23-.17-.48-.3z"/> 4760 </svg> 4761</a> 4762 4763<style> 4764 .wa-float-btn { 4765 position: fixed; 4766 bottom: 24px; 4767 right: 24px; 4768 width: 60px; 4769 height: 60px; 4770 background-color: #25D366; 4771 border-radius: 50%; 4772 display: flex; 4773 align-items: center; 4774 justify-content: center; 4775 box-shadow: 0 4px 14px rgba(0,0,0,0.25); 4776 z-index: 9999; 4777 text-decoration: none; 4778 transition: transform 0.2s ease, box-shadow 0.2s ease; 4779 } 4780 .wa-float-btn:hover { 4781 transform: scale(1.08); 4782 box-shadow: 0 6px 18px rgba(0,0,0,0.3); 4783 } 4784 @media (max-width: 480px) { 4785 .wa-float-btn { 4786 width: 52px; 4787 height: 52px; 4788 bottom: 16px; 4789 right: 16px; 4790 } 4791 } 4792</style> 4793 4794<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4795 OCULTAR SECCIONES CON LISTAS CMS VACÃAS (ciudad + objetivo) 4796 Solo listas de primer nivel: una lista anidada vacÃa dentro 4797 de una tarjeta (p. ej. las especialidades de un entrenador) 4798 no debe esconder la sección entera. 4799 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ -->
4800<script> 4801document.querySelectorAll('.w-dyn-empty').forEach(function (emptyEl) { 4802 var lista = emptyEl.closest('.w-dyn-list'); 4803 if (!lista) return; 4804 4805 // Lista anidada dentro de otra lista CMS (una tarjeta): no decide por la sección. 4806 if (lista.parentElement && lista.parentElement.closest('.w-dyn-list')) return; 4807 4808 // Webflow deja el estado vacio en el HTML aunque la lista tenga elementos: 4809 // lo tapa con w-dyn-hide. Sin esta comprobacion se escondia un bloque lleno. 4810 if (lista.querySelector('.w-dyn-item')) return; 4811 4812 // En la ficha del entrenador cada bloque es un .entrenador_sobre-m dentro de 4813 // una unica <section> que ademas lleva el formulario de contacto, asi que la 4814 // regla de abajo no llegaba y se quedaba a la vista el "No items found." de 4815 // Modalidad (Ismael Diaz, 20/08/2026). Aqui manda el bloque, no la seccion. 4816 var bloque = emptyEl.closest('.entrenador_sobre-m'); 4817 if (bloque) { 4818 bloque.style.display = 'none'; 4819 return; 4820 } 4821 4822 var section = emptyEl.closest('section'); 4823 if (section && !section.querySelector('form')) { 4824 section.style.display = 'none'; 4825 } 4826}); 4827</script>
4827 4828 4829<!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4830 EMAIL â aviso de errata en el dominio (sugiere, no bloquea) 4831 Vale para TODOS los formularios del sitio, presentes y futuros: 4832 engancha por delegacion a cualquier <input> de email, no hay 4833 que tocar los widgets uno a uno. 4834 La regla vive en supabase/functions/_shared/validacion-contacto.ts 4835 (`sugerenciaDominioEmail`); esto es su copia de navegador. 4836 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 4837<style> 4838 .re-mailerrata { margin-top:6px; padding:8px 10px; border-radius:8px; background:#FEF3C7; color:#92400E; font-size:12.5px; line-height:1.35; } 4839 .re-mailerrata__fix { padding:0; border:0; background:none; font:inherit; font-weight:700; color:#92400E; text-decoration:underline; cursor:pointer; } 4840</style>
4841<script> 4842(function () { 4843 /* Dominios que damos por buenos. Un dominio de esta lista no se marca nunca, 4844 y un dominio a 1-2 letras de uno de ellos es el que se sugiere. */ 4845 var DOMINIOS = [ 4846 'gmail.com','googlemail.com', 4847 'hotmail.com','hotmail.es','hotmail.fr','hotmail.it','hotmail.co.uk','hotmail.com.ar','hotmail.com.mx', 4848 'outlook.com','outlook.es','outlook.fr','outlook.it', 4849 'live.com','live.com.mx','live.co.uk','msn.com', 4850 'yahoo.com','yahoo.es','yahoo.it','yahoo.fr','yahoo.co.uk','yahoo.com.ar','yahoo.com.mx', 4851 'icloud.com','me.com','mac.com', 4852 'aol.com','protonmail.com','proton.me','gmx.es','gmx.com','gmx.net','zoho.com','mail.com', 4853 'terra.es','telefonica.net','movistar.es','orange.es','wanadoo.es','ya.com','arrakis.es','euskalnet.net' 4854 ]; 4855 4856 function distancia(a, b) { 4857 var m = a.length, n = b.length, i, j, fila = [], ant, tmp; 4858 for (j = 0; j <= n; j++) fila[j] = j; 4859 for (i = 1; i <= m; i++) { 4860 ant = fila[0]; fila[0] = i; 4861 for (j = 1; j <= n; j++) { 4862 tmp = fila[j]; 4863 fila[j] = Math.min(fila[j] + 1, fila[j - 1] + 1, ant + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1)); 4864 ant = tmp; 4865 } 4866 } 4867 return fila[n]; 4868 } 4869 4870 /* Devuelve el email corregido o null. Solo mira el dominio, y solo cuando el 4871 email ya tiene forma valida: el "falta algo" lo dice el widget, no esto. 4872 El tope de 2 letras se reserva a dominios largos; en uno corto (ya.com, 4873 me.com) dos cambios ya son otro dominio distinto, no una errata. */ 4874 function sugerencia(valor) { 4875 var limpio = String(valor == null ? '' : valor).trim().toLowerCase(); 4876 if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(limpio)) return null; 4877 var corte = limpio.lastIndexOf('@'); 4878 var local = limpio.slice(0, corte), dominio = limpio.slice(corte + 1); 4879 if (DOMINIOS.indexOf(dominio) !== -1) return null; 4880 var tope = dominio.length >= 8 ? 2 : 1; 4881 var mejor = null, mejorD = 3, i, d; 4882 for (i = 0; i < DOMINIOS.length; i++) { 4883 d = distancia(dominio, DOMINIOS[i]); 4884 if (d <= tope && d < mejorD) { mejorD = d; mejor = DOMINIOS[i]; } 4885 } 4886 return mejor ? local + '@' + mejor : null; 4887 } 4888 4889 function esCampoEmail(el) { 4890 if (!el || el.tagName !== 'INPUT') return false; 4891 var t = (el.getAttribute('type') || '').toLowerCase(); 4892 if (t === 'email') return true; 4893 if (t && t !== 'text') return false; 4894 if ((el.getAttribute('inputmode') || '').toLowerCase() === 'email') return true; 4895 return /e-?mail|correo/i.test((el.name || '') + ' ' + (el.id || '') + ' ' + (el.getAttribute('autocomplete') || '')); 4896 } 4897 4898 function quitar(input) { 4899 var sig = input.nextElementSibling; 4900 if (sig && sig.classList && sig.classList.contains('re-mailerrata')) sig.parentNode.removeChild(sig); 4901 } 4902 4903 function medir(evento, dominio) { 4904 try { if (window.amplitude && window.amplitude.track) window.amplitude.track(evento, { dominio: dominio }); } catch (e) {} 4905 } 4906 4907 function revisar(input) { 4908 quitar(input); 4909 if (!input.isConnected) return; 4910 var propuesta = sugerencia(input.value); 4911 if (!propuesta) return; 4912 4913 var caja = document.createElement('div'); 4914 caja.className = 're-mailerrata'; 4915 caja.setAttribute('role', 'status'); 4916 caja.appendChild(document.createTextNode('¿Quisiste decir ')); 4917 4918 var btn = document.createElement('button'); 4919 btn.type = 'button'; 4920 btn.className = 're-mailerrata__fix'; 4921 btn.textContent = propuesta; 4922 btn.addEventListener('click', function () { 4923 input.value = propuesta;
4924 try { 4925 input.dispatchEvent(new Event('input', { bubbles: true })); 4926 input.dispatchEvent(new Event('change', { bubbles: true })); 4927 } catch (e) {} 4928 quitar(input); 4929 input.focus(); 4930 medir('email_errata_corregida', propuesta.split('@')[1]); 4931 }); 4932 4933 caja.appendChild(btn); 4934 caja.appendChild(document.createTextNode('?')); 4935 input.insertAdjacentElement('afterend', caja); 4936 medir('email_errata_sugerida', input.value.split('@').pop().toLowerCase()); 4937 } 4938 4939 /* Al escribir se propone con retardo (asi el aviso ya esta en pantalla antes 4940 de que llegue al boton de enviar) y al salir del campo, de inmediato. */ 4941 var reloj = null; 4942 document.addEventListener('input', function (e) { 4943 if (!esCampoEmail(e.target)) return; 4944 quitar(e.target); 4945 clearTimeout(reloj); 4946 var input = e.target; 4947 reloj = setTimeout(function () { revisar(input); }, 500); 4948 }); 4949 document.addEventListener('focusout', function (e) { 4950 if (!esCampoEmail(e.target)) return; 4951 clearTimeout(reloj); 4952 revisar(e.target); 4953 }); 4954})(); 4955</script>
4955 4956 4957<!-- Master Tag add just before the closing </body> tag --> 4958
4958<script src="https://www.dwin1.com/130189.js" type="text/javascript" defer="defer"></script>
vendor: 1 bytes, line 4958
4958
4959<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a815b2ecfc98b6d18a9afab%2Fvisitas_ficha_publica-1.0.0.js" type="text/javascript"></script>
4959<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a9562a243b6dd3757592da5%2Flimpiarcamposcontacto-1.0.0.js" type="text/javascript"></script>
4959</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.