PageSourceSearch

https://redentrenadores-268bb8.webflow.io/

html redentrenadores-268bb8.webflow.io collected 2026-10-03 13:38:34 UTC 281,577 bytes, 4,959 lines download raw bytes

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{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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(&quot;https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F69a07ccb1e9bd657dced8e6f_5319754-uhd_2160_3840_25fps_poster.0000000.jpg&quot;)" 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">&#10005;</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">&#10005;</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">&#10005;</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.