PageSourceSearch

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

html redentrenadores-268bb8.webflow.io collected 2026-10-03 13:38:41 UTC 236,283 bytes, 3,892 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="6a836f59023080aa5e2a44fe" data-wf-site="68c5a729c71fa3f37858fbbe" lang="es"><head><meta charset="utf-8"/><link href="https://cdn.prod.website-files.com" rel="preconnect" crossorigin="anonymous"/><title>Dónde entrenar: gimnasios, parques y pistas | RedEntrenadores</title><meta content="Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allí y escríbele gratis." name="description"/><meta content="Dónde entrenar: gimnasios, parques y pistas | RedEntrenadores" property="og:title"/><meta content="Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allí y escríbele gratis." property="og:description"/><meta content="https://images.unsplash.com/photo-1778996454753-ca8b1d21b4c7?auto=format&amp;fit=crop&amp;w=1200&amp;h=630&amp;q=60" property="og:image"/><meta content="Dónde entrenar: gimnasios, parques y pistas | RedEntrenadores" name="twitter:title"/><meta content="Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allí y escríbele gratis." name="twitter:description"/><meta content="https://images.unsplash.com/photo-1778996454753-ca8b1d21b4c7?auto=format&amp;fit=crop&amp;w=1200&amp;h=630&amp;q=60" name="twitter:image"/><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.6a836f59023080aa5e2a44fe.72bed271c.opt.min.css" rel="stylesheet" type="text/css" integrity="sha384-cr7SccRUHWG1uANAmMLnXF3Y90TamgViBCc6pmJQEOVqTKXFL4Z3sz5x1YdPU2Kk" 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": "CollectionPage",
5  "@id": "https://www.redentrenadores.com/donde-entrenar#webpage",
6  "name": "Dónde entrenar cerca de ti",
7  "description": "Mapa de sitios donde entrenar: gimnasios, parques, pistas y playas. Mira cómo es cada uno, descubre quién entrena allí y escríbele gratis.",
8  "url": "https://www.redentrenadores.com/donde-entrenar",
9  "inLanguage": "es",
10  "publisher": {
11    "@id": "https://www.redentrenadores.com/#organization"
12  },
13  "breadcrumb": {
14    "@type": "BreadcrumbList",
15    "itemListElement": [
16      {
17        "@type": "ListItem",
18        "position": 1,
19        "name": "Inicio",
20        "item": "https://www.redentrenadores.com"
21      },
22      {
23        "@type": "ListItem",
24        "position": 2,
25        "name": "Dónde entrenar",
26        "item": "https://www.redentrenadores.com/donde-entrenar"
27      }
28    ]
29  }
30}
31</script>
31<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>
31<script async="" src="/avljl2rk9q5pNjhjNWE3MjljNzFmYTNmMzc4NThmYmJl/peOuxr4X3pGz_RiXea8PdbkeTQo"></script>
31<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>
31<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>
31<!-- Google Tag Manager -->
32<script>
vendor: 327 bytes, lines 32-36
32(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
33new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
34j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
35'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
36})(window,document,'script','dataLayer','
36GTM-KBD83QKS
vendor: 3 bytes, line 36
36');
36</script>
36
37<!-- End Google Tag Manager -->
38
39<!-- Start cookieyes banner --> 
40<script id="cookieyes" type="text/javascript" src="https://cdn-cookieyes.com/client_data/3b90490e3918e885f5cdab3d/script.js"></script>
40 
41<!-- End cookieyes banner -->
42
43
44<!-- Hotjar / Contentsquare: async porque es analitica pura, nada en el repo
45     depende de que este cargado (grep sin resultados fuera de backups de este
46     mismo bloque). Eran 623 KB bloqueando el primer pintado: el mayor peso de
47     los 4760ms de "solicitudes que bloquean el renderizado" de PageSpeed. -->
48<script src="https://t.contentsquare.net/uxa/e67e16087d6b8.js" async></script>
48
49
50
51<!-- Memberstack: solo fuera de /lp/*. Fuente y porque:
52     widgets/custom-code-memberstack-por-ruta.html -->
53<script>
54(function () {
55  // Las landings de paid no tienen nada que dependa de Memberstack. Cualquier
56  // otra ruta lo carga igual que siempre, bloqueando, porque los widgets del
57  // panel dan por hecho que $memberstackDom ya existe cuando ellos corren.
58  if (/^\/lp(\/|$)/.test(location.pathname)) return;
59
60  document.write(
61    '
61<script data-memberstack-app="app_cmgl62z3b00kw0tt21gk97j2x"' +
62    ' src="https://static.memberstack.com/scripts/v1/memberstack.js"' +
63    ' type="text/javascript"><\/script>'
64  );
65})();
66</script>
66
67
68<!-- Para que los dropdown sean blancos --> 
69<style>
70.w-dropdown-list {
71  background-color: white !important;
72}
73
74.w-dropdown.w--open .w-dropdown-list {
75  background-color: white !important;
76}
77
78.search-dropdown {
79  background-color: white !important;
80}
81</style>
82
83
84<!-- Limita el texto descriptivo de cada entrenador -->
85<script>
86document.addEventListener("DOMContentLoaded", function() {
87  const elements = document.querySelectorAll(".limit-text");
88
89  elements.forEach(el => {
90    const text = el.textContent.trim();
91    
92    if (text.length > 80) {
93      el.textContent = text.substring(0, 80) + "...";
94    }
95  });
96});
97</script>
97
98
99
100<!-- Canonical autorreferenciado (auditoria SEO 2026-08) -->
101<script>
102(function () {
103  if (document.querySelector('link[rel="canonical"]')) return;
104  var p = location.pathname;
105  while (p.length > 1 && p.charAt(p.length - 1) === '/') p = p.slice(0, -1);
106  var l = document.createElement('link');
107  l.rel = 'canonical';
108  l.href = 'https://www.redentrenadores.com' + (p || '/');
109  document.head.appendChild(l);
110})();
111</script>
111
112
113
114<!-- Impide que se descargue el video de fondo de la seccion de FAQ.
115     Es decorativo, esta al final de la pagina y su webm pesa 4,6 MB en las 61
116     paginas que lo llevan (home, listado y las 59 de ciudad). Ocultarlo por CSS
117     no basta: Webflow crea el <video> con autoplay y el navegador se lo baja
118     igual aunque el contenedor este en display:none (comprobado: paused=false,
119     buffered=15s). Sin data-video-urls el script de Webflow no crea el <video>,
120     asi que no hay nada que pedir.
121     Para revertirlo, basta con borrar este bloque y quitar el display:none de
122     la clase bg-video_faqs.
123     ACTUALIZACION 2026-08-11: en /entrenadores-personales/* el elemento se
124     borro directamente (ver commit), asi que este bloque ya no hace nada ahi.
125     Sigue haciendo falta en / y /entrenador-personal hasta que se borre el
126     elemento tambien en esas dos plantillas (vive dentro de un componente que
127     la API no puede recorrer). -->
128<script>
129document.addEventListener('DOMContentLoaded', function () {
130  document.querySelectorAll('[data-video-urls]').forEach(function (el) {
131    el.removeAttribute('data-video-urls');
132  });
133});
134</script>
134
135
136
137<!-- ══════════════════════════════════════════════════════════
138     AMPLITUDE - analitica + session replay (2026-08-14)
139     El proyecto no tiene bundler (no existe package.json en el repo), asi que
140     en vez de `npm install @amplitude/unified` se carga EL MISMO paquete como
141     modulo ESM desde jsDelivr por import() dinamico. La llamada de init es la
142     de la documentacion: initAll, no init.
143
144     CONSENTIMIENTO: no arranca hasta que cookieyes tiene la categoria
145     "analytics" en yes. Esto vale tanto para la analitica como para Session
146     Replay, que graba el 100% de las sesiones (sampleRate 1) y sin
147     consentimiento no puede grabar nada. Verificado sobre el propio banner.js
148     que sirve el sitio: la cookie es "cookieyes-consent" con pares
149     clave:valor separados por coma, las categorias son necessary / functional
150     / analytics / performance / advertisement, y al aceptar se despacha
151     "cookieyes_consent_update" sobre document. Bajo RGPD analytics arranca en
152     "no", asi que por defecto aqui no se carga nada.
153
154     El stub de window.amplitude existe desde el primer momento y encola los
155     track() previos a la carga del modulo; al inicializar se vacia la cola. Si
156     el usuario nunca da su consentimiento, la cola se descarta sin enviarse.
157     Los embeds de widgets/ llaman a amplitude.track(...) contando con esto.
158     ══════════════════════════════════════════════════════════ -->
159<script>
160(function () {
161  window.amplitude = window.amplitude || { _q: [], track: function () { this._q.push(['track', arguments]); } };
162
163  function hayConsentimiento() {
164    var m = document.cookie.match(/(?:^|;\s*)cookieyes-consent=([^;]*)/);
165    if (!m) return false;
166    return decodeURIComponent(m[1]).split(',').indexOf('analytics:yes') !== -1;
167  }
168
169  var arrancado = false;
170  function arrancar() {
171    if (arrancado || !hayConsentimiento()) return;
172    arrancado = true;
173    import('https://cdn.jsdelivr.net/npm/@amplitude/unified@1/+esm').then(function (amplitude) {
174      var pendientes = (window.amplitude && window.amplitude._q) || [];
175      // Amplitude ingestion key — public by design; move to an env var when you set up environments.
176      amplitude.initAll('21ce582269eabdf7c4208d1960a7731e', {"analytics":{"autocapture":true},"sessionReplay":{"sampleRate":1}});
177      window.amplitude = amplitude;
178      pendientes.forEach(function (c) { amplitude[c[0]].apply(null, c[1]); });
179    }).catch(function () { arrancado = false; });
180  }
181
182  arrancar();
183  document.addEventListener('cookieyes_consent_update', arrancar);
184})();
185</script>
185
186
187<!-- ══════════════════════════════════════════════════════════
188     SIMULAR PANEL DE ENTRENADOR (2026-08-21)
189     Deja que el equipo recorra /panel-entrenador viendo los datos de un
190     entrenador concreto, sin entrar con su cuenta y sin poder tocar nada. Lo
191     enciende la pestana "Simular entrenador" de /admin, que abre el panel con
192     ?simular=<token>. Solo lectura: el shim corta cualquier metodo que no sea
193     GET, y las Edge Functions lo rechazan tambien en el servidor.
194
195     Va aqui y no como registered script porque esos tienen un tope de 2.000
196     caracteres y este no cabe (son 10 KB). Se pega MINIFICADO: la fuente
197     legible, con el porque de cada parte, es
198     widgets/custom-code-simular-entrenador.js del repo, y lo de abajo sale de
199     widgets/custom-code-simular-entrenador.min.js. Para cambiarlo: editar la
200     fuente, regenerar con terser y volver a pegar. No editar esto a mano.
201
202     Se corta solo en cuanto la ruta no empieza por /panel-entrenador, asi que
203     en el resto del sitio no hace nada mas que existir.
204     ══════════════════════════════════════════════════════════ -->
205<script>
206/*! Simular panel de entrenador — RedEntrenadores
207 *  Version minificada. La fuente legible, con el porque de cada parte, esta en
208 *  widgets/custom-code-simular-entrenador.js del repo. Para cambiarlo: editar
209 *  ese fichero, regenerar con `npx terser` y volver a pegar aqui. No editar
210 *  este bloque a mano. */
211!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;
211min-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'})}}}();
212
213</script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a7b1ad624d451ff7ab95b57%2Fdeep_link_panel_tras_login-0.0.1.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a8188e2745a991cf146025e%2Fredirect_urls_antiguas_panel-1.0.0.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a90ae0a69c0c92f2517a3b6%2Fawintracking2-1.0.0.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6aa83d1b2d69974135a99d99%2Fguardian_sesion_panel-1.0.0.js" type="text/javascript"></script>
213<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6aaed9e15b8290737a4ca482%2Fguardia_onboarding_entrenador-1.0.0.js" type="text/javascript"></script>
213<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>
213</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="/" class="rl_navbar5_logo-link w-nav-brand"><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" aria-current="page" class="nav-pill-item w--current">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">
214<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"/>
215</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
215pe-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" aria-current="page" class="rl_navbar5_dropdown-link w--current"><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">
215<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
216<path d="M6 3L11 8L6 13" stroke="CurrentColor" stroke-width="1.5"/>
217</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">
217<script>
218/* Webflow solo permite 991/767/479 como punto de colapso y su JS decide por su
219   cuenta (ignora el CSS). Con data-collapse=all lo gestiona en todos los anchos
220   y el <style> de abajo devuelve la fila a partir de 1180px. Este embed es hijo
221   del navbar y se ejecuta al parsearse, antes de que cargue webflow.js al final
222   del body, asi que el atributo ya esta puesto cuando Webflow inicializa.
223   El listener de resize hace falta porque, al creer que siempre esta colapsado,
224   Webflow ya no cierra el menu al cruzar un breakpoint: sin esto, quien abre el
225   menu a 1100px y luego amplia la ventana se queda con el menu enganchado
226   (Webflow mueve el <nav> dentro de .w-nav-overlay al abrir). Se cierra con su
227   propio boton para que Webflow restaure el DOM y limpie sus estilos inline. */
228(function(){
229  var s=document.currentScript,n=s&&s.parentElement&&s.parentElement.closest(".w-nav");
230  if(!n)return;
231  n.setAttribute("data-collapse","all");
232  var t;
233  window.addEventListener("resize",function(){
234    clearTimeout(t);
235    t=setTimeout(function(){
236      if(window.innerWidth<1180)return;
237      var b=n.querySelector(".w-nav-button.w--open");
238      if(b)b.click();
239    },100);
240  });
241})();
242</script>
242
243<style>
244/* El menu colapsa a hamburguesa hasta 1179px: por debajo de ~1165 no caben los
245   dos enlaces destacados y el flexbox aplasta el logo de 250 a 99px (medido).
246   Este bloque replica, para 992-1179px, las reglas que Webflow ya aplica bajo
247   991px al desplegar el menu. Revertir: borrar este embed entero y poner
248   "Collapse menu at" = medium en el Navbar.
249   Fuente en el repo: widgets/custom-code-header-colapso.css */
250@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}}
251@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}}
252</style></div></div><div class="mapa-hero"><h1 class="mapa-hero_titulo">Dónde entrenar cerca de ti</h1><p class="mapa-hero_entradilla">Gimnasios, parques, pistas y playas donde entrenan de verdad los entrenadores personales. Mira cómo es cada sitio, descubre quién entrena allí y escríbele directamente.</p><img src="https://images.unsplash.com/photo-1778996454753-ca8b1d21b4c7?auto=format&amp;fit=crop&amp;w=1200&amp;q=60" alt="Aparatos de ejercicio al aire libre en un parque" width="1200" height="300" fetchpriority="high" decoding="async" class="mapa-hero_img"/></div><div><div class="w-embed w-script"><!-- RedEntrenadores · Mapa de lugares. COMPONENTE, no página.
253
254     Esto es solo el mapa: filtros, lienzo, pines, burbuja y panel. Ni H1, ni
255     entradilla, ni FAQ: todo eso vive fuera, como elementos nativos de la
256     página que lo use. Así el mismo mapa se puede meter en /donde-entrenar, en
257     las páginas de tipo, en las de ciudad o donde haga falta sin arrastrar un
258     segundo H1 ni duplicar contenido.
259
260     Sale de partir widget-donde-entrenar-mapa.html con
261     scripts/partir-embed-mapa.js. El id pasó de rt-donde-entrenar (nombre de
262     página) a rt-mapa-lugares (nombre de componente).
263
264     El filtro inicial no se decide aquí: lo pone la página, y de aplicarlo se
265     encarga widget-donde-entrenar-filtro-glue.html, que lee un data-tipo o un
266     data-familia del contenedor —o el hash de la URL— y pulsa los propios
267     botones del mapa.
268
269     Lee directo de Supabase con la clave publicable: `lugares`,
270     `entrenador_lugares` y el RPC `lugares_en_bbox` tienen lectura pública. -->
271<style>
272#rt-mapa-lugares{font-family:"Plus Jakarta Sans",sans-serif;color:#1B1E22;background:#fff;}
273#rt-mapa-lugares *{box-sizing:border-box;}
274#rt-mapa-lugares .rt-ancho{max-width:1200px;margin:0 auto;padding:0 24px;}
275#rt-mapa-lugares .rt-filtros-caja{background:#F9FAFB;border:1px solid #d9dde2;border-radius:14px;padding:14px 16px;margin:0 auto 16px;max-width:1200px;}
276#rt-mapa-lugares .rt-segmentado{display:inline-flex;background:#fff;border:1px solid #d9dde2;border-radius:999px;padding:4px;gap:4px;max-width:100%;overflow-x:auto;}
277#rt-mapa-lugares .rt-seg-btn{border:none;background:none;border-radius:999px;padding:9px 20px;font-family:inherit;font-size:13.5px;font-weight:600;color:#4B5563;cursor:pointer;white-space:nowrap;}
278#rt-mapa-lugares .rt-seg-btn:hover{color:#089F3B;}
279#rt-mapa-lugares .rt-seg-btn.sel{background:#089F3B;color:#fff;}
280#rt-mapa-lugares .rt-fila{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;}
281#rt-mapa-lugares .rt-fila-tipos{margin-top:12px;padding-top:12px;border-top:1px dashed #d9dde2;}
282#rt-mapa-lugares .rt-etiqueta{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;flex-shrink:0;}
283#rt-mapa-lugares .rt-tipos{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;}
284#rt-mapa-lugares .rt-chip{border:1px solid #d9dde2;background:#fff;border-radius:999px;padding:10px 20px;font-size:14px;font-weight:600;font-family:inherit;color:#4B5563;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.06);}
285#rt-mapa-lugares .rt-chip:hover{border-color:#089F3B;color:#089F3B;}
286#rt-mapa-lugares .rt-chip.sel{border-color:#089F3B;background:#089F3B;color:#fff;}
287#rt-mapa-lugares .rt-chip-peq{padding:7px 15px;font-size:12.5px;font-weight:500;box-shadow:none;}
288#rt-mapa-lugares .rt-chip-peq.sel{background:#EAF3DE;color:#089F3B;border-color:#089F3B;}
289#rt-mapa-lugares .rt-lienzo{position:relative;border:1px solid #d9dde2;border-radius:14px;overflow:hidden;max-width:1200px;margin:0 auto;}
290#rt-mapa-lugares .rt-mapa{height:600px;}
291#rt-mapa-lugares .rt-pin{width:34px;height:34px;display:flex;align-items:center;justify-content:center;border:2.5px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.35);color:#fff;}
292#rt-mapa-lugares .rt-pin.centro{border-radius:9px;}
293#rt-mapa-lugares .rt-pin.publico{border-radius:50% 50% 50% 4px;transform:rotate(-45deg);}
294#rt-mapa-lugares .rt-pin.publico svg{transform:rotate(45deg);}
295#rt-mapa-lugares .rt-pin svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
296#rt-mapa-lugares .leaflet-popup-content-wrapper{border-radius:12px;box-shadow:0 6px 24px rgba(0,0,0,.22);}
297#rt-mapa-lugares .leaflet-popup-content{margin:0;width:290px!important;font-family:"Plus Jakarta Sans",sans-serif;}
298#rt-mapa-lugares .rt-bur-cab{padding:14px 16px 12px;}
299#rt-mapa-lugares .rt-bur-cab h3{font-family:"Barlow",sans-serif;font-size:17px;font-weight:700;margin:0 0 3px;line-height:1.25;}
300#rt-mapa-lugares .rt-bur-meta{font-size:12.5px;color:#6b7280;margin:0;}
301#rt-mapa-lugares .rt-bur-ents{border-top:1px solid #eef0f2;padding:12px 16px 14px;}
302#rt-mapa-lugares .rt-bur-tit{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;margin:0 0 10px;}
303#rt-mapa-lugares .rt-caras{display:flex;align-items:center;gap:-8px;margin-bottom:10px;}
304#rt-mapa-lugares .rt-cara{width:46px;height:46px;border-radius:50%;border:2px solid #fff;margin-right:-8px;background:#F3F3F3;object-fit:cover;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#1B1E22;flex-shrink:0;}
305/* La nota va siempre en verde de marca, tambien dentro de un parrafo gris: la
306   estrella es marca, no estado (design system §48). */
307#rt-mapa-lugares .rt-estrellas{color:#089F3B;font-weight:700;white-space:nowrap;}
308#rt-mapa-lugares .rt-bur-nombres{font-size:12.5px;color:#4B5563;line-height:1.45;margin:0 0 12px;}
309#rt-mapa-lugares .rt-bur-btn{width:100%;padding:10px;border:none;border-radius:8px;background:#089F3B;color:#fff;font-family:inherit;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;cursor:pointer;}
310#rt-mapa-lugares .rt-bur-btn:hover{background:#067A2D;}
311#rt-mapa-lugares .rt-bur-btn-sec{background:#fff;color:#089F3B;border:1px solid #089F3B;}
312#rt-mapa-lugares .rt-bur-btn-sec:hover{background:#EAF3DE;color:#067A2D;}
313#rt-mapa-lugares .rt-panel{position:absolute;top:0;right:0;bottom:0;width:380px;max-width:100%;background:#fff;border-left:1px solid #d9dde2;overflow-y:auto;transform:translateX(101%);transition:transform .2s ease;z-index:1000;}
314#rt-mapa-lugares .rt-panel.abierto{transform:translateX(0);}
315#rt-mapa-lugares .rt-panel-cab{padding:20px 18px 16px;border-bottom:1px solid #d9dde2;position:relative;}
316#rt-mapa-lugares .rt-cerrar{position:absolute;top:12px;right:12px;background:none;border:none;font-size:22px;line-height:1;color:#6b7280;cursor:pointer;padding:4px;}
317#rt-mapa-lugares .rt-panel h2{font-family:"Barlow",sans-serif;font-size:20px;font-weight:700;margin:0 26px 4px 0;}
318#rt-mapa-lugares .rt-meta{font-size:12.5px;color:#6b7280;margin:0;}
319#rt-mapa-lugares .rt-ent{display:flex;gap:12px;padding:16px 18px;border-bottom:1px solid #d9dde2;}
320#rt-mapa-lugares .rt-avatar{width:64px;height:64px;border-radius:50%;background:#F3F3F3;flex-shrink:0;object-fit:cover;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px;}
321#rt-mapa-lugares .rt-ent-info{min-width:0;flex:1;}
322#rt-mapa-lugares .rt-ent-nombre{font-size:14.5px;font-weight:600;margin:0;}
323#rt-mapa-lugares .rt-ent-meta{font-size:12.5px;color:#089F3B;font-weight:600;margin:2px 0 0;}
324#rt-mapa-lugares .rt-ent-desc{font-size:12.5px;color:#4B5563;line-height:1.5;margin:6px 0 0;}
325#rt-mapa-lugares .btn{padding:9px 18px;border:none;border-radius:8px;background:#089F3B;color:#fff;text-transform:uppercase;font-weight:700;letter-spacing:.03em;font-size:11px;font-family:inherit;cursor:pointer;}
326#rt-mapa-lugares .btn:hover:not(:disabled){background:#067A2D;}
327#rt-mapa-lugares .btn:disabled{opacity:.5;cursor:not-allowed;}
328#rt-mapa-lugares .btn-peq{padding:7px 14px;margin-top:8px;}
329#rt-mapa-lugares .rt-vacio{padding:26px 18px;font-size:13px;color:#6b7280;text-align:center;}
330#rt-mapa-lugares .input,#rt-mapa-lugares textarea{border:1px solid #d9dde2;border-radius:8px;padding:9px 11px;font-size:13.5px;font-family:inherit;width:100%;margin-bottom:10px;}
331#rt-mapa-lugares .input:focus,#rt-mapa-lugares textarea:focus{outline:none;border-color:#089F3B;box-shadow:0 0 0 3px rgba(8,159,59,.15);}
332#rt-mapa-lugares .rt-form{padding:16px 18px;background:#F9FAFB;}
333#rt-mapa-lugares .rt-form h3{font-size:14px;margin:0 0 4px;}
334#rt-mapa-lugares .rt-ayuda{font-size:12px;color:#6b7280;margin:0 0 12px;}
335#rt-mapa-lugares .rt-error{font-size:12.5px;color:#791F1F;margin:8px 0 0;}
336#rt-mapa-lugares .rt-ok{font-size:13px;color:#067A2D;line-height:1.5;}
337@media(max-width:700px){#rt-mapa-lugares .rt-mapa{height:440px;}
338#rt-mapa-lugares .rt-panel{width:100%;top:auto;height:72%;border-left:none;border-top:1px solid #d9dde2;transform:translateY(101%);}
339#rt-mapa-lugares .rt-panel.abierto{transform:translateY(0);}
340#rt-mapa-lugares .leaflet-popup-content{width:230px!important;}
341#rt-mapa-lugares .rt-ancho{padding-left:16px;padding-right:16px;}
342#rt-mapa-lugares .rt-fila{flex-direction:column;gap:8px;}
343#rt-mapa-lugares .rt-segmentado{width:100%;justify-content:flex-start;}
344#rt-mapa-lugares .rt-seg-btn{flex:1;padding:9px 12px;font-size:12.5px;}}
345
346</style>
347
348<div id="rt-mapa-lugares">
349
350
351  <div class="rt-ancho">
352    <div class="rt-filtros-caja">
353      <div class="rt-fila" id="rt-de-fam">
354        <span class="rt-etiqueta">Qué buscas</span>
355        <div class="rt-segmentado">
356          <button class="rt-seg-btn sel" data-familia="">Todos</button>
357          <button class="rt-seg-btn" data-familia="centro">Centros</button>
358          <button class="rt-seg-btn" data-familia="publico">Al aire libre</button>
359        </div>
360      </div>
361      <div id="rt-de-tipos"></div>
362    </div>
363    <div class="rt-lienzo">
364      <div class="rt-mapa" id="rt-de-mapa"></div>
365      <div class="rt-panel" id="rt-de-panel"></div>
366    </div>
367  </div>
368
369
370</div>
371
372<script>
373(function () {
374  var SUPABASE_URL = 'https://yrzanatzshdpvypazgeb.supabase.co';
375  var ANON_KEY = 'sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD';
376  var raiz = document.getElementById('rt-mapa-lugares');
377  if (!raiz) return;
378
379  // Cada tipo tiene su icono; la familia decide la forma del pin (cuadrado
380  // redondeado para lo cerrado, gota para lo abierto). Jugar solo con el color
381  // no bastaba: hay que ver de un vistazo si es un gimnasio o un parque.
382  var ICONOS = {
383    'gimnasio': '<path d="M4 9v6M8 7v10M16 7v10M20 9v6M8 12h8"/>',
384    'estudio-box': '<path d="M5 8v8M19 8v8M9 6v12M15 6v12"/>',
385    'polideportivo-instalacion': '<path d="M4 20V9l8-5 8 5v11"/><path d="M9 20v-6h6v6"/>',
386    'parque': '<path d="M12 3l5 7h-3l3 5H7l3-5H7z"/><path d="M12 15v6"/>',
387    'calistenia': '<path d="M5 5v14M19 5v14M5 9h14M5 14h14"/>',
388    'pista-publica': '<circle cx="12" cy="12" r="8"/><path d="M4 12h16M12 4a8 8 0 0 1 0 16a8 8 0 0 1 0-16"/>',
389    'ruta-sendero': '<path d="M6 20c0-6 4-4 4-8s-4-2-4-8"/><path d="M18 4c0 6-4 4-4 8s4 2 4 8"/>',
390    'playa': '<path d="M3 17c2 0 2 2 4 2s2-2 4-2 2 2 4 2 2-2 4-2"/><path d="M12 14V6M12 6c-3 0-5 2-5 4h10c0-2-2-4-5-4z"/>'
391  };
392
393  var FAMILIAS = {
394    centro: {
395      etiqueta: 'Centros',
396      color: '#089F3B',
397      tipos: {
398        'gimnasio': 'Gimnasios',
399        'estudio-box': 'Estudios y boxes',
400        'polideportivo-instalacion': 'Polideportivos'
401      }
402    },
403    publico: {
404      etiqueta: 'Al aire libre',
405      color: '#1B4D89',
406      tipos: {
407        'parque': 'Parques',
408        'calistenia': 'Calistenia y barras',
409        'pista-publica': 'Pistas públicas',
410        'ruta-sendero': 'Rutas y senderos',
411        'playa': 'Playas'
412      }
413    }
414  };
415
416  var SINGULAR = {
417    'gimnasio': 'Gimnasio', 'estudio-box': 'Estudio o box',
418    'polideportivo-instalacion': 'Polideportivo o instalación',
419    'parque': 'Parque', 'calistenia': 'Calistenia y barras',
420    'pista-publica': 'Pista o cancha pública', 'ruta-sendero': 'Ruta o sendero',
421    'playa': 'Playa'
422  };
423
424  var CENTRO_ESPANA = [40.4168, -3.7038];
425
426  function escapar(t) {
427    return String(t == null ? '' : t).replace(/[&<>"']/g, function (c) {
428      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
429    });
430  }
431  function plural(n, s, p) { return n + ' ' + (n === 1 ? s : p); }
432  function iniciales(n) {
433    return (n || '?').trim().split(/\s+/).slice(0, 2).map(function (p) { return p[0]; }).join('').toUpperCase();
434  }
435  // Devuelve HTML ya escapado, no texto: la nota tiene que ir en verde de marca
436  // aunque el parrafo que la rodea sea gris. La estrella es un elemento de
437  // marca, no un estado, y va fija en #089F3B (design system §48).
438  function metaEntrenador(e) {
439    var trozos = [];
440    var esp = e.especialidades && e.especialidades.length ? e.especialidades[0] : null;
441    if (esp) trozos.push(escapar(esp));
442    if (e.rating_promedio && e.total_reviews) {
443      trozos.push('<span class="rt-estrellas">★ '
444        + escapar(Number(e.rating_promedio).toFixed(1).replace('.', ','))
445        + '</span> (' + escapar(e.total_reviews) + ')');
446    }
447    return trozos.join(' · ');
448  }
449
450  function nombreFamilia(tipo) {
451    for (var f in FAMILIAS) if (FAMILIAS[f].tipos[tipo]) return f;
452    return 'publico';
453  }
454
455  function api(ruta, opciones) {
456    opciones = opciones || {};
457    var cab = { apikey: ANON_KEY, Authorization: 'Bearer ' + ANON_KEY };
458    if (opciones.cuerpo) cab['Content-Type'] = 'application/json';
459    return fetch(SUPABASE_URL + ruta, {
460      method: opciones.metodo || 'GET', headers: cab,
461      body: opciones.cuerpo ? JSON.stringify(opciones.cuerpo) : undefined
462    }).then(function (r) {
463      return r.json().catch(function () { return null; }).then(function (d) {
464        if (!r.ok) throw new Error((d && (d.error || d.message)) || 'Error');
465        return d;
466      });
467    });
468  }
469
470  var leafletListo = null;
471  function cargarLeaflet() {
472    if (leafletListo) return leafletListo;
473    leafletListo = new Promise(function (ok, mal) {
474      if (window.L) return ok(window.L);
475      var css = document.createElement('link');
476      css.rel = 'stylesheet';
477      css.href = 'https://unpkg.com/[email protected]/dist/leaflet.css';
478      document.head.appendChild(css);
479      var js = document.createElement('script');
480      js.src = 'https://unpkg.com/[email protected]/dist/leaflet.js';
481      js.onload = function () { ok(window.L); };
482      js.onerror = function () { mal(new Error('Leaflet')); };
483      document.head.appendChild(js);
484    });
485    return leafletListo;
486  }
487
488  var mapa = null, capaPines = null, filtro = { familia: null, tipo: null }, temporizador = null;
489
490  // Los filtros ya vienen en el HTML del embed; aqui solo se les da vida. Si
491  // anades una familia a FAMILIAS, anade tambien su boton alli a mano.
492  function conectarFiltros() {
493    raiz.querySelector('#rt-de-fam').addEventListener('click', function (e) {
494      var chip = e.target.closest('[data-familia]');
495      if (!chip) return;
496      filtro.familia = chip.dataset.familia || null;
497      filtro.tipo = null;
498      raiz.querySelectorAll('[data-familia]').forEach(function (c) { c.classList.remove('sel'); });
499      chip.classList.add('sel');
500      pintarTipos();
501      // Cambiar de filtro cierra la burbuja: su pin ya no tiene por que seguir
502      // en el mapa, y cargarPines() solo respeta al que la sostiene si esta abierta.
503      if (mapa) mapa.closePopup();
504      cargarPines();
505    });
506  }
507
508  function pintarTipos() {
509    var caja = raiz.querySelector('#rt-de-tipos');
510    if (!filtro.familia) { caja.innerHTML = ''; return; }
511    var tipos = FAMILIAS[filtro.familia].tipos;
512    caja.innerHTML = '<div class="rt-fila rt-fila-tipos">'
513      + '<span class="rt-etiqueta">De qué tipo</span>'
514      + '<div class="rt-tipos">'
515      + Object.keys(tipos).map(function (t) {
516          return '<button class="rt-chip rt-chip-peq" data-tipo="' + t + '">' + tipos[t] + '</button>';
517        }).join('')
518      + '</div></div>';
519    caja.querySelectorAll('[data-tipo]').forEach(function (chip) {
520      chip.addEventListener('click', function () {
521        var yaEstaba = chip.classList.contains('sel');
522        caja.querySelectorAll('[data-tipo]').forEach(function (c) { c.classList.remove('sel'); });
523        filtro.tipo = yaEstaba ? null : chip.dataset.tipo;
524        if (filtro.tipo) chip.classList.add('sel');
525        if (mapa) mapa.closePopup();
526        cargarPines();
527      });
528    });
529  }
530
531  function iconoPin(L, lugar) {
532    var fam = lugar.familia;
533    var color = (FAMILIAS[fam] || FAMILIAS.publico).color;
534    var svg = '<svg viewBox="0 0 24 24">' + (ICONOS[lugar.tipo] || ICONOS.parque) + '</svg>';
535    return L.divIcon({
536      className: '',
537      html: '<div class="rt-pin ' + fam + '" style="background:' + color + '">' + svg + '</div>',
538      iconSize: [34, 34],
539      iconAnchor: [17, fam === 'publico' ? 34 : 17],
540      popupAnchor: [0, fam === 'publico' ? -34 : -18]
541    });
542  }
543
544  // El RPC devuelve solo lo que pinta un pin; el detalle se pide al abrirlo.
545  //
546  // El contador evita que dos cargas solapadas —un paneo y el ResizeObserver
547  // disparando a la vez— se pisen: si las dos limpian antes de que la primera
548  // añada, los pines salen duplicados.
549  //
550  // Los pines se refrescan por diferencia, no borrando la capa entera. Vaciarla
551  // mataba la burbuja abierta: al abrirla Leaflet mueve el mapa para que quepa
552  // (autoPan), ese movimiento dispara 'moveend', y 250 ms despues la recarga se
553  // llevaba por delante el marcador que la sostenia. En movil pasaba siempre
554  // —el mapa mide 440 px de alto y la burbuja no cabe sin desplazar—, asi que
555  // la burbuja se abria y se cerraba sola.
556  var cargaActual = 0;
557  var pines = {};
558  function cargarPines() {
559    if (!mapa) return;
560    var miCarga = ++cargaActual;
561    var b = mapa.getBounds();
562    api('/rest/v1/rpc/lugares_en_bbox', {
563      metodo: 'POST',
564      cuerpo: {
565        min_lat: b.getSouth(), max_lat: b.getNorth(),
566        min_lng: b.getWest(), max_lng: b.getEast(),
567        p_familia: filtro.familia, p_tipo: filtro.tipo, p_limite: 500
568      }
569    }).then(function (lugares) {
570      if (miCarga !== cargaActual) return;
571      var vivos = {};
572      (lugares || []).forEach(function (l) {
573        vivos[l.id] = true;
574        if (pines[l.id]) return;
575        // La burbuja se ata al crear el pin: de abrirla y cerrarla se encarga
576        // Leaflet, y aqui solo se rellena cuando ya esta abierta.
577        var marcador = L.marker([l.lat, l.lng], { icon: iconoPin(L, l), title: l.nombre })
578          .bindPopup('<div class="rt-bur-cab"><h3>' + escapar(l.nombre) + '</h3>'
579            + '<p class="rt-bur-meta">Cargando…</p></div>', { maxWidth: 300, autoPanPadding: [30, 30] })
580          .on('popupopen', function () { rellenarBurbuja(l, marcador); });
581        marcador.addTo(capaPines);
582        pines[l.id] = marcador;
583      });
584      Object.keys(pines).forEach(function (id) {
585        if (vivos[id]) return;
586        // El pin de la burbuja abierta sobrevive aunque el paneo lo saque del
587        // encuadre: quitarlo la cerraria. Al cambiar de filtro se cierra antes.
588        if (pines[id].isPopupOpen()) return;
589        capaPines.removeLayer(pines[id]);
590        delete pines[id];
591      });
592    }).catch(function () { /* un paneo fallido no rompe el mapa */ });
593  }
594
595  // Detalle de un lugar: quién entrena ahí y los datos que no vienen en el RPC.
596  // Se memoriza porque la burbuja y el panel piden lo mismo.
597  var cache = {};
598  function detalleDe(lugar) {
599    if (cache[lugar.id]) return Promise.resolve(cache[lugar.id]);
600    var ruta = '/rest/v1/entrenador_lugares?lugar_id=eq.' + encodeURIComponent(lugar.id)
601      + '&select=descripcion,created_at,entrenadores_publico!inner(slug,nombre,foto_url,titulo_perfil,especialidades,rating_promedio,total_reviews)'
602      + '&order=created_at.asc';
603    return Promise.all([
604      api(ruta).catch(function () { return []; }),
605      // El slug de la ficha es el del CENTRO, no el del lugar. Coinciden por
606      // casualidad en Mitolab y no tienen por qué: el lugar puede llamarse
607      // distinto o llevar sufijo si el slug estaba cogido.
608      api('/rest/v1/lugares?id=eq.' + encodeURIComponent(lugar.id) + '&select=direccion,centro_id,centros(slug,nombre)').catch(function () { return []; })
609    ]).then(function (res) {
610      var vinculos = (res[0] || []).filter(function (v) { return v.entrenadores_publico; });
611      // Orden por aportación (§8.1): primero quien ha descrito el sitio.
612      vinculos.sort(function (a, b) { return (a.descripcion ? 0 : 1) - (b.descripcion ? 0 : 1); });
613      var d = { vinculos: vinculos, extra: (res[1] || [])[0] || {} };
614      cache[lugar.id] = d;
615      return d;
616    });
617  }
618
619  // La burbuja: lo mínimo para decidir si te interesa, encima del mapa. Esto
620  // SOLO la rellena; de abrirla y cerrarla se encarga Leaflet con el manejador
621  // de clic que instala bindPopup.
622  //
623  // Sin autoClose ni closeOnClick: los dos estaban en false para que la burbuja
624  // dejara de cerrarse sola, pero eso no era un clic —era la recarga de pines
625  // borrando su marcador— y a cambio dejaban la burbuja pegada: no se cerraba
626  // ni pulsando fuera. Con los valores por defecto, tocas fuera y se cierra.
627  //
628  // Y no la abrimos nosotros: antes el clic del pin llamaba a openPopup() y de
629  // paso volvia a atar la burbuja con bindPopup. El manejador de Leaflet corria
630  // despues sobre ese mismo clic, veia la burbuja ya abierta y la cerraba —o
631  // sea que, una vez cerrada, el pin no la volvia a abrir nunca.
632  function rellenarBurbuja(lugar, marcador) {
633    history.replaceState(null, '', '#lugar=' + lugar.slug);
634
635    detalleDe(lugar).then(function (d) {
636      var meta = [SINGULAR[lugar.tipo] || lugar.tipo, d.extra.direccion].filter(
636Boolean).join(' · ');
637      var html = '<div class="rt-bur-cab"><h3>' + escapar(lugar.nombre) + '</h3>'
638        + '<p class="rt-bur-meta">' + escapar(meta) + '</p></div>';
639
640      // Un lugar puede ser un centro con ficha propia en /centros/[slug] y no
641      // tener todavía ningún entrenador vinculado — es el caso de Mitolab, el
642      // primero real. Sin este enlace la burbuja se quedaba sin salida: decía
643      // "todavía no hay entrenadores" y ahí moría, teniendo una ficha completa
644      // detrás.
645      var enlaceCentro = d.extra.centros
646        ? '<a class="rt-bur-btn" style="display:block;text-align:center;text-decoration:none;box-sizing:border-box;" target="_blank" rel="noopener" href="/centros/'
647          + escapar(d.extra.centros.slug) + '">Ver ficha del centro</a>'
648        : '';
649
650      if (!d.vinculos.length) {
651        html += '<div class="rt-bur-ents">'
652          + (enlaceCentro
653              ? enlaceCentro
654              : '<p class="rt-bur-meta">Todavía no hay entrenadores en este sitio.</p>')
655          + '</div>';
656      } else {
657        var tres = d.vinculos.slice(0, 3);
658        var nombres = tres.map(function (v) { return v.entrenadores_publico.nombre.trim(); });
659        var resto = d.vinculos.length - tres.length;
660        html += '<div class="rt-bur-ents">'
661          + '<p class="rt-bur-tit">Entrenadores que puedes encontrar aquí</p>'
662          + '<div class="rt-caras">'
663          + tres.map(function (v) {
664              var e = v.entrenadores_publico;
665              return e.foto_url
666                ? '<img class="rt-cara" src="' + escapar(e.foto_url) + '" alt="' + escapar(e.nombre) + '" loading="lazy" />'
667                : '<div class="rt-cara">' + escapar(iniciales(e.nombre)) + '</div>';
668            }).join('')
669          + '</div>'
670          + '<p class="rt-bur-nombres">' + escapar(nombres.join(', ')) + (resto > 0 ? ' y ' + plural(resto, 'más', 'más') : '') + '</p>'
671          + (d.vinculos.length === 1 && metaEntrenador(d.vinculos[0].entrenadores_publico)
672              ? '<p class="rt-bur-meta" style="margin:-6px 0 12px;">' + metaEntrenador(d.vinculos[0].entrenadores_publico) + '</p>'
673              : '')
674          + (d.vinculos.length === 1
675              ? '<a class="rt-bur-btn" style="display:block;text-align:center;text-decoration:none;" target="_blank" rel="noopener" href="/entrenador-personal/'
676                + escapar(d.vinculos[0].entrenadores_publico.slug) + '">Ver perfil del entrenador</a>'
677              : '<button class="rt-bur-btn" data-ver>Ver los ' + d.vinculos.length + ' entrenadores</button>')
678          + (enlaceCentro ? '<div style="margin-top:8px;">' + enlaceCentro.replace('rt-bur-btn', 'rt-bur-btn rt-bur-btn-sec') + '</div>' : '')
679          + '</div>';
680      }
681
682      marcador.setPopupContent(html);
683      var pop = marcador.getPopup().getElement();
684      var btn = pop && pop.querySelector('[data-ver]');
685      if (btn) btn.addEventListener('click', function () { abrirPanel(lugar, d); });
686    });
687  }
688
689  // El panel: el detalle completo y el contacto.
690  function abrirPanel(lugar, d) {
691    var panel = raiz.querySelector('#rt-de-panel');
692    panel.classList.add('abierto');
693    var meta = [SINGULAR[lugar.tipo] || lugar.tipo, d.extra.direccion].filter(Boolean).join(' · ');
694
695    var html = '<div class="rt-panel-cab">'
696      + '<button class="rt-cerrar" aria-label="Cerrar">&times;</button>'
697      + '<h2>' + escapar(lugar.nombre) + '</h2>'
698      + '<p class="rt-meta">' + escapar(meta) + '</p>'
699      + (d.extra.centros ? '<p style="margin:10px 0 0;"><a target="_blank" rel="noopener" href="/centros/' + escapar(d.extra.centros.slug) + '" style="font-size:12.5px;color:#089F3B;font-weight:600;">Ver ficha completa del centro</a></p>' : '')
700      + '</div>';
701
702    if (!d.vinculos.length) {
703      html += '<div class="rt-vacio">Todavía no hay entrenadores en este sitio.<br><br>'
704        + '<a class="btn" href="/encuentra-entrenador" style="display:inline-block;text-decoration:none;">Buscar entrenador</a></div>';
705    } else {
706      d.vinculos.forEach(function (v) {
707        var e = v.entrenadores_publico;
708        html += '<div class="rt-ent">'
709          + (e.foto_url
710              ? '<img class="rt-avatar" src="' + escapar(e.foto_url) + '" alt="" loading="lazy" />'
711              : '<div class="rt-avatar">' + escapar(iniciales(e.nombre)) + '</div>')
712          + '<div class="rt-ent-info">'
713          +   '<p class="rt-ent-nombre"><a target="_blank" rel="noopener" href="/entrenador-personal/' + escapar(e.slug) + '" style="color:inherit;text-decoration:none;">' + escapar(e.nombre) + '</a></p>'
714          +   (metaEntrenador(e) ? '<p class="rt-ent-meta">' + metaEntrenador(e) + '</p>' : '')
715          +   (e.titulo_perfil ? '<p class="rt-meta">' + escapar(e.titulo_perfil) + '</p>' : '')
716          +   (v.descripcion ? '<p class="rt-ent-desc">' + escapar(v.descripcion) + '</p>' : '')
717          +   '<a class="btn btn-peq" style="display:inline-block;text-decoration:none;" target="_blank" rel="noopener" href="/entrenador-personal/' + escapar(e.slug) + '">Ver perfil del entrenador</a>'
718          + '</div></div>';
719      });
720    }
721
722    panel.innerHTML = html;
723    panel.querySelector('.rt-cerrar').addEventListener('click', cerrarPanel);
724  }
725
726  function cerrarPanel() {
727    raiz.querySelector('#rt-de-panel').classList.remove('abierto');
728  }
729
730  // Deep link: /donde-entrenar#lugar=parque-del-retiro abre ese sitio (§6.3).
731  function abrirLugarPorSlug(slug) {
732    api('/rest/v1/lugares?slug=eq.' + encodeURIComponent(slug) + '&select=id,nombre,slug,familia,tipo,lat,lng&limit=1')
733      .then(function (filas) {
734        var l = (filas || [])[0];
735        if (!l) return;
736        mapa.setView([l.lat, l.lng], 15);
737        detalleDe(l).then(function (d) { abrirPanel(l, d); });
738      }).catch(function () {});
739  }
740
741  function abrirDesdeHash() {
742    var m = (location.hash || '').match(/lugar=([a-z0-9-]+)/i);
743    if (m) abrirLugarPorSlug(m[1]);
744  }
745
746  function irAlMapa() {
747    var lienzo = raiz.querySelector('.rt-lienzo');
748    if (lienzo) lienzo.scrollIntoView({ behavior: 'smooth', block: 'center' });
749  }
750
751  conectarFiltros();
752
753  cargarLeaflet().then(function (L) {
754    mapa = L.map('rt-de-mapa').setView(CENTRO_ESPANA, 6);
755    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
756      attribution: '© OpenStreetMap', maxZoom: 19
757    }).addTo(mapa);
758    capaPines = L.layerGroup().addTo(mapa);
759
760    // Sin espera, arrastrar el mapa lanzaría decenas de consultas.
761    mapa.on('moveend', function () {
762      clearTimeout(temporizador);
763      temporizador = setTimeout(cargarPines, 250);
764    });
765
766    // Si el contenedor todavía no tiene ancho cuando Leaflet arranca —un embed
767    // dentro de una pestaña, un acordeón o una sección que se dimensiona
768    // tarde—, el mapa nace de 0px: `getBounds()` devuelve una caja degenerada,
769    // la consulta no trae nada y no se pinta ni un pin. Se vigila el tamaño y,
770    // en cuanto cambia de verdad, se recalcula y se vuelven a pedir.
771    var anchoPrevio = 0;
772    function ajustar() {
773      var ancho = raiz.querySelector('#rt-de-mapa').clientWidth;
774      if (!ancho || ancho === anchoPrevio) return;
775      anchoPrevio = ancho;
776      mapa.invalidateSize();
777      cargarPines();
778    }
779
780    if (window.ResizeObserver) {
781      new ResizeObserver(ajustar).observe(raiz.querySelector('#rt-de-mapa'));
782    }
783    window.addEventListener('resize', ajustar);
784    setTimeout(ajustar, 300);
785
786    cargarPines();
787    abrirDesdeHash();
788
789    // La lista de abajo enlaza a #lugar=..., asi que un clic ahi tiene que
790    // abrir el sitio en el mapa y subir hasta el. abrirDesdeHash solo corria al
791    // cargar; sin esto los enlaces de la lista no harian nada visible.
792    window.addEventListener('hashchange', function () {
793      irAlMapa();
794      abrirDesdeHash();
795    });
796
797    // Y ademas hay que escuchar el clic, porque hay un caso en el que el
798    // navegador NO dispara hashchange: cuando el hash ya apunta a ese mismo
799    // sitio. Pasa constantemente —abrir un pin deja el hash puesto— y entonces
800    // su enlace de la lista no hacia absolutamente nada. Si el hash es otro no
801    // tocamos nada: la navegacion normal dispara hashchange y guarda su entrada
802    // en el historial, para que el boton Atras siga sirviendo.
803    document.addEventListener('click', function (e) {
804      var a = e.target && e.target.closest && e.target.closest('a[href*="#lugar="]');
805      if (!a) return;
806      var m = (a.getAttribute('href') || '').match(/#lugar=([a-z0-9-]+)/i);
807      if (!m || location.hash !== '#lugar=' + m[1]) return;
808      irAlMapa();
809      abrirLugarPorSlug(m[1]);
810    });
811  }).catch(function () {
812    raiz.querySelector('#rt-de-mapa').innerHTML =
813      '<p style="padding:24px;color:#791F1F;">No se ha podido cargar el mapa. Recarga la página.</p>';
814  });
815})();
816</script>
816
817</div><div class="w-embed w-script"><!-- RedEntrenadores · Glue del mapa: arrancar con un filtro puesto.
818
819     Va DEBAJO del embed del mapa, dentro del mismo componente. Deja el mapa
820     con una familia y un tipo seleccionados, como si los hubiera pulsado el
821     usuario. El filtro sale, por este orden:
822
823       1. Un data-tipo o un data-familia en el contenedor del mapa o en
824          cualquier ancestro suyo. Es como lo piden las páginas de tipo:
825          <div data-tipo="calistenia"> alrededor de la instancia.
826       2. El hash de la URL: #tipo=calistenia o #familia=publico. Es como
827          llegan los enlaces desde fuera.
828
829     Por qué un glue y no una opción dentro del mapa: el embed del mapa son
830     30 KB que solo se suben pegándolos (la API no escribe embeds), así que
831     cada cambio ahí cuesta una pegada a mano. Esto son cuarenta líneas que se
832     escriben por API y no lo tocan. Mismo patrón que
833     widget-listado-ciudad-glue.html con el listado compartido.
834
835     No inventa estado: pulsa los propios botones del mapa, así que la interfaz
836     queda coherente —las píldoras salen marcadas— y si el mapa cambiara sus
837     filtros, esto deja de encontrar los botones y no hace nada. Degrada a
838     "mapa sin filtrar", nunca a mapa roto.
839
840     Tipos válidos, los de la spec §4.1: gimnasio, estudio-box,
841     polideportivo-instalacion, parque, calistenia, pista-publica,
842     ruta-sendero, playa. -->
843
844<script>
845(function () {
846  var FAMILIA_DE = {
847    'gimnasio': 'centro', 'estudio-box': 'centro', 'polideportivo-instalacion': 'centro',
848    'parque': 'publico', 'calistenia': 'publico', 'pista-publica': 'publico',
849    'ruta-sendero': 'publico', 'playa': 'publico'
850  };
851
852  // rt-donde-entrenar es el id que tenía el mapa cuando vivía pegado al hub.
853  // Se sigue mirando por si queda alguna página sin migrar al componente.
854  function raizMapa() {
855    return document.getElementById('rt-mapa-lugares') ||
856           document.getElementById('rt-donde-entrenar');
857  }
858
859  function normaliza(tipo, familia) {
860    if (tipo && FAMILIA_DE[tipo]) return { familia: FAMILIA_DE[tipo], tipo: tipo };
861    if (familia === 'centro' || familia === 'publico') return { familia: familia, tipo: null };
862    return null;
863  }
864
865  function delContenedor(raiz) {
866    var el = raiz && raiz.closest('[data-tipo],[data-familia]');
867    if (!el) return null;
868    return normaliza(el.getAttribute('data-tipo'), el.getAttribute('data-familia'));
869  }
870
871  function delHash() {
872    var h = location.hash || '';
873    var t = h.match(/tipo=([a-z-]+)/i);
874    var f = h.match(/familia=(centro|publico)/i);
875    return normaliza(t && t[1], f && f[1].toLowerCase());
876  }
877
878  // El mapa monta sus escuchas al parsearse (conectarFiltros corre en línea),
879  // pero este embed puede ejecutarse antes de que el navegador haya terminado
880  // con el suyo. Se reintenta un par de segundos y se abandona en silencio.
881  function aplicar(pedido, intento) {
882    var raiz = raizMapa();
883    var fam = raiz && raiz.querySelector('#rt-de-fam [data-familia="' + pedido.familia + '"]');
884    if (fam) {
885      if (!fam.classList.contains('sel')) fam.click();
886      if (!pedido.tipo) return;
887      // pintarTipos() corre dentro del clic anterior, así que la píldora del
888      // tipo ya existe en este punto.
889      var chip = raiz.querySelector('#rt-de-tipos [data-tipo="' + pedido.tipo + '"]');
890      if (chip) {
891        if (!chip.classList.contains('sel')) chip.click();
892        return;
893      }
894    }
895    if (intento < 20) setTimeout(function () { aplicar(pedido, intento + 1); }, 100);
896  }
897
898  function arrancar() {
899    var pedido = delHash() || delContenedor(raizMapa());
900    if (pedido) aplicar(pedido, 0);
901  }
902
903  window.addEventListener('hashchange', arrancar);
904  if (document.readyState === 'loading') {
905    document.addEventListener('DOMContentLoaded', arrancar);
906  } else {
907    arrancar();
908  }
909})();
910</script>
910
911</div></div><div class="w-embed"><!-- RedEntrenadores · Embed de la lista de lugares de /donde-entrenar (spec §6.1).
912     Va DEBAJO del embed del mapa, en la misma página. Son dos embeds a
913     propósito: el mapa se edita a mano y esto lo regenera un script.
914
915     Por qué existe: el mapa lo pinta Leaflet leyendo Supabase, y un pin no es
916     un enlace. Sin esta lista la página no enlaza a ninguna otra del sitio
917     —los enlaces a fichas y centros solo nacen al pulsar un pin, o sea nunca
918     para un rastreador— y encima no tiene texto que Google pueda leer.
919
920     Lo que hay entre LISTA:INICIO y LISTA:FIN lo reescribe entero
921     `node scripts/generar-lista-lugares.js`. Todo lo de fuera de las marcas
922     —este comentario, el H2, la entradilla y el CSS— se edita a mano y el
923     script no lo toca.
924
925     El CSS cuelga de .rt-lista, no de #rt-donde-entrenar: este embed está
926     fuera del contenedor del mapa. -->
927
928<section class="rt-lista">
929  <h2>Todos los sitios donde entrenar</h2>
930  <p class="rt-lista-pie">Estos son los lugares que los entrenadores de RedEntrenadores han añadido al mapa. Pulsa cualquiera para verlo y descubrir quién entrena allí.</p>
931<!-- LISTA:INICIO -->
932    <div class="rt-lista-ciudad">
933      <h3><a href="/entrenadores-personales/madrid">Dónde entrenar en Madrid</a></h3>
934      <p class="rt-lista-intro">37 sitios donde entrenan entrenadores personales en Madrid y alrededores.</p>
935      <p class="rt-lista-fam">Al aire libre</p>
936      <ul>
937        <li><a href="/donde-entrenar#lugar=madrid-rio">Madrid Río</a> <span class="rt-lista-tipo">· Parque · Arganzuela</span> — entrenan aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a> y <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li>
938        <li><a href="/donde-entrenar#lugar=parque-2">Parque</a> <span class="rt-lista-tipo">· Parque</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
939        <li><a href="/donde-entrenar#lugar=parque-calero">Parque Calero</a> <span class="rt-lista-tipo">· Parque · Ciudad Lineal · Calle de José del Hierro</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
940        <li><a href="/donde-entrenar#lugar=parque-de-las-siete-tetas">Parque Cerro del Tío Pío (Siete Tetas)</a> <span class="rt-lista-tipo">· Parque · Vallecas · Calle de Ramón Pérez de Ayala, Puente de Vallecas</span> — entrenan aquí <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a> y <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
941        <li><a href="/donde-entrenar#lugar=parque-calistenia-arturo-soria">Parque de calistenia de Arturo Soria</a> <span class="rt-lista-tipo">· Calistenia · Ciudad Lineal · Arturo Soria-Gran Vía de Hortaleza, Calle de Arturo Soria</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
942        <li><a href="/donde-entrenar#lugar=parque-de-la-gavia">Parque de La Gavia</a> <span class="rt-lista-tipo">· Parque · Vallecas · Ensanche de Vallecas</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
943        <li><a href="/donde-entrenar#lugar=parque-del-oeste">Parque del Oeste</a> <span class="rt-lista-tipo">· Parque · Moncloa-Aravaca</span> — entrena aquí <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li>
944        <li><a href="/donde-entrenar#lugar=parque-del-retiro">Parque del Retiro</a> <span class="rt-lista-tipo">· Parque · Retiro</span> — entrenan aquí <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a> y <a href="/entrenador-personal/ines-narvaez-madrid">Inés Narváez</a></li>
945        <li><a href="/donde-entrenar#lugar=parque-juan-carlos-i">Parque Juan Carlos I</a> <span class="rt-lista-tipo">· Parque · Juan de Borbón-Ribera del Sena, Glorieta Juan de Borbón y Battenberg</span> — entrena aquí <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li>
946        <li><a href="/donde-entrenar#lugar=pinar-del-rey">Pinar del Rey</a> <span class="rt-lista-tipo">· Parque · Hortaleza · Avenida de la Gran Vía de Hortaleza</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
947      </ul>
948      <p class="rt-lista-fam">
948Gimnasios y centros</p>
949      <ul>
950        <li><a href="/donde-entrenar#lugar=basic-fit-calle-de-orense">Basic-fit calle de Orense</a> <span class="rt-lista-tipo">· Gimnasio</span> — entrena aquí <a href="/entrenador-personal/hugo-arias-madrid">Hugo Arias</a></li>
951        <li><a href="/donde-entrenar#lugar=basic-fit-calle-sor-angela">Basic-fit calle Sor Ángela</a> <span class="rt-lista-tipo">· Gimnasio · Centro · Basic-fit Sor Ángela</span> — entrenan aquí <a href="/entrenador-personal/hugo-arias-madrid">Hugo Arias</a> y <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
952        <li><a href="/donde-entrenar#lugar=basic-fit-vallecas">Basic-Fit Vallecas</a> <span class="rt-lista-tipo">· Gimnasio · Vallecas · Avenida Albufera 6</span> — entrena aquí <a href="/entrenador-personal/aitor-cuesta-madrid">Aitor Cuesta</a></li>
953        <li><a href="/donde-entrenar#lugar=ciclismo-y-rendimiento">Ciclismo y Rendimiento</a> <span class="rt-lista-tipo">· Estudio o box · Hortaleza · Calle de Agustín de Iturbide, 24</span> · <a href="/centros/ciclismo-y-rendimiento">ficha del centro</a></li>
954        <li><a href="/donde-entrenar#lugar=fitup-serrano">Fitup Serrano</a> <span class="rt-lista-tipo">· Gimnasio · Salamanca · Calle Serrano, 50</span> — entrenan aquí <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a>, <a href="/entrenador-personal/aitor-cuesta-madrid">Aitor Cuesta</a> y <a href="/entrenador-personal/laura-saez-culturismo-online">Laura Sáez</a> · <a href="/centros/fitup-serrano">ficha del centro</a></li>
955        <li><a href="/donde-entrenar#lugar=gimnasio-crom">Gimnasio Crom</a> <span class="rt-lista-tipo">· Gimnasio · Ciudad Lineal · Calle de Gregorio Donas, 10</span> — entrena aquí <a href="/entrenador-personal/rodrigo-bermejo">Rodrigo Bermejo</a></li>
956        <li><a href="/donde-entrenar#lugar=idm-basica-carril-del-conde">Instalación Deportiva Municipal Básica Carril del Conde</a> <span class="rt-lista-tipo">· Polideportivo · Hortaleza · Calle del Carril del Conde, Canillas</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
957        <li><a href="/donde-entrenar#lugar=idm-canillas">Instalación Deportiva Municipal Canillas</a> <span class="rt-lista-tipo">· Polideportivo · Hortaleza · Calle de Agustín de Iturbide, 1</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
958        <li><a href="/donde-entrenar#lugar=polideportivo">Polideportivo</a> <span class="rt-lista-tipo">· Polideportivo · Centro</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
959        <li><a href="/donde-entrenar#lugar=polideportivo-municipal-casa-de-campo">Polideportivo Municipal Casa de Campo</a> <span class="rt-lista-tipo">· Polideportivo · Moncloa-Aravaca · Ronda del Lago</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
960        <li><a href="/donde-entrenar#lugar=polideportivo-san-juan-bautista">Polideportivo San Juan Bautista</a> <span class="rt-lista-tipo">· Polideportivo · Ciudad Lineal · Calle de Ramírez de Arellano, San Juan Bautista</span> — entrena aquí <a href="/entrenador-personal/armando-alonso-madrid">Armando Alonso</a></li>
961        <li><a href="/donde-entrenar#lugar=polideportivo-vicalvaro">Polideportivo Vicálvaro</a> <span class="rt-lista-tipo">· Polideportivo · Paseo del Polideportivo, Casco Histórico de Vicálvaro</span> — entrena aquí <a href="/entrenador-personal/alberto-ullner-madrid">Alberto Ullner</a></li>
962        <li><a href="/donde-entrenar#lugar=vibra-bienestar">Vibra Bienestar</a> <span class="rt-lista-tipo">· Gimnasio · Calle Jaime Hermida 9</span> — entrena aquí <a href="/entrenador-personal/gabri-saez-madrid">Gabri Sáez</a></li>
963        <li><a href="/donde-entrenar#lugar=wellzone-las-tablas">Wellzone Las Tablas</a> <span class="rt-lista-tipo">· Gimnasio · Calle de María Tubau, 5</span> · <a href="/centros/wellzone-las-tablas">ficha del centro</a></li>
964        <li><a href="/donde-entrenar#lugar=wezone-almagro">Wezone Almagro</a> <span class="rt-lista-tipo">· Estudio o box · Chamberí · Calle de Zurbano, 56</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
965        <li><a href="/donde-entrenar#lugar=wezone-arturo-soria">Wezone Arturo Soria</a> <span class="rt-lista-tipo">· Estudio o box · Ciudad Lineal · Calle de Arturo Soria, 195</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
966        <li><a href="/donde-entrenar#lugar=wezone-barrio-salamanca">Wezone Barrio Salamanca</a> <span class="rt-lista-tipo">· Estudio o box · Salamanca · Calle de Diego de León, 31</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
967        <li><a href="/donde-entrenar#lugar=wezone-bernabeu">Wezone Bernabéu</a> <span class="rt-lista-tipo">· Estudio o box · Chamartín · Calle de Víctor Andrés Belaúnde, 25</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
968        <li><a href="/donde-entrenar#lugar=wezone-chamberi">Wezone Chamberí</a> <span class="rt-lista-tipo">· Estudio o box · Chamberí · Calle de Vallehermoso, 27</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
969        <li><a href="/donde-entrenar#lugar=wezone-conde-duque">Wezone Conde Duque</a> <span class="rt-lista-tipo">· Estudio o box · Centro · Calle de Alberto Aguilera, 15</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
970        <li><a href="/donde-entrenar#lugar=wezone-embajadores">Wezone Embajadores</a> <span class="rt-lista-tipo">· Estudio o box · Arganzuela · Calle de Martín Soler, 8</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
971        <li><a href="/donde-entrenar#lugar=wezone-pacifico">Wezone Pacífico</a> <span class="rt-lista-tipo">· Estudio o box · Retiro · Calle de Andalucía, 6</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
972        <li><a href="/donde-entrenar#lugar=wezone-plaza-castilla">Wezone Plaza Castilla</a> <span class="rt-lista-tipo">· Estudio o box · Tetuán · Calle de Bravo Murillo, 309</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
973        <li><a href="/donde-entrenar#lugar=wezone-ventas">Wezone Ventas</a> <span class="rt-lista-tipo">· Estudio o box · Salamanca · Calle de Azcona, 44</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
974        <li><a href="/donde-entrenar#lugar=wifit-guindalera">Wifit Guindalera</a> <span class="rt-lista-tipo">· Gimnasio · Salamanca · Calle de Cartagena, 16</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
975        <li><a href="/donde-entrenar#lugar=wifit-moratalaz">Wifit Moratalaz</a> <span class="rt-lista-tipo">· Gimnasio · Avenida de Moratalaz, 181</span> — entrena aquí <a href="/entrenador-personal/mario-fernandez-madrid">Mario Fernández</a></li>
976      </ul>
977      <p class="rt-lista-fam">Alrededores · Las Rozas de Madrid</p>
978      <ul>
979        <li><a href="/donde-entrenar#lugar=cep-las-rozas">CEP Las Rozas</a> <span class="rt-lista-tipo">· Estudio o box · Calle Módena, 25</span> · <a href="/centros/cep-las-rozas">ficha del centro</a></li>
980      </ul>
981      <p class="rt-lista-barrios"><strong>Barrios de Madrid con sitios para entrenar:</strong> Arganzuela, Centro, Chamartín, Chamberí, Ciudad Lineal, Hortaleza, Moncloa-Aravaca, Retiro, Salamanca, Tetuán, V
981allecas.</p>
982    </div>
983    <div class="rt-lista-ciudad">
984      <h3><a href="/entrenadores-personales/malaga">Dónde entrenar en Málaga</a></h3>
985      <p class="rt-lista-intro">8 sitios donde entrenan entrenadores personales en Málaga.</p>
986      <p class="rt-lista-fam">Al aire libre</p>
987      <ul>
988        <li><a href="/donde-entrenar#lugar=joaquin-ramirez-entrenador-personal">Joaquín Ramírez Entrenador Personal</a> <span class="rt-lista-tipo">· Calistenia · Málaga Este</span> — entrena aquí <a href="/entrenador-personal/joaquin-ramirez-malaga">Joaquín Ramírez</a></li>
989        <li><a href="/donde-entrenar#lugar=outdoor-workout-parque-de-santa-ana-teatinos-universidad">Outdoor Workout - Parque de Santa Ana - Teatinos-Universidad</a> <span class="rt-lista-tipo">· Calistenia · Teatinos · Avenida Diego Fernández de Mendoza</span> — entrena aquí <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li>
990        <li><a href="/donde-entrenar#lugar=outdoor-workout-parque-virgen-de-las-canas-los-almendros">Outdoor Workout - Parque Virgen de las Cañas - Los Almendros</a> <span class="rt-lista-tipo">· Parque · Puerto de la Torre · Avenida Andersen</span> — entrena aquí <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li>
991        <li><a href="/donde-entrenar#lugar=parque-calistenia-one-piece">Parque Calistenia &quot;One Piece&quot;</a> <span class="rt-lista-tipo">· Calistenia · Puerto de la Torre · Lope de Vega - Hacienda Cabello, Avenida Lope de Vega</span> — entrena aquí <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li>
992        <li><a href="/donde-entrenar#lugar=parque-deportivo-work-out-teatinos">Parque Deportivo Work Out Teatinos</a> <span class="rt-lista-tipo">· Calistenia · Teatinos · Calle Antonio Raíz 27</span> — entrena aquí <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li>
993        <li><a href="/donde-entrenar#lugar=parque-forestal-de-la-laguna-de-la-barrera-maria-eugenia-can">Parque Forestal de la Laguna de la Barrera &quot;María Eugenia Candau Rámila&quot;</a> <span class="rt-lista-tipo">· Parque · Teatinos · Laguna de la Barrera</span> — entrena aquí <a href="/entrenador-personal/florian-taillandier-malaga">Florian Taillandier</a></li>
994      </ul>
995      <p class="rt-lista-fam">Gimnasios y centros</p>
996      <ul>
997        <li><a href="/donde-entrenar#lugar=on-air">On Air</a> <span class="rt-lista-tipo">· Gimnasio · Centro · Calle Armengual de la Mota 39</span> — entrena aquí <a href="/entrenador-personal/antonio-porras-malaga">Antonio Porras</a></li>
998        <li><a href="/donde-entrenar#lugar=vals-sport-ave-maria">Vals Sport Ave María</a> <span class="rt-lista-tipo">· Gimnasio · Carretera de Cádiz · Avenida Sor Teresa Prat, 51</span> — entrena aquí <a href="/entrenador-personal/sergio-bermudez-malaga">Sergio Bermúdez</a></li>
999      </ul>
1000      <p class="rt-lista-barrios"><strong>Barrios de Málaga con sitios para entrenar:</strong> Carretera de Cádiz, Centro, Málaga Este, Puerto de la Torre, Teatinos.</p>
1001    </div>
1002    <div class="rt-lista-ciudad">
1003      <h3><a href="/entrenadores-personales/barcelona">Dónde entrenar en Barcelona</a></h3>
1004      <p class="rt-lista-intro">7 sitios donde entrenan entrenadores personales en Barcelona y alrededores.</p>
1005      <p class="rt-lista-fam">Al aire libre</p>
1006      <ul>
1007        <li><a href="/donde-entrenar#lugar=montjuic">Montjuïc</a> <span class="rt-lista-tipo">· Parque · Sants-Montjuïc</span> — entrena aquí <a href="/entrenador-personal/jesus-manuel-huamani-salazar">Jesus Manuel Huamani</a></li>
1008        <li><a href="/donde-entrenar#lugar=parc-de-la-ciutadella">Parc de la Ciutadella</a> <span class="rt-lista-tipo">· Parque · Ciutat Vella</span> — entrena aquí <a href="/entrenador-personal/jesus-manuel-huamani-salazar">Jesus Manuel Huamani</a></li>
1009        <li><a href="/donde-entrenar#lugar=parc-del-diagonal-mar">Parc del Diagonal Mar</a> <span class="rt-lista-tipo">· Parque · Sant Martí</span> — entrena aquí <a href="/entrenador-personal/jesus-manuel-huamani-salazar">Jesus Manuel Huamani</a></li>
1010        <li><a href="/donde-entrenar#lugar=parque-calistenia-puerto">Parque Calistenia Puerto</a> <span class="rt-lista-tipo">· Calistenia · Sant Martí · Moll de Mestral</span> — entrena aquí <a href="/entrenador-personal/nabil-darwish-barcelona">Nabil Darwish</a></li>
1011      </ul>
1012      <p class="rt-lista-fam">
1012Gimnasios y centros</p>
1013      <ul>
1014        <li><a href="/donde-entrenar#lugar=kenko-training">Kenko Training</a> <span class="rt-lista-tipo">· Estudio o box · Sant Martí · Carrer d'Edison 12</span></li>
1015        <li><a href="/donde-entrenar#lugar=welfare">Welfare</a> <span class="rt-lista-tipo">· Gimnasio · Sarrià-Sant Gervasi · Calle Santalo 15</span> — entrena aquí <a href="/entrenador-personal/raul-casillas-barcelona">Raúl Casillas</a></li>
1016      </ul>
1017      <p class="rt-lista-fam">Alrededores · Premià de Mar</p>
1018      <ul>
1019        <li><a href="/donde-entrenar#lugar=parque-de-calistenia-premia-de-mar">Parque de calistenia de Premià de Mar</a> <span class="rt-lista-tipo">· Calistenia · Carrer de Núria</span> — entrenan aquí <a href="/entrenador-personal/gonzalo-cruz-barcelona">Gonzalo Cruz</a> y <a href="/entrenador-personal/aron-barcelona">Aron</a></li>
1020      </ul>
1021      <p class="rt-lista-barrios"><strong>Barrios de Barcelona con sitios para entrenar:</strong> Ciutat Vella, Sant Martí, Sants-Montjuïc, Sarrià-Sant Gervasi.</p>
1022    </div>
1023    <div class="rt-lista-ciudad">
1024      <h3><a href="/entrenadores-personales/alicante">Dónde entrenar en Alicante</a></h3>
1025      <p class="rt-lista-intro">2 sitios donde entrenan entrenadores personales en Alicante.</p>
1026      <p class="rt-lista-fam">Gimnasios y centros</p>
1027      <ul>
1028        <li><a href="/donde-entrenar#lugar=fitness-park-luceros">Fitness Park Luceros</a> <span class="rt-lista-tipo">· Gimnasio · Avinguda de l'Estació, 5</span> — entrenan aquí <a href="/entrenador-personal/pablo-perez">Pablo Pérez</a> y <a href="/entrenador-personal/monica-minguez-alicante">Monica Minguez</a></li>
1029        <li><a href="/donde-entrenar#lugar=fitness-park-san-vicente-del-raspeig">Fitness Park San Vicente del Raspeig</a> <span class="rt-lista-tipo">· Gimnasio · Carrer Vicent Savall Pascual, 21, 03690 San Vicente del Raspeig</span> — entrenan aquí <a href="/entrenador-personal/pablo-perez">Pablo Pérez</a> y <a href="/entrenador-personal/monica-minguez-alicante">Monica Minguez</a></li>
1030      </ul>
1031    </div>
1032    <div class="rt-lista-ciudad">
1033      <h3><a href="/entrenadores-personales/estepona">Dónde entrenar en Estepona</a></h3>
1034      <p class="rt-lista-intro">2 sitios donde entrenan entrenadores personales en Estepona.</p>
1035      <p class="rt-lista-fam">Gimnasios y centros</p>
1036      <ul>
1037        <li><a href="/donde-entrenar#lugar=centro-fitness-la-academia">Centro Fitness La Academia</a> <span class="rt-lista-tipo">· Gimnasio · Calle La Viña</span> — entrena aquí <a href="/entrenador-personal/ivan-rivero-estepona">Ivan Rivero</a></li>
1038        <li><a href="/donde-entrenar#lugar=fitbike-estepona">Fitbike Estepona</a> <span class="rt-lista-tipo">· Gimnasio</span> — entrena aquí <a href="/entrenador-personal/ivan-rivero-estepona">Ivan Rivero</a></li>
1039      </ul>
1040    </div>
1041    <div class="rt-lista-ciudad">
1042      <h3><a href="/entrenadores-personales/a-coruna">Dónde entrenar en A Coruña</a></h3>
1043      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en A Coruña.</p>
1044      <p class="rt-lista-fam">Gimnasios y centros</p>
1045      <ul>
1046        <li><a href="/donde-entrenar#lugar=pilar-fernandez-entrenamiento-personal">Pilar Fernández — Entrenamiento Personal</a> <span class="rt-lista-tipo">· Estudio o box · Agra do Orzán, 38, 1D</span> — entrena aquí <a href="/entrenador-personal/pilar-fernandez-a-coruna">Pilar Fernández</a></li>
1047      </ul>
1048    </div>
1049    <div class="rt-lista-ciudad">
1050      <h3>Dónde entrenar en Albelda de Iregua</h3>
1051      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Albelda de Iregua.</p>
1052      <p class="rt-lista-fam">Gimnasios y centros</p>
1053      <ul>
1054        <li><a href="/donde-entrenar#lugar=recovery-training-lesion">Recovery Training Lesion</a> <span class="rt-lista-tipo">· Estudio o box · 26120 La Rioja</span> — entrena aquí <a href="/entrenador-personal/recovery-training-lesion-online">Recovery Training Lesion</a> · <a href="/centros/recovery-training-lesion">ficha del centro</a></li>
1055      </ul>
1056    </div>
1057    <div class="rt-lista-ciudad">
1058      <h3><a href="/entrenadores-personales/alhaurin-de-la-torre">Dónde entrenar en Alhaurín de la Torre</a></h3>
1059      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Alhaurín de la Torre.</p>
1060      <p class="rt-lista-fam">
1060Gimnasios y centros</p>
1061      <ul>
1062        <li><a href="/donde-entrenar#lugar=elite-wellness-alhaurin">Elite Wellness Alhaurín</a> <span class="rt-lista-tipo">· Gimnasio · Avenida 9, Polígono Industrial Fase II, Nave 73</span> — entrena aquí <a href="/entrenador-personal/antonio-porras-malaga">Antonio Porras</a></li>
1063      </ul>
1064    </div>
1065    <div class="rt-lista-ciudad">
1066      <h3><a href="/entrenadores-personales/almeria">Dónde entrenar en Almería</a></h3>
1067      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Almería.</p>
1068      <p class="rt-lista-fam">Gimnasios y centros</p>
1069      <ul>
1070        <li><a href="/donde-entrenar#lugar=basic-fit-almeria">Basic-Fit Almería Federico García Lorca</a> <span class="rt-lista-tipo">· Gimnasio · Avenida Federico García Lorca, 240</span> — entrena aquí <a href="/entrenador-personal/brayan-esteban-atuesta-almeria">Brayan Esteban Atuesta</a></li>
1071      </ul>
1072    </div>
1073    <div class="rt-lista-ciudad">
1074      <h3>Dónde entrenar en Basauri</h3>
1075      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Basauri.</p>
1076      <p class="rt-lista-fam">Gimnasios y centros</p>
1077      <ul>
1078        <li><a href="/donde-entrenar#lugar=polideportivo-de-basauri">Polideportivo de Basauri</a> <span class="rt-lista-tipo">· Polideportivo · Artunduaga kalea</span> — entrena aquí <a href="/entrenador-personal/aingeru-hernandez-bilbao">Aingeru Hernández</a></li>
1079      </ul>
1080    </div>
1081    <div class="rt-lista-ciudad">
1082      <h3><a href="/entrenadores-personales/cantillana">Dónde entrenar en Cantillana</a></h3>
1083      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Cantillana.</p>
1084      <p class="rt-lista-fam">Gimnasios y centros</p>
1085      <ul>
1086        <li><a href="/donde-entrenar#lugar=tocasport-cantillana">Tocasport Cantillana</a> <span class="rt-lista-tipo">· Gimnasio · Carretera de Lora, 33</span> — entrena aquí <a href="/entrenador-personal/jose-angel-perez-sevilla">José Ángel Pérez</a></li>
1087      </ul>
1088    </div>
1089    <div class="rt-lista-ciudad">
1090      <h3><a href="/entrenadores-personales/cordoba">Dónde entrenar en Córdoba</a></h3>
1091      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Córdoba.</p>
1092      <p class="rt-lista-fam">Gimnasios y centros</p>
1093      <ul>
1094        <li><a href="/donde-entrenar#lugar=gimnasio-el-euro">Gimnasio El Euro</a> <span class="rt-lista-tipo">· Gimnasio · Calle Músico Ziryab</span> — entrena aquí <a href="/entrenador-personal/oscar-herman-cordoba">Oscar Herman</a></li>
1095      </ul>
1096    </div>
1097    <div class="rt-lista-ciudad">
1098      <h3><a href="/entrenadores-personales/elche">Dónde entrenar en Elche</a></h3>
1099      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Elche.</p>
1100      <p class="rt-lista-fam">Gimnasios y centros</p>
1101      <ul>
1102        <li><a href="/donde-entrenar#lugar=fitness-park-elche-aljub">Fitness Park Elche Aljub</a> <span class="rt-lista-tipo">· Gimnasio · Carrer Jacarilla, 7 (C.C. L'Aljub)</span> — entrena aquí <a href="/entrenador-personal/monica-minguez-alicante">Monica Minguez</a></li>
1103      </ul>
1104    </div>
1105    <div class="rt-lista-ciudad">
1106      <h3><a href="/entrenadores-personales/llinars-del-valles">Dónde entrenar en Llinars del Vallès</a></h3>
1107      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Llinars del Vallès.</p>
1108      <p class="rt-lista-fam">Gimnasios y centros</p>
1109      <ul>
1110        <li><a href="/donde-entrenar#lugar=bodybuilding-center-llinars">Bodybuilding Center Llinars</a> <span class="rt-lista-tipo">· Gimnasio · Carrer de la Tecnologia, Polígon Industrial el Mogent</span> — entrena aquí <a href="/entrenador-personal/albert-fradera-riells">Albert Fradera</a></li>
1111      </ul>
1112    </div>
1113    <div class="rt-lista-ciudad">
1114      <h3><a href="/entrenadores-personales/murcia">Dónde entrenar en Murcia</a></h3>
1115      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Murcia.</p>
1116      <p class="rt-lista-fam">
1116Gimnasios y centros</p>
1117      <ul>
1118        <li><a href="/donde-entrenar#lugar=amador-ruiz-murcia">Amador Ruiz — Entrenamiento Personal</a> <span class="rt-lista-tipo">· Gimnasio · Calle Cisne, 3</span> — entrena aquí <a href="/entrenador-personal/amador-ruiz-murcia">Amador Ruiz</a> · <a href="/centros/amador-ruiz-murcia">ficha del centro</a></li>
1119      </ul>
1120    </div>
1121    <div class="rt-lista-ciudad">
1122      <h3><a href="/entrenadores-personales/oviedo">Dónde entrenar en Oviedo</a></h3>
1123      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Oviedo.</p>
1124      <p class="rt-lista-fam">Al aire libre</p>
1125      <ul>
1126        <li><a href="/donde-entrenar#lugar=parque-del-oeste-oviedo">Parque del Oeste</a> <span class="rt-lista-tipo">· Parque · Olivares</span> — entrena aquí <a href="/entrenador-personal/ivan-patallo-oviedo">Ivan Patallo</a></li>
1127      </ul>
1128    </div>
1129    <div class="rt-lista-ciudad">
1130      <h3>Dónde entrenar en Santovenia de Pisuerga</h3>
1131      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Santovenia de Pisuerga.</p>
1132      <p class="rt-lista-fam">Al aire libre</p>
1133      <ul>
1134        <li><a href="/donde-entrenar#lugar=parque-remuel">Parque Remuel</a> <span class="rt-lista-tipo">· Calistenia · Calle Real</span> — entrena aquí <a href="/entrenador-personal/adrian-garcia-valladolid">Adrian Garcia</a></li>
1135      </ul>
1136    </div>
1137    <div class="rt-lista-ciudad">
1138      <h3><a href="/entrenadores-personales/torrelodones">Dónde entrenar en Torrelodones</a></h3>
1139      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Torrelodones.</p>
1140      <p class="rt-lista-fam">Gimnasios y centros</p>
1141      <ul>
1142        <li><a href="/donde-entrenar#lugar=mitolab">Mitolab – Fitness Celular Integrativo</a> <span class="rt-lista-tipo">· Estudio o box · Calle San Ramón 2</span> · <a href="/centros/mitolab">ficha del centro</a></li>
1143      </ul>
1144    </div>
1145    <div class="rt-lista-ciudad">
1146      <h3><a href="/entrenadores-personales/valencia">Dónde entrenar en Valencia</a></h3>
1147      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Valencia.</p>
1148      <p class="rt-lista-fam">Al aire libre</p>
1149      <ul>
1150        <li><a href="/donde-entrenar#lugar=cauce-del-rio-turia-sin-ubicacion-concreta">Cauce del río turia, sin ubicación concreta.</a> <span class="rt-lista-tipo">· Parque · Cauce del río turia</span> — entrena aquí <a href="/entrenador-personal/xavi-orti-entrenamiento-personalizado">Xavi Ortí</a></li>
1151      </ul>
1152    </div>
1153    <div class="rt-lista-ciudad">
1154      <h3>Dónde entrenar en Villa de Otura</h3>
1155      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Villa de Otura.</p>
1156      <p class="rt-lista-fam">Gimnasios y centros</p>
1157      <ul>
1158        <li><a href="/donde-entrenar#lugar=alberto-diaz-entrenamiento-personal">Alberto Díaz — Entrenamiento Personal</a> <span class="rt-lista-tipo">· Estudio o box · Calle los Olivos, 10</span> — entrena aquí <a href="/entrenador-personal/alberto-diaz-granada">Alberto Díaz</a></li>
1159      </ul>
1160    </div>
1161    <div class="rt-lista-ciudad">
1162      <h3><a href="/entrenadores-personales/villaverde-del-rio">Dónde entrenar en Villaverde del Río</a></h3>
1163      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Villaverde del Río.</p>
1164      <p class="rt-lista-fam">Gimnasios y centros</p>
1165      <ul>
1166        <li><a href="/donde-entrenar#lugar=tocasport-villaverde">Tocasport Villaverde</a> <span class="rt-lista-tipo">· Gimnasio · Avenida del Convento, 54</span> — entrena aquí <a href="/entrenador-personal/jose-angel-perez-sevilla">José Ángel Pérez</a></li>
1167      </ul>
1168    </div>
1169    <div class="rt-lista-ciudad">
1170      <h3><a href="/entrenadores-personales/zaragoza">Dónde entrenar en Zaragoza</a></h3>
1171      <p class="rt-lista-intro">1 sitio donde entrenan entrenadores personales en Zaragoza.</p>
1172      <p class="rt-lista-fam">Gimnasios y centros</p>
1173      <ul>
1174        <li><a href="/donde-entrenar#lugar=torrero-gym">Torrero Gym</a> <span class="rt-lista-tipo">· Gimnasio · Calle Monterregado, 31</span> — entrena aquí <a href="/entrenador-personal/torrero-gym-zaragoza">Juan de Dios Sanzo</a> · <a href="/centros/torrero-gym">ficha del centro</a></li>
1175      </ul>
1176    </div>
1177<!-- LISTA:FIN -->
1178</section>
1179
1180<style>
1181.rt-lista{font-family:"Plus Jakarta Sans",sans-serif;color:#1B1E22;max-width:1200px;margin:0 auto;padding:44px 24px 0;}
1182.rt-lista *{box-sizing:border-box;}
1183.rt-lista h2{font-family:"Barlow",sans-serif;font-size:clamp(20px,3vw,26px);font-weight:800;text-transform:uppercase;text-align:center;margin:0 0 8px;}
1184.rt-lista-pie{font-size:14px;line-height:1.6;color:#6b7280;text-align:center;max-width:680px;margin:0 auto 26px;}
1185.rt-lista-ciudad{margin:0 0 30px;}
1186.rt-lista-ciudad h3{font-family:"Barlow",sans-serif;font-size:22px;font-weight:700;margin:0 0 4px;}
1187.rt-lista-intro{font-size:15px;line-height:1.6;color:#4B5563;margin:0;}
1188.rt-lista-fam{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;margin:16px 0 6px;}
1189.rt-lista ul{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0 28px;}
1190.rt-lista li{font-size:15px;line-height:1.55;color:#4B5563;border-bottom:1px solid #d9dde2;padding:9px 0;}
1191.rt-lista a{color:#089F3B;font-weight:600;text-decoration:none;}
1192.rt-lista a:hover{color:#067A2D;text-decoration:underline;}
1193.rt-lista-tipo{color:#6b7280;}
1194/* Los barrios se repiten como texto suelto porque es como se busca
1195   ("entrenar en Chamberí") y ahí no hay nada que enlazar todavía. */
1196.rt-lista-barrios{font-size:13.5px;line-height:1.6;color:#6b7280;margin:14px 0 0;}
1197@media(max-width:700px){
1198  .rt-lista{padding-left:16px;padding-right:16px;}
1199}
1200</style>
1201</div><section class="mapa-faq"><h2 class="mapa-faq_titulo">Preguntas frecuentes</h2><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Qué es este mapa?</h3><p class="mapa-faq_respuesta">Un mapa de los sitios donde entrenan de verdad los entrenadores personales de RedEntrenadores: gimnasios, estudios, parques, pistas públicas, rutas y playas. Cada punto lo ha añadido un profesional que entrena allí.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Cómo contacto con un entrenador del mapa?</h3><p class="mapa-faq_respuesta">Al abrir un sitio verás quién entrena en él. Desde ahí entras en su perfil, donde puedes ver su experiencia, sus precios y las opiniones de sus clientes, y contactar con él sin compromiso.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Cuesta algo contactar?</h3><p class="mapa-faq_respuesta">No. Contactar con un entrenador es gratis y sin compromiso. Él decide si puede ayudarte y te responde directamente.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿Puedo entrenar en un parque con un entrenador personal?</h3><p class="mapa-faq_respuesta">Sí, y es más habitual de lo que parece. Muchos entrenadores dan sus sesiones al aire libre —parques con barras, pistas de atletismo, playas— y llevan ellos el material. En el mapa están marcados como sitios al aire libre.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">Soy entrenador, ¿cómo añado los sitios donde entreno?</h3><p class="mapa-faq_respuesta">Desde tu panel, en &quot;Dónde entreno&quot;. Buscas la dirección, colocas el punto y en unos segundos aparece en el mapa con tu nombre. Si el sitio ya está, puedes unirte a él en vez de crearlo otra vez.</p></div><div class="mapa-faq_item"><h3 class="mapa-faq_pregunta">¿En qué ciudades funciona?</h3><p class="mapa-faq_respuesta">Estamos empezando por Madrid, Barcelona y Málaga, que es donde hay más entrenadores. El mapa crece según los profesionales van añadiendo sus sitios, así que irán aparecien
1201do más.</p></div></section><section class="general-seccion"><div class="wrap_general"><div class="rl_navbar5_item-title">Por tipo de sitio</div><a href="/donde-entrenar/parques-de-calistenia" class="enlace-mapa-ciudad">Parques de calistenia y barras cerca de ti</a><a href="/donde-entrenar/gimnasios-al-aire-libre" class="enlace-mapa-ciudad">Gimnasios al aire libre: qué máquinas hay y dónde están</a></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" aria-current="page" class="footer-link w--current">Dónde entrenar</a><a href="/calculadoras/precio-entrenador-personal" class="footer-link">Cuánto cuesta un entrenador</a><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>
1201<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>
1202  /* Namespace propio `re-rec-*` en TODO. El sitio ya tiene `.re-modal`,
1203     `.re-cta` y `.re-field` en las landings y en el listado, y este bloque
1204     carga en las mismas páginas: reutilizar esos nombres es garantizarse un
1205     choque de estilos el día que alguien toque cualquiera de los dos. */
1206  #re-rec-overlay {
1207    position: fixed;
1208    inset: 0;
1209    /* En móvil `inset: 0` mide el viewport GRANDE — el de la barra del
1210       navegador escondida —, así que el borde de abajo del overlay cae por
1211       debajo de lo que se ve. Como en móvil el modal se ancla justo ahí
1212       (bottom sheet), el último campo y el botón de enviar quedaban tapados
1213       por la barra del navegador, sin forma de llegar a ellos: el modal ya
1214       está en su tope de scroll interno y lo que sobra queda fuera de
1215       pantalla. `dvh` mide el viewport visible de verdad; el `vh` de arriba
1216       es el respaldo para navegadores sin `dvh`, que se quedan como estaban.
1217       El teclado no lo arregla ninguna unidad CSS — de eso se encarga
1218       `ajustarAlViewport()` en el bloque 3. */
1219    height: 100vh;
1220    height: 100dvh;
1221    z-index: 10000;
1222    display: none;
1223    align-items: center;
1224    justify-content: center;
1225    padding: 20px;
1226    background: rgba(27, 30, 34, 0.55);
1227    font-family: 'Plus Jakarta Sans', sans-serif;
1228    box-sizing: border-box;
1229  }
1230  #re-rec-overlay.re-rec-abierto { display: flex; }
1231
1232  #re-rec-overlay .re-rec-modal {
1233    position: relative;
1234    width: 100%;
1235    max-width: 520px;
1236    max-height: 90vh;
1237    overflow-y: auto;
1238    /* Que el scroll del modal no arrastre la página de detrás al llegar al
1239       final: en iOS el encadenado hace que el sheet parezca que "se va". */
1240    overscroll-behavior: contain;
1241    background: #fff;
1242    border-radius: 12px;
1243    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.09);
1244    padding: 28px;
1245    box-sizing: border-box;
1246    /* El paso de confirmación es bastante más ancho que las preguntas (tres
1247       fichas en fila); se ensancha con una clase en vez de dejar el modal
1248       ancho todo el rato, que haría que las 4 preguntas naden en el hueco. */
1249  }
1250  #re-rec-overlay .re-rec-modal.re-rec-ancho { max-width: 640px; }
1251
1252  #re-rec-overlay .re-rec-cerrar {
1253    position: absolute;
1254    top: 10px;
1255    right: 10px;
1256    width: 44px;
1257    height: 44px;
1258    border: 0;
1259    background: none;
1260    color: #6b7280;
1261    font-size: 20px;
1262    line-height: 1;
1263    cursor: pointer;
1264  }
1265  #re-rec-overlay .re-rec-cerrar:hover { color: #1B1E22; }
1266
1267  #re-rec-overlay .re-rec-dots { display: flex; gap: 6px; margin-bottom: 18px; }
1268  #re-rec-overlay .re-rec-dots span {
1269    width: 26px;
1270    height: 4px;
1271    border-radius: 999px;
1272    background: #d9dde2;
1273  }
1274  #re-rec-overlay .re-rec-dots span.re-rec-hecho { background: #089F3B; }
1275
1276  #re-rec-overlay .re-rec-eyebrow {
1277    font-size: 12px;
1278    font-weight: 700;
1279    letter-spacing: 0.48px;
1280    text-transform: uppercase;
1281    color: #089F3B;
1282    margin: 0 0 6px;
1283  }
1284  #re-rec-overlay h3 {
1285    font-family: 'Barlow', sans-serif;
1286    font-size: 24px;
1287    line-height: 1.25;
1288    font-weight: 700;
1289    color: #1B1E22;
1290    margin: 0 0 6px;
1291    padding-right: 34px;
1292  }
1293  #re-rec-overlay .re-rec-sub {
1294    font-size: 16px;
1295    line-height: 1.6;
1296    color: #4B5563;
1297    margin: 0 0 18px;
1298  }
1299
1300  #re-rec-overlay .re-rec-pregunta {
1301    font-size: 14px;
1302    font-weight: 600;
1303    color: #1B1E22;
1304    margin: 0 0 8px;
1305  }
1306
1307  /* Opciones: botón grande de fila completa. Objetivo táctil holgado (48px)
1308     por el punto 13 del Apéndice A del design system — el código nuevo no
1309     debería nacer ya con deuda de accesibilidad. */
1310  #re-rec-overlay .re-rec-opciones { display: grid; gap: 8px; }
1311  #re-rec-overlay .re-rec-op {
1312    display: flex;
1313    align-items: center;
1314    gap: 12px;
1315    width: 100%;
1316    min-height: 48px;
1317    padding: 10px 14px;
1318    text-align: left;
1319    background: #fff;
1320    border: 1px solid #d9dde2;
1321    border-radius: 8px;
1322    font-family: inherit;
1323    font-size: 16px;
1324    font-weight: 500;
1325    color: #1B1E22;
1326    cursor: pointer;
1327    box-sizing: border-box;
1328  }
1329  #re-rec-overlay .re-rec-op:hover { border-color: #089F3B; background: #EAF3DE; }
1330  #re-rec-overlay .re-rec-op.re-rec-op-on { border-color: #089F3B; background: #EAF3DE; color: #067A2D; font-weight: 600; }
1331  #re-rec-overlay .re-rec-op-txt { flex: 1; min-width: 0; }
1332  #re-rec-overlay .re-rec-op-nota { display: block; font-size: 13px; font-weight: 400; color: #6b7280; margin-top: 2px; }
1333
1334  /* El icono va en su propia pastilla verde clara y no suelto: suelto se
1335     pierde contra el blanco de la fila, y es lo que hace que la lista se lea
1336     de un vistazo en vez de leerse renglón a renglón. Al pasar por encima el
1337     fondo de la fila se vuelve `#EAF3DE`, el mismo de la pastilla, así que
1338     ahí la pastilla invierte a verde macizo para no desaparecer. */
1339  #re-rec-overlay .re-rec-op-ico {
1340    display: flex;
1341    align-items: center;
1342    justify-content: center;
1343    width: 38px;
1344    height: 38px;
1345    flex-shrink: 0;
1346    border-radius: 10px;
1347    background: #EAF3DE;
1348    color: #089F3B;
1349  }
1350  #re-rec-overlay .re-rec-op-ico svg { width: 21px; height: 21px; display: block; }
1351  #re-rec-overlay .re-rec-op:hover .re-rec-op-ico,
1352  #re-rec-overlay .re-rec-op.re-rec-op-on .re-rec-op-ico { background: #089F3B; color: #fff; }
1353
1354  #re-rec-overlay .re-rec-op-flecha {
1355    width: 17px;
1356    height: 17px;
1357    flex-shrink: 0;
1358    color: #C9CED4;
1359  }
1360  #re-rec-overlay .re-rec-op:hover .re-rec-op-flecha,
1361  #re-rec-overlay .re-rec-op.re-rec-op-on .re-rec-op-flecha { color: #089F3B; }
1362
1363  /* "Ver más opciones" no es una opción más: va sin caja para que no compita
1364     con las dos que sí son elegibles. */
1365  #re-rec-overlay .re-rec-vermas {
1366    justify-self: start;
1367    min-height: 44px;
1368    padding: 4px 0;
1369    background: none;
1370    border: 0;
1371    cursor: pointer;
1372    font-family: inherit;
1373    font-size: 14px;
1374    font-weight: 600;
1375    color: #089F3B;
1376    text-decoration: underline;
1377  }
1378  #re-rec-overlay .re-rec-vermas:hover { color: #067A2D; }
1379
1380  #re-rec-overlay .re-rec-campo { margin-bottom: 14px; }
1381  /* La ciudad aparece debajo de las opciones de modalidad, no en su propia
1382     pantalla: necesita aire por arriba para no leerse como una quinta opción. */
1383  #re-rec-overlay .re-rec-campo-ciudad { margin-top: 18px; margin-bottom: 0; }
1384  #re-rec-overlay .re-rec-campo label {
1385    display: block;
1386    font-size: 14px;
1387    font-weight: 600;
1388    color: #1B1E22;
1389    margin-bottom: 6px;
1390  }
1391  #re-rec-overlay .re-rec-campo input {
1392    width: 100%;
1393    min-height: 48px;
1394    padding: 12px 14px;
1395    border: 1px solid #d9dde2;
1396    border-radius: 8px;
1397    font-family: inherit;
1398    font-size: 16px;
1399    color: #1B1E22;
1400    background: #fff;
1401    box-sizing: border-box;
1402  }
1403  /* 16px en los inputs y no 14: por debajo de 16 Safari en iPhone hace zoom
1404     al enfocar el campo y descoloca el modal entero. */
1405  #re-rec-overlay .re-rec-campo input:focus {
1406    outline: none;
1407    border-color: #089F3B;
1408    box-shadow: 0 0 0 3px rgba(8, 159, 59, 0.15);
1409  }
1410  #re-rec-overlay .re-rec-opcional { font-weight: 400; color: #6b7280; }
1411
1412  #re-rec-overlay .re-rec-btn {
1413    display: inline-block;
1414    border: 0;
1415    border-radius: 8px;
1416    background: #089F3B;
1417    color: #fff;
1418    font-family: inherit;
1419    font-size: 16px;
1420    font-weight: 600;
1421    padding: 13px 22px;
1422    min-height: 48px;
1423    cursor: pointer;
1424  }
1425  #re-rec-overlay .re-rec-btn:hover { background: #067A2D; }
1426  #re-rec-overlay .re-rec-btn:disabled { opacity: 0.5;
1426 cursor: not-allowed; }
1427
1428  #re-rec-overlay .re-rec-nav { display: flex; align-items: center; gap: 14px; margin-top: 12px; }
1429  #re-rec-overlay .re-rec-atras {
1430    background: none;
1431    border: 0;
1432    padding: 12px 0;
1433    cursor: pointer;
1434    font-family: inherit;
1435    font-size: 14px;
1436    font-weight: 600;
1437    color: #4B5563;
1438    text-decoration: underline;
1439  }
1440  #re-rec-overlay .re-rec-atras:hover { color: #067A2D; }
1441
1442  #re-rec-overlay .re-rec-error {
1443    display: none;
1444    margin: 12px 0 0;
1445    font-size: 14px;
1446    color: #791F1F;
1447    background: #FCEBEB;
1448    border-radius: 8px;
1449    padding: 10px 12px;
1450  }
1451  #re-rec-overlay .re-rec-legal {
1452    margin: 12px 0 0;
1453    font-size: 12px;
1454    line-height: 1.45;
1455    color: #6b7280;
1456  }
1457  #re-rec-overlay .re-rec-legal a { color: #6b7280; text-decoration: underline; }
1458
1459  /* El pie: lo que antes era el subtítulo de arriba. Va separado por un filete
1460     y en cuerpo pequeño — es contexto para quien duda, no la instrucción de la
1461     pantalla, que ahora es el título más las propias opciones. */
1462  #re-rec-overlay .re-rec-pie {
1463    display: flex;
1464    align-items: flex-start;
1465    gap: 9px;
1466    margin: 18px 0 0;
1467    padding-top: 14px;
1468    border-top: 1px solid #E5E6E8;
1469    font-size: 13px;
1470    line-height: 1.5;
1471    color: #6b7280;
1472  }
1473  #re-rec-overlay .re-rec-pie-ico { color: #089F3B; flex-shrink: 0; line-height: 0; }
1474  #re-rec-overlay .re-rec-pie-ico svg { width: 17px; height: 17px; display: block; }
1475
1476  /* ---------------------------------------------------------- confirmación */
1477
1478  #re-rec-overlay .re-rec-fichas {
1479    display: grid;
1480    grid-template-columns: repeat(3, minmax(0, 1fr));
1481    gap: 12px;
1482    margin: 18px 0 0;
1483  }
1484  /* Sin `:hover` ni cursor de mano: la ficha no es clicable (ver fichaHTML), y
1485     un borde que reacciona al pasar por encima promete un clic que no existe. */
1486  #re-rec-overlay .re-rec-ficha {
1487    display: block;
1488    text-align: center;
1489    border: 1px solid #d9dde2;
1490    border-radius: 12px;
1491    padding: 14px 10px;
1492    color: inherit;
1493  }
1494  /* El envoltorio existe solo para anclar el badge: la foto es un <img> y no
1495     puede tener hijos. Se lleva el margen que antes tenía la foto. */
1496  #re-rec-overlay .re-rec-foto-wrap {
1497    position: relative;
1498    display: block;
1499    width: 96px;
1500    margin: 0 auto 10px;
1501  }
1502  /* La foto crece dentro de la misma tarjeta: es lo único de la ficha que se
1503     reconoce de un vistazo, y a 64px competía con el texto en vez de mandar.
1504     Cuadrada y no circular: a igual ancho, el círculo recorta los laterales y
1505     se come parte de la cara. Las esquinas van a 8px, el mismo radio que los
1506     botones — no a 12px, que es el de la tarjeta que la contiene. */
1507  #re-rec-overlay .re-rec-foto {
1508    width: 100%;
1509    height: 96px;
1510    border-radius: 8px;
1511    object-fit: cover;
1512    display: block;
1513    background: #F3F3F3;
1514  }
1515
1516  /* Badge "Solo online" encima de la foto, arriba a la izquierda, como en el
1517     resto de tarjetas del sitio. Solo el icono: a 96px —y a 64px en móvil— el
1518     literal no cabe, y ya sale escrito como primer dato de la ficha, justo
1519     debajo. El texto lo llevan title y aria-label. Blanco con borde y sombra,
1520     no verde: informa de la modalidad, no premia nada (§50). */
1521  #re-rec-overlay .re-rec-online {
1522    position: absolute;
1523    top: 6px;
1524    left: 6px;
1525    z-index: 2;
1526    display: inline-flex;
1527    align-items: center;
1528    justify-content: center;
1529    width: 22px;
1530    height: 22px;
1531    box-sizing: border-box;
1532    background: #fff;
1533    border: 1px solid #d9dde2;
1534    border-radius: 999px;
1535    box-shadow: 0 2px 6px rgba(0,0,0,.18);
1536    color: #4B5563;
1537  }
1538  #re-rec-overlay .re-rec-online svg { width: 13px; height: 13px; display: block; }
1539  #re-rec-overlay .re-rec-ficha-nombre {
1540    font-family: 'Barlow', sans-serif;
1541    font-size: 15px;
1542    font-weight: 700;
1543    color: #1B1E22;
1544    display: block;
1545    line-height: 1.25;
1546  }
1547  /* Píldora pública: radio 999px, nunca los 12px de la píldora de panel. */
1548  #re-rec-overlay .re-rec-pill {
1549    display: inline-block;
1550    margin-top: 6px;
1551    padding: 4px 10px;
1552    border-radius: 999px;
1553    background: #F3F4F6;
1554    color: #4B5563;
1555    font-size: 11px;
1556    font-weight: 600;
1557    line-height: 1.3;
1558  }
1559  #re-rec-overlay .re-rec-pill-match { background: #EAF3DE; color: #089F3B; }
1560  #re-rec-overlay .re-rec-ficha-meta {
1561    font-size: 12px;
1562    line-height: 1.4;
1563    color: #4B5563;
1564    display: block;
1565    margin-top: 5px;
1566  }
1567  #re-rec-overlay .re-rec-estrella { vertical-align: -1px; }
1568
1569  #re-rec-overlay .re-rec-p2 { margin-top: 22px; padding-top: 18px; border-top: 1px solid #d9dde2; }
1570  #re-rec-overlay .re-rec-p2 h4 {
1571    font-family: 'Barlow', sans-serif;
1572    font-size: 18px;
1573    font-weight: 700;
1574    color: #1B1E22;
1575    margin: 0 0 4px;
1576  }
1577  /* minmax(0, 1fr) y no 1fr: el mínimo "auto" de un <select> es el ancho de su
1578     opción más larga, así que con 1fr las columnas no encogen y desbordan. */
1579  #re-rec-overlay .re-rec-p2-grid {
1580    display: grid;
1581    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
1582    gap: 0 14px;
1583  }
1584  #re-rec-overlay .re-rec-p2-grid > div { min-width: 0; }
1585  #re-rec-overlay .re-rec-p2-full { grid-column: 1 / -1; }
1586  #re-rec-overlay .re-rec-campo select,
1587  #re-rec-overlay .re-rec-campo textarea {
1588    width: 100%;
1589    min-height: 48px;
1590    padding: 12px 14px;
1591    border: 1px solid #d9dde2;
1592    border-radius: 8px;
1593    font-family: inherit;
1594    font-size: 16px;
1595    color: #1B1E22;
1596    background: #fff;
1597    box-sizing: border-box;
1598  }
1599  #re-rec-overlay .re-rec-campo textarea { min-height: 70px; resize: vertical; }
1600  #re-rec-overlay .re-rec-acciones { display: flex; align-items: center; gap: 18px; margin-top: 8px; }
1601  #re-rec-overlay .re-rec-saltar {
1602    background: none;
1603    border: 0;
1604    padding: 12px 0;
1605    cursor: pointer;
1606    font-family: inherit;
1607    font-size: 14px;
1608    font-weight: 600;
1609    color: #4B5563;
1610    text-decoration: underline;
1611  }
1612
1613  @media (max-width: 560px) {
1614    #re-rec-overlay {
1615      padding: 12px;
1616      /* La barra de gestos del iPhone se come los últimos ~34px: sin esto el
1617         pie del modal queda por debajo de ella. */
1618      padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
1619      align-items: flex-end;
1620    }
1621    /* `100%` es el alto del overlay ya descontado su padding, o sea lo que de
1622       verdad se ve. Antes era `92vh`, que se mide contra el viewport grande y
1623       dejaba el pie del modal fuera de pantalla. */
1624    #re-rec-overlay .re-rec-modal { padding: 22px 18px; max-height: 100%; }
1625
1626    /* Con el teclado abierto el modal se queda en ~350px de alto y el
1627       contenido no cabe de ninguna manera: al escribir la ciudad, el botón
1628       quedaba por debajo del borde del modal. Se veía cortado y no había nada
1629       que indicara que el modal tiene scroll propio, así que el paso parecía
1630       roto. Pegado abajo, el botón está siempre a la vista y lo que scrolla es
1631       lo de arriba. Solo en móvil: en escritorio no hay teclado que robe media
1632       pantalla y el modal casi nunca desborda.
1633
1634       El fondo blanco es obligatorio —el contenido pasa por detrás— y el
1635       padding de abajo replica el del modal, porque `bottom: 0` ancla contra
1636       el borde del scrollport y no contra su padding; el margen negativo lo
1637       descuenta para que en reposo, sin pegar, nada se mueva de sitio. */
1638    #re-rec-overlay .re-rec-nav {
1639      position: sticky;
1640      bottom: 0;
1641      background: #fff;
1642      padding-bottom: 22px;
1643      margin-bottom: -22px;
1644      /* El padding de arriba se descuenta del margen para que en reposo la
1645         separación siga siendo la misma. La sombra es lo que hace que, cuando
1646         hay contenido pasando por detrás, la barra se lea como barra y no como
1647         un campo cortado a medias; a esta opacidad no se nota cuando la barra
1648         está en su sitio y no tapa nada. */
1649      padding-top: 10px;
1650      margin-top: 2px;
1651      box-shadow: 0 -6px 10px -8px rgba(27, 30, 34, 0.16);
1652    }
1653    #re-rec-overlay h3 { font-size: 21px; }
1654    #re-rec-overlay .re-rec-fichas { grid-template-columns: minmax(0, 1fr); }
1655    #re-rec-overlay .re-rec-ficha { display: flex; align-items: center; gap: 14px; text-align: left; }
1656    #re-rec-overlay .re-rec-foto-wrap { width: 64px; margin: 0; flex-shrink: 0; }
1657    #re-rec-overlay .re-rec-foto { height: 64px; }
1658    #re-rec-overlay .re-rec-online { top: 4px; left: 4px;
1658 width: 20px; height: 20px; }
1659    #re-rec-overlay .re-rec-ficha-datos { min-width: 0; }
1660    #re-rec-overlay .re-rec-p2-grid { grid-template-columns: minmax(0, 1fr); }
1661  }
1662</style></div><div class="w-embed w-script">
1662<script>
1663(function () {
1664  "use strict";
1665
1666  window.RE_RECOMENDADOR_VERSION = "1.0.0";
1667
1668  var SUPABASE_URL = "https://yrzanatzshdpvypazgeb.supabase.co";
1669  var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlyemFuYXR6c2hkcHZ5cGF6Z2ViIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ0NjczNzcsImV4cCI6MjEwMDA0MzM3N30.WNRs7Bc8lA1JUw-KbOHYDBlEjTIPjwxx3cvlBmdpgHU";
1670  var PRIVACIDAD_URL = "https://www.redentrenadores.com/legal/politica-de-privacidad";
1671
1672  // Rutas donde el pop-up no debe salir nunca. El panel y el área de usuario
1673  // son zonas privadas de alguien que ya está dentro; /lp/ son las landings de
1674  // paid, que ya llevan este mismo flujo dentro de su propio widget y verían
1675  // dos modales peleándose; /crear-cuenta y /crea-tu-perfil son el alta y el
1676  // onboarding del entrenador, a quien ofrecerle buscar entrenador no le sirve
1677  // de nada y encima le tapa el formulario a medio rellenar; en
1678  // /pregunta-al-experto es donde contesta preguntas. /donde-entrenar queda
1679  // fuera por decisión de Esteve (20/08/2026).
1680  var RUTAS_EXCLUIDAS = [
1681    "/panel-entrenador", "/usuarios/", "/mi-cuenta", "/admin", "/lp/",
1682    "/crear-cuenta", "/crea-tu-perfil", "/pregunta-al-experto", "/donde-entrenar"
1683  ];
1684
1685  var SEGUNDOS_ESPERA = 25;
1686  var SCROLL_DISPARO = 0.5;
1687
1688  var CLAVE_APAGADO = "re_recomendador_off";     // localStorage: no volver a salir
1689  var CLAVE_SESION = "re_recomendador_visto";    // sessionStorage: ya salió hoy
1690
1691  // Los `value` son las etiquetas exactas de `entrenadores.tipo_atencion`, no
1692  // slugs, y tienen que seguir siéndolo: es lo que ya hay guardado en
1693  // `solicitudes_contacto.modalidad` y lo que el panel del entrenador pinta
1694  // sin traducir. El label es lo único redactable para el usuario.
1695  //
1696  // El orden importa: se enseñan solo las dos primeras y el resto se destapa
1697  // con "Ver más", igual que hace el wizard de /encuentra-entrenador. Online y
1698  // gimnasio son las dos que cubren a casi todo el mundo, y una pantalla con
1699  // cuatro opciones grandes pesa más de lo que aporta como primera impresión.
1700  var MODALIDADES = [
1701    { value: "Online", label: "Online, por videollamada", nota: "Desde donde quieras" },
1702    { value: "En un gimnasio", label: "En un gimnasio", nota: "En su centro o en el tuyo" },
1703    { value: "A domicilio", label: "En mi casa", nota: "El entrenador se desplaza hasta ti" },
1704    { value: "En exteriores", label: "Al aire libre", nota: "Parques y espacios abiertos" }
1705  ];
1706  // Cuántas opciones se enseñan de entrada en cada paso; el resto va tras
1707  // "Ver más opciones". Las listas están ordenadas para que las visibles sean
1708  // las que cubren a más gente: en objetivos, las tres primeras son las que
1709  // más entrenadores trabajan (53, 47 y 45 de 95).
1710  var VISIBLES = { donde: 2, objetivo: 3 };
1711
1712  // Son las especialidades tal cual están en `entrenadores.especialidades`,
1713  // porque la RPC filtra con `especialidades @> ARRAY[p_objetivo]`: cualquier
1714  // slug inventado aquí no casaría con nadie y devolvería lista vacía. Se
1715  // ofrecen las 6 con cobertura real (11 y 5 entrenadores las menos vistas de
1716  // esta lista); las de cola larga caen en "Aún no lo sé", que no filtra.
1717  var OBJETIVOS = [
1718    { value: "Pérdida de peso", label: "Perder peso" },
1719    { value: "Ganar masa muscular", label: "Ganar masa muscular" },
1720    { value: "Mejora de forma física", label: "Estar en forma" },
1721    { value: "Entrenamiento funcional", label: "Entrenamiento funcional" },
1722    { value: "Recuperación de lesiones", label: "Recuperarme de una lesión" },
1723    { value: "Preparación para deportistas", label: "Preparar una prueba o competición" },
1724    { value: "", label: "Aún no lo tengo claro", nota: "Te ayudarán a definirlo" }
1725  ];
1726
1727  // `cuando_empieza` no tiene hoy ni una fila rellena en toda la tabla (el
1728  // campo existe en el formulario de la ficha pero no llega), así que estos
1729  // valores no chocan con nada previo. Son texto legible a propósito: el
1730  // entrenador los lee tal cual en su panel, sin traducción.
1731  var CUANDOS = [
1732    { value: "Lo antes posible", label: "Lo antes posible" },
1733    { value: "En 2-4 semanas", label: "En 2-4 semanas" },
1734    { value: "En 1-2 meses", label: "En 1 o 2 meses" },
1735    { value: "Solo me estoy informando", label: "Solo me estoy informando" }
1736  ];
1737
1738  var NIVELES = [
1739    { value: "sedentario", label: "Sedentario, casi no me muevo" },
1740    { value: "poco-activo", label: "Poco activo, 1-2 días" },
1741    { value: "activo", label: "Activo, 3-4 días" },
1742    { value: "muy-activo", label: "Muy activo, 5 días o más" }
1743  ];
1744
1745  var estado = {
1746    indice: 0,                // posición dentro de pasos()
1747    fase: "preguntas",        // "preguntas" | "confirmacion" | "cierre"
1748    modalidad: "",
1749    abiertas: {},             // pasos cuyo "Ver más" ya se ha pulsado
1750    ciudad: "",
1751    ciudadDePagina: false,    // en las 60 páginas de ciudad tampoco se pregunta
1752    objetivo: "",
1753    objetivoDePagina: false,  // en /objetivo/* no se pregunta, ya lo sabemos
1754    objetivoRespondido: false,
1755    cuando: "",
1756    nombre: "",
1757    email: "",
1758    telefono: "",
1759    enviando: false,
1760    error: "",
1761    top3: [],
1762    top3Clave: null,          // respuestas con las que se busco ese reparto
1763    claimToken: null,
1764
1765    // Solo para la medición: qué pantallas se han contado ya en esta apertura,
1766    // cuándo se abrió y si esta apertura cuenta. Las previsualizaciones y el
1767    // ?recomendador=1 no cuentan: si no, cada vez que se revisa el copy se
1768    // ensucia el embudo con una sesión que no es de nadie.
1769    vistas: {},
1770    abiertoEn: 0,
1771    midiendo: true
1772  };
1773
1774  // La secuencia de pantallas no es fija: quien entrena online no tiene que
1775  // decir su ciudad, y en una página de ciudad o de objetivo esa respuesta ya
1776  // la da la propia página. Se calcula en cada render en vez de encadenar
1777  // condiciones dentro de cada paso — un contador fijo de "paso N de 4" con
1778  // ramas de distinta longitud acaba pintando 5 pantallas bajo 4 puntos.
1779  //
1780  // Nunca se pregunta algo que ya sabemos: quien entra por
1781  // /entrenadores-personales/valencia ya ha dicho dónde busca, y volver a
1782  // preguntárselo se lee como que no le hemos escuchado.
1783  function pasos() {
1784    var lista = ["donde"];
1785    if (!estado.objetivoDePagina) lista.push("objetivo");
1786    lista.push("cuando", "datos");
1787    return lista;
1788  }
1789
1790  function pasoActual() {
1791    return pasos()[estado.indice];
1792  }
1793
1794  var ciudades = [];
1795
1796  // ------------------------------------------------------------------ utils
1797
1798  function esc(str) {
1799    var d = document.createElement("div");
1800    d.textContent = str == null ? "" : String(str);
1801    return d.innerHTML;
1802  }
1803
1804  function leer(clave, almacen) {
1805    try { return almacen.getItem(clave); } catch (e) { return null; }
1806  }
1807
1808  function escribir(clave, valor, almacen) {
1809    try { almacen.setItem(clave, valor); } catch (e) {}
1810  }
1811
1812  // La ciudad de las 60 páginas de ciudad sale de #re-ciudad-nombre, el
1813  // elemento oculto bindeado al campo Nombre de la colección Ciudades que ya
1814  // usa el glue del listado. Se usa el nombre y no el slug porque la RPC casa
1815  // por `ciudad_nombre ILIKE` y hay ciudades cuyo slug no coincide con su
1816  // nombre ("la-sagra-toledo" contra "La sagra  Toledo").
1817  function ciudadDeLaPagina() {
1818    var el = document.getElementById("re-ciudad-nombre");
1819    if (el && el.textContent.trim()) return el.textContent.trim();
1820    var param = new URLSearchParams(location.search).get("ciudad");
1821    return param ? param.trim() : "";
1822  }
1823
1824  // Las marcas que deja NFD al separar las tildes (U+0300-U+036F). Va con
1825  // fromCharCode y no con un escape unicode porque este bloque se sube al
1826  // embed dentro del JSON de una llamada MCP y esa secuencia no sobrevive:
1827  // queda el carácter combinante suelto, invisible en el editor. El rango es
1828  // exactamente el mismo. La propiedad Diacritic no vale como atajo: deja
1829  // fuera 19 puntos de este bloque.
1830  var MARCAS_COMBINANTES = new RegExp(
1831    "[" + String.fromCharCode(0x300) + "-" + String.fromCharCode(0x36f) + "]", "g");
1832
1833  // En /objetivo/* el objetivo ya lo dice la propia página: preguntarlo otra
1834  // vez es hacerle repetir al usuario lo que acaba de elegir al entrar.
1835  function aSlug(texto) {
1836    return texto.toLowerCase()
1837      .normalize("NFD").replace(MARCAS_COMBINANTES, "")
1838      .replace(/[^a-z0-9]+/g, "-")
1839      .replace(/^-|-$/g, "");
1840  }
1841
1842  function objetivoDeLaPagina() {
1843    var m = location.pathname.match(/^\/objetivo\/([^/]+)/);
1844    if (!m) return null;
1845    var encontrado = OBJETIVOS.filter(function (o) { return o.value; })
1846      .find(function (o) { return aSlug(o.value) === m[1]; });
1847    return encontrado ? encontrado.value : null;
1848  }
1849
1850  // ------------------------------------------------------------------ datos
1851
1852  function rest(ruta, opciones) {
1853    var o = opciones || {};
1854    return fetch(SUPABASE_URL + ruta, {
1855      method: o.method || "GET",
1856      headers: {
1857        apikey: SUPABASE_ANON_KEY,
1858        Authorization: "Bearer " + SUPABASE_ANON_KEY,
1859        "Content-Type": "application/json"
1860      },
1861      body: o.body ? JSON.stringify(o.body) : undefined
1862    }).then(function (res) {
1863      if (!res.ok) throw new Error(ruta + " -> " + res.status);
1864      return res.json();
1865    });
1866  }
1867
1868  function cargarCiudades() {
1869    if (ciudades.length) return Promise.resolve(ciudades);
1870    return rest("/rest/v1/entrenadores_publico?select=ciudad_nombre&tipo_entrenador=eq.personal&ciudad_nombre=not.is.null")
1871      .then(function (filas) {
1872        ciudades = Array.from(new Set(filas.map(function (f) { return f.ciudad_nombre; })))
1873          .sort(function (a, b) { return a.localeCompare(b, "es"); });
1874        var dl = document.getElementById("re-rec-ciudades");
1875        if (dl) dl.innerHTML = ciudades.map(function (c) { return '<option value="' + esc(c) + '">'; }).join("");
1876        return ciudades;
1877      })
1878      .catch(function (err) {
1879        // Sin la lista el campo sigue siendo un input de texto libre y la RPC
1880        // casa por ILIKE, así que se puede enviar igual: solo se pierde el
1881        // autocompletado. No es motivo para bloquear el formulario.
1882        console.warn("[re-recomendador] no se pudieron cargar las ciudades", err);
1883        return [];
1884      });
1885  }
1886
1887  // A quién se le manda. Se pide a la MISMA RPC que pinta el listado público,
1888  // así que el orden es el mismo que ve el usuario si navega él solo — no un
1889  // ranking paralelo que dijera otra cosa.
1890  //
1891  // NO se fuerza a que salgan tres. Si con lo que ha pedido solo encaja uno, se
1892  // le manda a ese uno y ya: rellenar el hueco con gente que no encaja es un
1893  // mal reparto para el usuario y un contacto quemado para el entrenador.
1894  // Valladolid es el caso real: un único entrenador en toda la ciudad, y la
1895  // versión vieja acababa soltando la ciudad para llegar a tres, así que
1896  // devolvía tres de cualquier sitio menos el de Valladolid.
1897  //
1898  // Los filtros solo se sueltan cuando no encaja NADIE, de menos a más
1899  // importante: primero el objetivo (casi cualquier entrenador sirve para un
1900  // objetivo que no tenga marcado), luego la modalidad, y la ciudad la última
1901  // porque quien no cubre la zona no puede atender a este cliente y punto.
1902  function buscarTop3() {
1903    var intentos = [
1904      { objetivo: estado.objetivo, modalidad: estado.modalidad, ciudad: estado.ciudad },
1905      { objetivo: "", modalidad: estado.modalidad, ciudad: estado.ciudad },
1906      { objetivo: "", modalidad: "", ciudad: estado.ciudad },
1907      { objetivo: "", modalidad: estado.modalidad, ciudad: "" }
1908    ];
1909
1910    function intentar(i) {
1911      if (i >= intentos.length) return Promise.resolve([]);
1912      var f = intentos[i];
1913      return rest("/rest/v1/rpc/buscar_entrenadores_listado", {
1914        method: "POST",
1915        body: {
1916          p_objetivo: f.objetivo || null,
1917          p_modalidad: f.modalidad || null,
1918          p_ciudad: f.ciudad || null,
1919          p_perfil: null,
1920          p_precio_min: null,
1921          p_precio_max: null,
1922          p_orden: "relevancia",
1923          p_limite: 3,
1924          p_offset: 0
1925        }
1926      }).then(function (filas) {
1927        // Basta con uno: el siguiente intento no sería "mejor", sería menos
1928        // fiel a lo que ha pedido.
1929        if (filas && filas.length) return filas;
1930        return intentar(i + 1);
1931      });
1932    }
1933
1934    return intentar(0);
1935  }
1936
1937  // ------------------------------------------------------------------ vistas
1938
1939  function dotsHTML() {
1940    var total = pasos().length;
1941    var html = "";
1942    for (var i = 0; i < total; i++) {
1943      html += '<span class="' + (i <= estado.indice ? "re-rec-hecho" : "") + '"></span>';
1944    }
1945    return '<div class="re-rec-dots">' + html + "</div>";
1946  }
1947
1948  function cabeceraHTML(eyebrow, titulo, sub) {
1949    return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">&#10005;</button>' +
1950      dotsHTML() +
1951      (eyebrow ? '<p class="re-rec-eyebrow">' + esc(eyebrow) + "</p>" : "") +
1952      "<h3>" + esc(titulo) + "</h3>" +
1953      (sub ? '<p class="re-rec-sub">' + esc(sub) + "</p>" : "");
1954  }
1955
1956  // Las opciones se identifican por su valor y no por su posición: con la
1957  // lista de modalidades plegada, un índice apuntaría a la posición dentro de
1958  // lo que se está pintando y no dentro de la lista real.
1959  // Pinta las opciones de un paso ya recortadas a las visibles, y decide sola
1960  // si hace falta el "Ver más": así ningún paso puede quedarse con el enlace
1961  // puesto pero sin nada que destapar.
1962  function opcionesDelPaso(paso, lista, seleccionado) {
1963    var tope = VISIBLES[paso];
1964    var plegado = tope && !estado.abiertas[paso] && lista.length > tope;
1965    return opcionesHTML(plegado ? lista.slice(0, tope) : lista, seleccionado, plegado);
1966  }
1967
1968  function opcionesHTML(lista, seleccionado, verMas) {
1969    return '<div class="re-rec-opciones">' + lista.map(function (o) {
1970      var on = o.value === seleccionado;
1971      return '<button type="button" class="re-rec-op' + (on ? " re-rec-op-on" : "") + '"' +
1972        ' data-accion="opcion" data-valor="' + esc(o.value) + '">' +
1973        "<span>" + esc(o.label) +
1974        (o.nota ? '<span class="re-rec-op-nota">' + esc(o.nota) + "</span>" : "") +
1975        "</span></button>";
1976    }).join("") +
1977      (verMas ? '<button type="button" class="re-rec-vermas" data-accion="ver-mas">Ver más opciones</button>' : "") +
1978      "</div>";
1979  }
1980
1981  function navHTML(conAtras, textoBoton) {
1982    return '<div class="re-rec-nav">' +
1983      (conAtras ? '<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>' : "") +
1984      (textoBoton ? '<button type="button" class="re-rec-btn" data-accion="siguiente">' + esc(textoBoton) + "</button>" : "") +
1985      "</div>";
1986  }
1987
1988  function errorHTML() {
1989    return '<div class="re-rec-error"' + (estado.error ? ' style="display:block"' : "") + ">" + esc(estado.error) + "</div>";
1990  }
1991
1992  // ¿Hace falta preguntar la ciudad? Solo si ya ha dicho que quiere entrenar
1993  // presencialmente y la página no nos la ha dado hecha. Entrenar por
1994  // videollamada no depende de dónde viva, y filtrar por su ciudad ahí solo
1995  // serviría para dejar fuera a entrenadores que le valdrían igual.
1996  function pideCiudad() {
1997    return !!estado.modalidad && estado.modalidad !== "Online" && !estado.ciudadDePagina;
1998  }
1999
2000  // Modalidad y ciudad comparten pantalla: son la misma pregunta ("¿dónde?")
2001  // partida en dos, y separarlas metía un paso entero para un solo campo. El
2002  // campo de ciudad no está desde el principio — aparece al elegir una
2003  // modalidad presencial, porque hasta entonces no se sabe si hace falta.
2004  function pasoDondeHTML() {
2005    // El número va escrito y se cuenta solo. Dos motivos: el recorrido no
2006    // siempre tiene los mismos pasos (el objetivo se salta en /objetivo/*), y
2007    // la última pantalla no es una pregunta sino el formulario de contacto —
2008    // por eso se descuenta uno y no se anuncian cuatro preguntas.
2009    var PALABRAS = { 1: "Una", 2: "Dos", 3: "Tres" };
2010    return cabeceraHTML(
2011      "Te ayudamos a decidir",
2012      "¿Todavía no lo tienes claro? Te recomendamos un entrenador.",
2013      (PALABRAS[pasos().length - 1] || "Unas") + " preguntas rápidas" +
2014        " y te ponemos en contacto con los entrenadores" +
2015        (estado.ciudad ? " de " + estado.ciudad : "") +
2016        " que mejor encajan contigo. Gratis y sin compromiso."
2017    ) +
2018      // Al dejar de ser el título, la primera pregunta necesita su propia
2019      // etiqueta: sin ella las cuatro opciones quedan colgando del subtítulo
2020      // y no se entiende qué se está eligiendo.
2021      '<p class="re-rec-pregunta">¿Dónde te gustaría entrenar?</p>' +
2022      opcionesDelPaso("donde", MODALIDADES, estado.modalidad) +
2023      (pideCiudad()
2024        ? '<div class="re-rec-campo re-rec-campo-ciudad">' +
2025            '<label for="re-rec-ciudad">¿En qué ciudad?</label>' +
2026            '<input type="text" id="re-rec-ciudad" list="re-rec-ciudades" autocomplete="address-level2" placeholder="Empieza a escribir…" value="' + esc(estado.ciudad) + '">' +
2027            '<datalist id="re-rec-ciudades"></datalist>' +
2028          "</div>"
2029        : "") +
2030      errorHTML() +
2031      (pideCiudad() ? navHTML(false, "Siguiente") : "");
2032  }
2033
2034  function pasoObjetivoHTML() {
2035    return cabeceraHTML("", "¿Cuál es tu objetivo?", "Con esto sabemos qué entrenadores están especializados en lo que buscas.") +
2036      // El centinela es `null` y no "": "" es una respuesta valida ("aun no lo
2037      // tengo claro"), y sin distinguirla esa opcion saldria ya marcada antes
2038      // de que el usuario hubiera contestado nada.
2039      opcionesDelPaso("objetivo", OBJETIVOS, estado.objetivoRespondido ? estado.objetivo : null) +
2040      errorHTML() +
2041      navHTML(true, "");
2042  }
2043
2044  function pasoCuandoHTML() {
2045    return cabeceraHTML("", "¿Cuándo te gustaría empezar?", "Así te proponen un plan realista desde el primer mensaje.") +
2046      opcionesHTML(CUANDOS, estado.cuando) +
2047      errorHTML() +
2048      navHTML(true, "");
2049  }
2050
2051  // "Ana, Marcos y Juanma" — coma entre todos menos el último, que va con "y".
2052  function enumerar(nombres) {
2053    if (nombres.length === 1) return nombres[0];
2054    return nombres.slice(0, -1).join(", ") + " y " + nombres[nombres.length - 1];
2055  }
2056
2057  // El subtítulo de la última pantalla nombra a los tres entrenadores en
2058  // cuanto se sabe quiénes son. Mientras la búsqueda va en camino (o si
2059  // falla) se cae a la versión sin nombres: pedir el email es el momento de
2060  // menos paciencia del recorrido y no puede quedarse esperando a nada.
2061  function subtituloDatosHTML() {
2062    if (!estado.top3.length) {
2063      return "Enviaremos tu solicitud a los entrenadores que mejor encajan contigo. Te escribirán ellos directamente.";
2064    }
2065    var nombres = estado.top3.map(function (e) { return "<strong>" + esc(e.nombre) + "</strong>"; });
2066    return enumerar(nombres) +
2067      (estado.top3.length === 1 ? " es el que mejor encaja" : " son los que mejor encajan") +
2068      " contigo. Te " + (estado.top3.length === 1 ? "escribirá" : "escribirán") + " directamente.";
2069  }
2070
2071  // El aviso legal tiene que decir con cuánta gente se comparte el dato, y
2072  // ya no siempre son tres: si en la ciudad solo encaja uno, es uno. Si la
2073  // búsqueda aún no ha vuelto no se inventa un número.
2074  function conQuienHTML() {
2075    var cuantos = estado.top3.length;
2076    if (cuantos === 1) return "este entrenador";
2077    if (cuantos) return "estos " + cuantos + " entrenadores";
2078    return "los entrenadores que mejor encajen contigo";
2079  }
2080
2081  function pasoDatosHTML() {
2082    return cabeceraHTML("", "Último paso: ¿cómo te contactamos?", null) +
2083      '<p class="re-rec-sub">' + subtituloDatosHTML() + "</p>" +
2084      '<div class="re-rec-campo">' +
2085        '<label for="re-rec-nombre">Nombre</label>' +
2086        '<input type="text" id="re-rec-nombre" autocomplete="given-name" value="' + esc(estado.nombre) + '">' +
2087      "</div>" +
2088      // El email vuelve al paso 1 y es obligatorio: sin él no hay chat, porque
2089      // no hay a quién avisar de un mensaje nuevo ni con qué entrar al área
2090      // (la cuenta es passwordless por email). Ver specs/spec-chat-plataforma.md.
2091      '<div class="re-rec-campo">' +
2092        '<label for="re-rec-email">Email</label>' +
2093        '<input type="email" id="re-rec-email" autocomplete="email" inputmode="email" placeholder="[email protected]" value="' + esc(estado.email) + '">' +
2094      "</div>" +
2095      // El teléfono se queda, pero pasa a opcional. No se quita: es el dato con
2096      // el que el entrenador llama, y en los 90 días previos las solicitudes
2097      // que solo lo traían se gestionaron casi todas, frente a dos tercios de
2098      // las que solo traían email.
2099      '<div class="re-rec-campo">' +
2100        '<label for="re-rec-telefono">Teléfono <span class="re-rec-opcional">(opcional)</span></label>' +
2101        '<input type="tel" id="re-rec-telefono" autocomplete="tel" inputmode="tel" placeholder="600 11 22 33" value="' + esc(estado.telefono) + '">' +
2102      "</div>" +
2103      errorHTML() +
2104      '<div class="re-rec-nav">' +
2105        '<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>' +
2106        '<button type="button" class="re-rec-btn" data-accion="enviar"' + (estado.enviando ? " disabled" : "") + ">" +
2107          (estado.enviando ? "Enviando…" : "Enviar mi solicitud") +
2108        "</button>" +
2109      "</div>" +
2110      '<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>';
2111  }
2112
2113  // Estrella siempre en el verde de marca, con independencia de la nota: es la
2114  // convención del listado público (§48 del design system), no un color de
2115  // estado que deba variar con el resultado.
2116  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>';
2117
2118  // La píldora solo lleva el check cuando ese entrenador trabaja de verdad el
2119  // objetivo que ha pedido el usuario. Cuando el reparto ha tenido que soltar
2120  // el filtro de objetivo para completar los tres (o cuando ha contestado "aún
2121  // no lo tengo claro"), se enseña su especialidad principal sin check: decir
2122  // "coincide con lo que buscas" de alguien que no lo trabaja es mentir en la
2123  // pantalla donde más confianza hace falta.
2124  function especialidadHTML(e) {
2125    var suyas = e.especialidades || [];
2126    if (!suyas.length) return "";
2127
2128    var coincide = estado.objetivo && suyas.indexOf(estado.objetivo) !== -1;
2129    var texto = coincide ? etiquetaObjetivo(estado.objetivo) : suyas[0];
2130    return '<span class="re-rec-pill' + (coincide ? " re-rec-pill-match" : "") + '">' +
2131      (coincide ? "✓ " : "") + esc(texto) +
2132      "</span>";
2133  }
2134
2135  // "Pérdida de peso" es como está guardado; "Perder peso" es como se lo hemos
2136  // preguntado. Se le devuelve su propia palabra.
2137  function etiquetaObjetivo(valor) {
2138    var op = OBJETIVOS.find(function (o) { return o.value === valor; });
2139    return op ? op.label : valor;
2140  }
2141
2142  function fichaHTML(e) {
2143    // Sin `loading="lazy"`: son tres fotos que se pintan ya visibles y en el
2144    // momento en que se muestra el modal. Diferirlas solo consigue que la
2145    // confirmación aparezca un instante con tres huecos grises.
2146    var foto = e.foto_url
2147      ? '<img class="re-rec-foto" src="' + esc(e.foto_url) + '" alt="' + esc(e.nombre) + '">'
2148      : '<div class="re-rec-foto"></div>';
2149
2150    // Nota y experiencia, y nada más: el precio se probó como relleno y se
2151    // descartó (decisión de Esteve del 17/08/2026). Esta pantalla va de si el
2152    // entrenador encaja, y meter una cifra en euros justo cuando el usuario
2153    // acaba de dar sus datos convierte una confirmación en un escaparate.
2154    //
2155    // La consecuencia asumida es que muchas fichas se quedan solo con nombre y
2156    // especialidad: de los 93 entrenadores personales, 22 tienen años de
2157    // experiencia y 15 tienen opiniones. Eso no se arregla aquí sino haciendo
2158    // que rellenen esos campos.
2159    //
2160    // Nunca se pinta un cero: un "0 opiniones" o un "0 años" en la pantalla
2161    // que tiene que dar confianza juega en contra del entrenador.
2162    var datos = [];
2163    if (e.total_reviews > 0) {
2164      datos.push(ESTRELLA_SVG + " " + e.rating_promedio + " (" + e.total_reviews +
2165        (e.total_reviews === 1 ? " opinión)" : " opiniones)"));
2166    }
2167    if (e.anos_experiencia > 0) {
2168      // "de experiencia" completo y no solo "2 años": suelto, se lee como la
2169      // edad del entrenador.
2170      datos.push(e.anos_experiencia + (e.anos_experiencia === 1 ? " año" : " años") + " de experiencia");
2171    }
2172
2173    // La ficha NO es un enlace, a propósito. Debajo de estas tres tarjetas
2174    // está el paso de completar el perfil, que es lo único que queda por
2175    // hacer y lo que da valor a la solicitud recién enviada; un enlace al
2176    // perfil aquí arriba se lleva al usuario a otra pestaña justo antes de
2177    // llegar a él. Las fotos y los datos ya cumplen su función, que es
2178    // demostrar que la solicitud ha ido a gente real.
2179    return '<div class="re-rec-ficha">' +
2180      foto +
2181      '<span class="re-rec-ficha-datos">' +
2182        '<span class="re-rec-ficha-nombre">' + esc(e.nombre) + "</span>" +
2183        especialidadHTML(e) +
2184        // Cada dato en su línea y no separados por "·": en una tarjeta de
2185        // 180px, "5 (12 opiniones) · 8 años de experiencia" se parte por donde
2186        // le toca y deja huérfano medio dato en la línea siguiente.
2187        datos.map(function (d) { return '<span class="re-rec-ficha-meta">' + d + "</span>"; }).join("") +
2188      "</span>" +
2189      "</div>";
2190  }
2191
2192  function opcionesSelectHTML(lista, placeholder) {
2193    return '<option value="">' + esc(placeholder) + "</option>" + lista.map(function (o) {
2194      return '<option value="' + esc(o.value) + '">
2194' + esc(o.label) + "</option>";
2195    }).join("");
2196  }
2197
2198  // Paso 2 del journey de usuario. Va SIEMPRE después de la solicitud, nunca
2199  // antes: es opcional y no puede convertirse en un peaje para escribir a un
2200  // entrenador. `claim_token` es lo que devuelve solicitar-contacto-entrenador
2201  // y lo único que prueba que quien rellena esto es quien acaba de enviar.
2202  function confirmacionHTML() {
2203    var cuantos = estado.top3.length;
2204    var p2 = estado.claimToken
2205      ? '<div class="re-rec-p2">' +
2206          "<h4>Cuéntanos algo más</h4>" +
2207          '<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>' +
2208          // El nivel de actividad va a fila completa porque su etiqueta no
2209          // cabe a media columna; edad y peso comparten fila, que son dos
2210          // campos de tres caracteres.
2211          '<div class="re-rec-p2-grid">' +
2212            // El email ya no se pide aquí: lo trae el paso 1, obligatorio.
2213            // Pedirlo dos veces es peor que pedirlo una.
2214            '<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>" +
2215            '<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>' +
2216            '<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>' +
2217            // Altura y disponibilidad los pide la tarjeta de "Solicitud inicial"
2218            // del panel del entrenador, que enseña ocho datos del cliente.
2219            // Están aquí y no en el paso 1 por lo mismo que el resto: el paso 1
2220            // pide lo mínimo para poder responder, esto es lo que ayuda a
2221            // preparar la propuesta y se pregunta cuando ya no cuesta conversión.
2222            '<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>' +
2223            '<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>' +
2224            '<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>' +
2225          "</div>" +
2226          '<div class="re-rec-error" id="re-rec-p2-error"></div>' +
2227          '<div class="re-rec-acciones">' +
2228            '<button type="button" class="re-rec-btn" data-accion="guardar-perfil">Enviar mis datos</button>' +
2229            '<button type="button" class="re-rec-saltar" data-accion="cerrar">Ahora no</button>' +
2230          "</div>" +
2231        "</div>"
2232      : "";
2233
2234    return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">&#10005;</button>' +
2235      '<p class="re-rec-eyebrow">Solicitud enviada</p>' +
2236      "<h3>" + (cuantos === 1 ? "Este es tu entrenador" : "Estos son tus " + cuantos + " entrenadores") + "</h3>" +
2237      '<p class="re-rec-sub">Ya tienen tu solicitud y te escribirán directamente.</p>' +
2238      '<div class="re-rec-fichas">' + estado.top3.map(fichaHTML).join("") + "</div>" +
2239      p2;
2240  }
2241
2242  // `ofreceCuenta` sigue siendo un parámetro y no un `true` fijo aunque el
2243  // email vuelva a ser obligatorio en el paso 1: este cierre también lo pinta
2244  // el camino en el que el paso 2 falla, y ahí no se da nada por hecho.
2245  function cierreHTML(guardado, ofreceCuenta) {
2246    var corto = estado.nombre ? estado.nombre.trim().split(" ")[0] : "";
2247    return '<button type="button" class="re-rec-cerrar" data-accion="cerrar" aria-label="Cerrar">&#10005;</button>' +
2248      "<h3>" + esc(guardado ? "¡Listo!" : (corto ? "¡Gracias, " + corto + "!" : "¡Gracias!")) + "</h3>" +
2249      '<p class="re-rec-sub">' +
2250        (guardado
2251          ? "Tus datos ya van con la solicitud. Se pondrán en contacto contigo muy pronto."
2252          : "Tu solicitud está enviada. Se pondrán en contacto contigo muy pronto.") +
2253      "</p>" +
2254      (estado.claimToken && ofreceCuenta
2255        ? '<div class="re-rec-p2">' +
2256            "<h4>¿Quieres seguir tus solicitudes?</h4>" +
2257            '<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>' +
2258            '<div class="re-rec-error" id="re-rec-cuenta-error"></div>' +
2259            '<button type="button" class="re-rec-btn" data-accion="crear-cuenta">Crear mi cuenta</button>' +
2260            '<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>' +
2261          "</div>"
2262        : "");
2263  }
2264
2265  // ------------------------------------------------------------------ puente
2266
2267  // El widget no cabe en un solo Embed de Webflow (tope de 50.000 caracteres),
2268  // asi que va partido en dos bloques. Esto es TODO lo que cruza de este
2269  // bloque al siguiente; es interno y no es la API publica, que sigue siendo
2270  // window.reRecomendador y la monta el bloque de comportamiento.
2271  //
2272  // `estado` cruza como referencia, no como copia: los dos bloques escriben
2273  // sobre el mismo objeto y esa es justo la idea.
2274  window.__reRecNucleo = {
2275    RUTAS_EXCLUIDAS: RUTAS_EXCLUIDAS,
2276    SEGUNDOS_ESPERA: SEGUNDOS_ESPERA,
2277    SCROLL_DISPARO: SCROLL_DISPARO,
2278    CLAVE_APAGADO: CLAVE_APAGADO,
2279    CLAVE_SESION: CLAVE_SESION,
2280    MODALIDADES: MODALIDADES,
2281    OBJETIVOS: OBJETIVOS,
2282    CUANDOS: CUANDOS,
2283    estado: estado,
2284    pasos: pasos,
2285    pasoActual: pasoActual,
2286    pideCiudad: pideCiudad,
2287    leer: leer,
2288    escribir: escribir,
2289    ciudadDeLaPagina: ciudadDeLaPagina,
2290    objetivoDeLaPagina: objetivoDeLaPagina,
2291    rest: rest,
2292    cargarCiudades: cargarCiudades,
2293    buscarTop3: buscarTop3,
2294    pasoDondeHTML: pasoDondeHTML,
2295    pasoObjetivoHTML: pasoObjetivoHTML,
2296    pasoCuandoHTML: pasoCuandoHTML,
2297    pasoDatosHTML: pasoDatosHTML,
2298    subtituloDatosHTML: subtituloDatosHTML,
2299    confirmacionHTML: confirmacionHTML,
2300    cierreHTML: cierreHTML
2301  };
2302})();
2303</script>
2303
2304</div><div class="w-embed w-script"><!-- RedEntrenadores - Parche del pop-up recomendador: a quien se reparte -->
2305<!-- Va en el Footer V1, ENTRE el bloque 2 (JS nucleo) y el bloque 3 (JS
2306     comportamiento), y tiene que seguir ahi: el bloque 3 se lleva `buscarTop3`
2307     a una variable local nada mas cargar, asi que quien quiera cambiarla tiene
2308     que hacerlo antes.
2309
2310     Que cambia. El nucleo reparte con `buscar_entrenadores_listado`, que es la
2311     RPC del listado publico y ordena por un score de cinco criterios (plan 35,
2312     puja 25, recomendaciones 20, calidad 12, valoracion 8). Eso vale para
2313     ordenar un listado, pero repartir no es ordenar: con 180 entrenadores el
2314     score le daba casi todos los contactos a los mismos seis.
2315
2316     Desde el 12/09/2026 el reparto son dos criterios y ni uno mas
2317     (`buscar_entrenadores_reparto`, migracion 20260913030000):
2318
2319       1. primero los de plan de pago, que pagan por eso;
2320       2. despues todos los demas, empezando por quien menos leads ha recibido.
2321
2322     Y sigue haciendo lo de antes: `p_solo_contactables` deja fuera del reparto
2323     a los perfiles de escaparate (`entrenadores.recibe_contactos = false`), que
2324     tienen que seguir saliendo en el listado y no pueden salir aqui, porque
2325     aqui no se enseña, se reparte el contacto. El servidor ya los descartaba
2326     (`solicitar-contacto-entrenador` no les manda nada), pero el pop-up
2327     enseñaba su tarjeta diciendo "ya tienen tu solicitud", que de ellos no era
2328     verdad.
2329
2330     Por que un parche y no una linea en el bloque 2. Escribir ese embed por
2331     MCP obliga a reproducir sus 34 KB enteros a mano, sobre el flujo que trae
2332     los leads. Esto son 30 lineas y se puede tirar entero el dia que se toque
2333     el nucleo: entonces, cambia alli la RPC y borra este embed.
2334
2335     La cascada de intentos es copia literal del nucleo, con el corte en cuanto
2336     hay UNO (23/08/2026, cambiado en los dos sitios a la vez). Si algun dia
2337     cambia la del nucleo, el numero de version de abajo apaga el parche en vez
2338     de imponer la version vieja en silencio.
2339
2340     ID del embed: e4e348ca-b183-eafe-444a-cd8dbed588ae (componente Footer V1). -->
2341<script>
2342(function () {
2343  "use strict";
2344
2345  var N = window.__reRecNucleo;
2346  if (!N || !N.buscarTop3) return;
2347
2348  // Copiado del bloque 2 con esta version. Si el nucleo cambia de version, el
2349  // parche se aparta: mejor que el pop-up reparta con el score viejo (el
2350  // servidor sigue filtrando) a que se quede congelado con una busqueda vieja.
2351  if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") {
2352    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.");
2353    return;
2354  }
2355
2356  var estado = N.estado;
2357  var rest = N.rest;
2358
2359  N.buscarTop3 = function buscarTop3() {
2360    var intentos = [
2361      { objetivo: estado.objetivo, modalidad: estado.modalidad, ciudad: estado.ciudad },
2362      { objetivo: "", modalidad: estado.modalidad, ciudad: estado.ciudad },
2363      { objetivo: "", modalidad: "", ciudad: estado.ciudad },
2364      { objetivo: "", modalidad: estado.modalidad, ciudad: "" }
2365    ];
2366
2367    function intentar(i) {
2368      if (i >= intentos.length) return Promise.resolve([]);
2369      var f = intentos[i];
2370      return rest("/rest/v1/rpc/buscar_entrenadores_reparto", {
2371        method: "POST",
2372        body: {
2373          p_objetivo: f.objetivo || null,
2374          p_modalidad: f.modalidad || null,
2375          p_ciudad: f.ciudad || null,
2376          p_limite: 3,
2377          // Va dentro de la busqueda y no filtrando el resultado porque el
2378          // LIMIT ya se ha aplicado: quitar a uno aqui fuera dejaria dos, y un
2379          // lead repartido de menos es un entrenador que no recibe nada.
2380          p_solo_contactables: true
2381        }
2382      }).then(function (filas) {
2383        // Basta con uno: el siguiente intento no seria "mejor", seria menos
2384        // fiel a lo que ha pedido.
2385        if (filas && filas.length) return filas;
2386        return intentar(i + 1);
2387      });
2388    }
2389
2390    return intentar(0);
2391  };
2392
2393  window.RE_RECOMENDADOR_ESCAPARATE = "2026-09-12-1";
2394})();
2395</script>
2395</div><div class="w-embed w-script"><!-- RedEntrenadores - Parche del pop-up recomendador: no preguntar lo que ya nos han dicho -->
2396<!-- Va en el Footer V1, ENTRE el parche 2b (escaparate) y el bloque 3 (JS
2397     comportamiento), y tiene que seguir ahi: el bloque 3 se lleva `pasos`,
2398     `pasoActual` y las cuatro funciones de pantalla a variables locales nada
2399     mas cargar, asi que quien quiera cambiarlas tiene que hacerlo antes.
2400
2401     Que arregla. El buscador de la home (widget-home-hero-buscar.html) ya le ha
2402     sacado a la persona el objetivo, la modalidad, la ciudad y cuando quiere
2403     empezar. Sin esto, el pop-up se abria igualmente por la primera pantalla
2404     ("¿donde quieres entrenar?") a preguntarle lo que acababa de escribir.
2405
2406     Como se le dice lo que ya sabemos. Quien abra el pop-up con respuestas
2407     puestas marca ademas `estado.sabidos`:
2408
2409       estado.sabidos = { donde: true, objetivo: true, cuando: true }
2410
2411     Solo se salta lo que este marcado Y ademas tenga valor de verdad. "Donde"
2412     es la unica con truco: esa pantalla pregunta modalidad y, si es presencial,
2413     tambien ciudad, asi que saltarla exige tener las dos.
2414
2415     El paso "datos" no se salta nunca: es donde se piden nombre y email, que es
2416     lo unico que no se puede adivinar de una frase. Con las cuatro respuestas
2417     puestas, el pop-up abre directamente ahi.
2418
2419     Sin `estado.sabidos` la lista es exactamente la del nucleo (["donde",
2420     "objetivo", "cuando", "datos"], con "objetivo" fuera en /objetivo/*). Quien
2421     llega por el pop-up automatico de los 25 segundos no nota ningun cambio y
2422     el embudo que ya se esta midiendo sigue igual.
2423
2424     Por que hay que parchear CUATRO cosas y no solo `pasos`:
2425
2426       - `pasoActual` la calcula el nucleo con su `pasos` interna, no con la
2427         exportada. Sin parchearla, el bloque 3 pintaria la pantalla de la lista
2428         vieja: la lista diria "datos" y la pantalla seguiria siendo "donde".
2429       - Los puntos de progreso y el "Tres preguntas rapidas" tambien salen de
2430         esa `pasos` interna, y estan dentro de `cabeceraHTML`, que no se
2431         exporta. Se corrigen sobre el HTML ya generado, que es feo pero cabe
2432         aqui; la alternativa era reescribir los 30 KB del nucleo a mano.
2433       - El boton "Atras" de la pantalla de datos esta escrito a pelo (no pasa
2434         por `navHTML`), asi que cuando datos es la unica pantalla se queda un
2435         boton que no lleva a ningun sitio. Se quita cuando no hay nada detras.
2436
2437     Desde el 23/08/2026 corrige ademas las tres frases del nucleo que daban por
2438     hecho que el reparto siempre son tres entrenadores; ya no lo es.
2439
2440     Todo esto se cae solo el dia que el nucleo incorpore `sabidos`: sube su
2441     RE_RECOMENDADOR_VERSION y este embed se apaga entero. Mismo criterio y
2442     mismo guardia de version que el parche 2b. -->
2443<script>
2444(function () {
2445  "use strict";
2446
2447  var N = window.__reRecNucleo;
2448  if (!N || !N.pasos || !N.estado) return;
2449
2450  // Igual que en el parche 2b: si el nucleo cambia de version, este parche se
2451  // aparta y vuelve el recorrido completo. Preguntar de mas es recuperable;
2452  // saltarse una pantalla que ha cambiado de contenido, no.
2453  if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") {
2454    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.");
2455    return;
2456  }
2457
2458  var estado = N.estado;
2459
2460  // La pantalla "donde" son dos preguntas en una. Saltarla solo vale si ninguna
2461  // de las dos queda en el aire: online no necesita ciudad, presencial si.
2462  function sabemosDonde() {
2463    if (!estado.modalidad) return false;
2464    if (estado.modalidad === "Online") return true;
2465    return !!estado.ciudad || !!estado.ciudadDePagina;
2466  }
2467
2468  N.pasos = function pasos() {
2469    var sabidos = estado.sabidos || {};
2470    var lista = [];
2471    if (!sabidos.donde || !sabemosDonde()) lista.push("donde");
2472    if (!estado.objetivoDePagina && !(sabidos.objetivo && estado.objetivo)) lista.push("objetivo");
2473    if (!(sabidos.cuando && estado.cuando)) lista.push("cuando");
2474    lista.push("datos");
2475    return lista;
2476  };
2477
2478  N.pasoActual = function pasoActual() {
2479    return N.pasos()[estado.indice];
2480  };
2481
2482  var PALABRAS = { 1: "Una", 2: "Dos", 3: "Tres" };
2483
2484  function dotsHTML() {
2485    var total = N.pasos().length;
2486    var html = "";
2487    for (var i = 0; i < total; i++) {
2488      html += '<span class="' + (i <= estado.indice ? "re-rec-hecho" : "") + '"></span>';
2489    }
2490    return '<div class="re-rec-dots">' + html + "</div>";
2491  }
2492
2493  // Cuantos entrenadores van a recibir la solicitud de verdad. En la primera
2494  // pintada de la pantalla de datos todavia no se sabe (la busqueda va en
2495  // camino), y ahi no se inventa un numero.
2496  function conQuien() {
2497    var n = (estado.top3 || []).length;
2498    if (n === 1) return "este entrenador";
2499    if (n) return "estos " + n + " entrenadores";
2500    return "los entrenadores que mejor encajen contigo";
2501  }
2502
2503  // Cirugia sobre el HTML que devuelve el nucleo. Los reemplazos son literales
2504  // suyos: si alguno deja de aparecer, no rompe nada (el replace no encuentra
2505  // y devuelve el mismo HTML), solo deja de corregir esa parte.
2506  function corregir(html) {
2507    html = html.replace(/<div class="re-rec-dots">[\s\S]*?<\/div>/, dotsHTML());
2508
2509    var preguntas = N.pasos().length - 1;   // la ultima pantalla no es pregunta
2510    html = html.replace(
2511      /(?:Una|Dos|Tres|Unas) preguntas rápidas/,
2512      preguntas === 1 ? "Una pregunta rápida" : (PALABRAS[preguntas] || "Unas") + " preguntas rápidas"
2513    );
2514
2515    // El reparto dejo de forzar tres (parche 2b, 23/08/2026): si en la ciudad
2516    // solo encaja uno, se le manda a uno. Estas tres frases del nucleo daban
2517    // por hecho que siempre eran tres, asi que la primera pantalla prometia
2518    // algo que no se iba a cumplir y el aviso legal decia con cuanta gente se
2519    // comparte el dato sin que fuera verdad.
2520    html = html.replace("con los 3 entrenadores", "con los entrenadores");
2521    html = html.replace("Enviaremos tu solicitud a los 3 entrenadores", "Enviaremos tu solicitud a los entrenadores");
2522    html = html.replace("con estos 3 entrenadores", "con " + conQuien());
2523
2524    if (estado.indice === 0) {
2525      html = html.replace('<button type="button" class="re-rec-atras" data-accion="atras">Atrás</button>', "");
2526    }
2527    return html;
2528  }
2529
2530  ["pasoDondeHTML", "pasoObjetivoHTML", "pasoCuandoHTML", "pasoDatosHTML"].forEach(function (nombre) {
2531    var original = N[nombre];
2532    if (typeof original !== "function") return;
2533    N[nombre] = function () { return corregir(original.apply(null, arguments)); };
2534  });
2535})();
2536</script>
2536</div><div class="w-embed w-script"><!-- ============================================================
2537     RedEntrenadores — Parche del pop-up recomendador: gancho manual en una ruta excluida
2538
2539     QUÉ ARREGLA: en /pregunta-al-experto el recomendador está en `RUTAS_EXCLUIDAS`, y
2540     el bloque 3 entiende "excluida" como "ni siquiera arranques": sale de `init()` antes
2541     de montar nada, así que allí no existe `window.reRecomendador` y no hay forma de
2542     ofrecerlo a mano. Lo queremos ofrecer justo después de enviar la pregunta, que es
2543     cuando quien pregunta tiene el problema fresco y toda la atención puesta.
2544
2545     QUÉ HACE: en esa ruta, y solo en esa,
2546
2547       1. saca `/pregunta-al-experto` de `RUTAS_EXCLUIDAS` para que el bloque 3 monte el
2548          pop-up y deje el gancho `window.reRecomendador.abrir()` disponible;
2549       2. le hace creer que ya salió en esta sesión, envolviendo `leer` para la clave de
2550          sesión. `puedeSalir()` la consulta antes de cada disparo por tiempo o scroll,
2551          así que el pop-up no sale por su cuenta; `abrir()` no la mira, así que el
2552          gancho manual sigue funcionando.
2553
2554     Quien lo abre es el pop-up de confirmación de esa landing:
2555     `widget-pregunta-al-experto-2-popup.html`.
2556
2557     POR QUÉ UN PARCHE Y NO TOCAR EL BLOQUE 3: aquel embed son 28 KB que habría que
2558     reproducir enteros para cambiarle dos líneas. Es el mismo motivo por el que ya
2559     existen `-escaparate` y `-saltar-sabidos`. El día que se reescriba el bloque 3,
2560     el cambio limpio es que `excluida()` apague solo los disparos automáticos y deje
2561     el resto del `init()` en pie; entonces este embed se borra.
2562
2563     DÓNDE VA: cuarto Embed del componente Footer V1, ENTRE el núcleo (bloque 2, del que
2564     lee `window.__reRecNucleo`) y el comportamiento (bloque 3, al que tiene que
2565     adelantarse). Junto a los otros dos parches, delante del bloque 3.
2566     ============================================================ -->
2567
2568<script>
2569(function () {
2570  var N = window.__reRecNucleo;
2571  if (!N) return;   // sin el núcleo no hay nada que parchear; el bloque 3 ya avisa por consola
2572
2573  // Guardia de versión, como los otros parches: si el núcleo cambia de versión, este
2574  // bloque se calla en vez de operar sobre algo que ya no es lo que espera.
2575  if (window.RE_RECOMENDADOR_VERSION !== "1.0.0") {
2576    console.warn("[re-recomendador] parche del gancho manual: version distinta, no se aplica");
2577    return;
2578  }
2579
2580  var RUTA = "/pregunta-al-experto";
2581  if (location.pathname.indexOf(RUTA) !== 0) return;
2582
2583  N.RUTAS_EXCLUIDAS = N.RUTAS_EXCLUIDAS.filter(function (r) { return r !== RUTA; });
2584
2585  // El bloque 3 copia `N.leer` a una variable local en cuanto arranca, así que esto
2586  // tiene que estar puesto antes que él. Solo miente sobre la clave de sesión: todo lo
2587  // demás (el apagado permanente, entre otros) sigue leyéndose de verdad.
2588  var leerOriginal = N.leer;
2589  N.leer = function (clave, almacen) {
2590    if (clave === N.CLAVE_SESION) return "1";
2591    return leerOriginal(clave, almacen);
2592  };
2593
2594  window.RE_RECOMENDADOR_GANCHO_MANUAL = true;
2595})();
2596</script>
2596
2597</div><div class="w-embed w-script">
2597<script>
2598(function () {
2599  "use strict";
2600
2601  // Segunda mitad del pop-up: cuando se abre, que pasa al pulsar y que se
2602  // mide. Depende del bloque anterior; si por lo que sea no ha cargado, aqui
2603  // no se hace nada en vez de reventar con un TypeError por cada pagina.
2604  var N = window.__reRecNucleo;
2605  if (!N) {
2606    console.error("[re-recomendador] falta el bloque 1, el pop-up no arranca");
2607    return;
2608  }
2609
2610  // Se sacan a variables locales para que el resto del bloque se lea igual
2611  // que cuando todo esto era un unico fichero.
2612  var RUTAS_EXCLUIDAS = N.RUTAS_EXCLUIDAS;
2613  var SEGUNDOS_ESPERA = N.SEGUNDOS_ESPERA;
2614  var SCROLL_DISPARO = N.SCROLL_DISPARO;
2615  var CLAVE_APAGADO = N.CLAVE_APAGADO;
2616  var CLAVE_SESION = N.CLAVE_SESION;
2617  var MODALIDADES = N.MODALIDADES;
2618  var OBJETIVOS = N.OBJETIVOS;
2619  var CUANDOS = N.CUANDOS;
2620  var estado = N.estado;
2621  var pasos = N.pasos;
2622  var pasoActual = N.pasoActual;
2623  var pideCiudad = N.pideCiudad;
2624  var leer = N.leer;
2625  var escribir = N.escribir;
2626  var ciudadDeLaPagina = N.ciudadDeLaPagina;
2627  var objetivoDeLaPagina = N.objetivoDeLaPagina;
2628  var rest = N.rest;
2629  var cargarCiudades = N.cargarCiudades;
2630  var buscarTop3 = N.buscarTop3;
2631  var pasoDondeHTML = N.pasoDondeHTML;
2632  var pasoObjetivoHTML = N.pasoObjetivoHTML;
2633  var pasoCuandoHTML = N.pasoCuandoHTML;
2634  var pasoDatosHTML = N.pasoDatosHTML;
2635  var subtituloDatosHTML = N.subtituloDatosHTML;
2636  var confirmacionHTML = N.confirmacionHTML;
2637  var cierreHTML = N.cierreHTML;
2638
2639  var overlay = null;
2640  var caja = null;
2641
2642  // ------------------------------------------------ alto real de la pantalla
2643
2644  // Lo que el CSS no puede arreglar solo: ni `100dvh` ni un `position: fixed`
2645  // se enteran de que ha subido el teclado. El modal se sigue dibujando contra
2646  // el viewport de siempre y el teclado le tapa la parte de abajo, que en el
2647  // paso de datos es justo donde están el último campo y el botón de enviar.
2648  // La única medida que sí encoge con el teclado es `visualViewport`, así que
2649  // mientras el pop-up está abierto el overlay se pega a ella. Hace falta
2650  // colocarlo además de medirlo (`top`/`left`) porque iOS no encoge el
2651  // viewport visible: lo desplaza.
2652  var vv = window.visualViewport || null;
2653  var ultimoEncaje = "";
2654
2655  function ajustarAlViewport() {
2656    if (!overlay || !vv) return;
2657    var encaje = [vv.height, vv.width, vv.offsetTop, vv.offsetLeft].join("|");
2658    if (encaje === ultimoEncaje) return;
2659    ultimoEncaje = encaje;
2660    // Con `top` + `height` y `left` + `width` puestos, el `inset: 0` del CSS
2661    // deja de mandar en el borde de abajo y en el de la derecha.
2662    overlay.style.top = vv.offsetTop + "px";
2663    overlay.style.left = vv.offsetLeft + "px";
2664    overlay.style.height = vv.height + "px";
2665    overlay.style.width = vv.width + "px";
2666    // Si estaba escribiendo, que su campo quede en el centro de lo que queda
2667    // visible. `center` y no `nearest`: `nearest` lo deja pegado al borde de
2668    // abajo, que es justo donde está la barra de botones pegada, y el campo
2669    // acaba detrás de ella.
2670    var foco = document.activeElement;
2671    if (foco && caja && foco !== caja && caja.contains(foco)) {
2672      try { foco.scrollIntoView({ block: "center" }); } catch (e) {}
2673    }
2674  }
2675
2676  function soltarViewport() {
2677    if (!overlay) return;
2678    ultimoEncaje = "";
2679    overlay.style.top = "";
2680    overlay.style.left = "";
2681    overlay.style.height = "";
2682    overlay.style.width = "";
2683  }
2684
2685  // ----------------------------------------------------------------- render
2686
2687  var VISTAS = {
2688    donde: pasoDondeHTML,
2689    objetivo: pasoObjetivoHTML,
2690    cuando: pasoCuandoHTML,
2691    datos: pasoDatosHTML
2692  };
2693
2694  var LISTAS = {
2695    donde: MODALIDADES,
2696    objetivo: OBJETIVOS,
2697    cuando: CUANDOS
2698  };
2699
2700  function pintar() {
2701    var html;
2702    if (estado.fase === "confirmacion") html = confirmacionHTML();
2703    else html = VISTAS[pasoActual()]();
2704
2705    caja.innerHTML = html;
2706    caja.classList.toggle("re-rec-ancho", estado.fase !== "preguntas");
2707    caja.scrollTop = 0;
2708
2709    if (document.getElementById("re-rec-ciudad")) cargarCiudades();
2710    if (estado.fase === "preguntas" && pasoActual() === "datos") precargarTop3();
2711    // El foco automático solo en escritorio: en móvil abre el teclado y tapa
2712    // media pantalla justo cuando el usuario aún está leyendo la pregunta.
2713    var primerInput = caja.querySelector("input");
2714    if (primerInput && window.innerWidth > 560) primerInput.focus();
2715
2716    medirPantalla();
2717  }
2718
2719  function claveBusqueda() {
2720    return [estado.ciudad, estado.objetivo, estado.modalidad].join("|");
2721  }
2722
2723  // Se busca a los tres al llegar a la pantalla de datos, no al enviarla, para
2724  // poder nombrarlos antes de pedir el email. El resultado se guarda con la
2725  // clave de las respuestas que lo produjeron: si vuelve atrás y cambia una,
2726  // la caché deja de valer sola y `enviar` no manda la solicitud a los tres
2727  // entrenadores de una búsqueda que ya no es la suya.
2728  function precargarTop3() {
2729    var clave = claveBusqueda();
2730    if (estado.top3Clave === clave) return;
2731    estado.top3Clave = clave;
2732    estado.top3 = [];
2733
2734    buscarTop3().then(function (top3) {
2735      if (estado.top3Clave !== clave) return;   // llegó tarde: ya cambió algo
2736      estado.top3 = top3;
2737      // Se reescribe solo el subtítulo en vez de repintar la pantalla: un
2738      // `pintar()` aquí borraría lo que el usuario ya estuviera escribiendo
2739      // en el nombre o el email.
2740      var sub = caja.querySelector(".re-rec-sub");
2741      if (sub && estado.fase === "preguntas" && pasoActual() === "datos") {
2742        sub.innerHTML = subtituloDatosHTML();
2743      }
2744    }).catch(function (err) {
2745      console.warn("[re-recomendador] no se pudo precargar el reparto", err);
2746      estado.top3Clave = null;   // que `enviar` lo reintente
2747    });
2748  }
2749
2750  function avanzar() {
2751    estado.error = "";
2752    estado.indice += 1;
2753    pintar();
2754  }
2755
2756  function elegirOpcion(valor) {
2757    var paso = pasoActual();
2758    var lista = LISTAS[paso];
2759    if (!lista) return;
2760    var op = lista.find(function (o) { return o.value === valor; });
2761    if (!op) return;
2762    guardarCampos();
2763
2764    if (paso === "donde") {
2765      estado.modalidad = op.value;
2766      medir("recomendador_respuesta", { paso: "modalidad", valor: op.value });
2767      // Elegir modalidad avanza solo, salvo cuando eso destapa el campo de
2768      // ciudad: ahí hay que quedarse en la pantalla para que lo rellene.
2769      if (pideCiudad()) { estado.error = ""; return pintar(); }
2770    } else if (paso === "objetivo") {
2771      estado.objetivo = op.value;
2772      estado.objetivoRespondido = true;
2773      // El objetivo vacío es la opción "Aún no lo tengo claro", no un fallo:
2774      // se le pone nombre para poder contarla como una respuesta más.
2775      medir("recomendador_respuesta", { paso: "objetivo", valor: op.value || "Aun no lo tengo claro" });
2776    } else if (paso === "cuando") {
2777      estado.cuando = op.value;
2778      medir("recomendador_respuesta", { paso: "cuando", valor: op.value });
2779    }
2780
2781    avanzar();
2782  }
2783
2784  function validarYAvanzar() {
2785    var input = document.getElementById("re-rec-ciudad");
2786    if (!input) return;
2787    var ciudad = input.value.trim();
2788    if (!ciudad) return mostrarError("Dinos en qué ciudad buscas.");
2789    estado.ciudad = ciudad;
2790    // La ciudad va tal cual la escribe: saber que piden Albacete y que ahí no
2791    // hay nadie vale más que el propio embudo.
2792    medir("recomendador_respuesta", { paso: "ciudad", valor: ciudad });
2793    avanzar();
2794  }
2795
2796  function mostrarError(texto) {
2797    estado.error = texto;
2798    var el = caja.querySelector(".re-rec-error");
2799    if (el) { el.textContent = texto; el.style.display = "block"; }
2800  }
2801
2802  // Cualquier repintado tira el contenido de los campos, así que antes de
2803  // movernos se guarda lo que haya escrito. Sin esto, quien vuelve atrás a
2804  // cambiar el objetivo se encuentra el nombre y el email en blanco al
2805  // regresar, y eso es un abandono servido en bandeja.
2806  function guardarCampos() {
2807    var campos = { nombre: "re-rec-nombre", email: "re-rec-email", telefono: "re-rec-telefono", ciudad: "re-rec-ciudad" };
2808    Object.keys(campos).forEach(function (clave) {
2809      var el = document.getElementById(campos[clave]);
2810      if (el) estado[clave] = el.value.trim();
2811    });
2812  }
2813
2814  function retroceder() {
2815    guardarCampos();
2816    medir("recomendador_atras", { desde: pasoActual() });
2817    estado.error = "";
2818    if (estado.indice > 0) estado.indice -= 1;
2819    pintar();
2820  }
2821
2822  // ----------------------------------------------------------------- envío
2823
2824  // re-validar: copia inline de `_shared/validacion-contacto.ts`. Los embeds de
2825  // Webflow son sueltos y no pueden importar nada, así que cuando se toque
2826  // aquel fichero hay que tocar también esto. Deja el número en E.164 o
2827  // devuelve null: 9 dígitos se asumen españoles, 00 pasa a +, y lo que no
2828  // encaje se rechaza en vez de guardarse en crudo.
2829  function reNormalizarTelefono(valor) {
2830    if (typeof valor !== "string") return null;
2831    var limpio = valor.replace(/[\s\-.()]/g, "");
2832    if (!limpio) return null;
2833    var candidato;
2834    if (limpio.charAt(0) === "+") candidato = limpio;
2835    else if (limpio.slice(0, 2) === "00") candidato = "+" + limpio.slice(2);
2836    else if (/^\d{9}$/.test(limpio)) candidato = "+34" + limpio;
2837    else return null;
2838    return /^\+\d{6,15}$/.test(candidato) ? candidato : null;
2839  }
2840
2841  function enviar() {
2842    var nombre = (document.getElementById("re-rec-nombre").value || "").trim();
2843    var email = (document.getElementById("re-rec-email").value || "").trim();
2844    var telefono = (document.getElementById("re-rec-telefono").value || "").trim();
2845
2846    estado.nombre = nombre;
2847    estado.email = email;
2848    estado.telefono = telefono;
2849
2850    if (!nombre) {
2851      medir("recomendador_datos_invalidos", { campo: "nombre" });
2852      return mostrarError("Introduce tu nombre.");
2853    }
2854    // El email es el campo obligatorio desde que existe el chat: es lo que
2855    // permite avisar de un mensaje y lo que abre la cuenta.
2856    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
2857      medir("recomendador_datos_invalidos", { campo: "email" });
2858      return mostrarError(email
2859        ? "Revisa el email, parece que falta algo."
2860        : "Introduce tu email para que puedan escribirte.");
2861    }
2862    // El teléfono es opcional, pero si lo escribe tiene que servir para llamar.
2863    // Se manda ya normalizado para que el backend guarde la misma forma.
2864    var telefonoE164 = telefono ? reNormalizarTelefono(telefono) : null;
2865    if (telefono && !telefonoE164) {
2866      medir("recomendador_datos_invalidos", { campo: "telefono" });
2867      return mostrarError("Revisa el teléfono: 9 dígitos, o con prefijo internacional (+34…).");
2868    }
2869
2870    estado.enviando = true;
2871    estado.error = "";
2872    pintar();
2873
2874    // Si la precarga de esta misma búsqueda ya trajo el reparto, se manda a
2875    // esos: son exactamente los que se le acaban de nombrar en pantalla.
2876    var yaEstan = estado.top3.length && estado.top3Clave === claveBusqueda();
2877    (yaEstan ? Promise.resolve(estado.top3) : buscarTop3()).then(function (top3) {
2878      if (!top3.length) throw new Error("sin_entrenadores");
2879      estado.top3 = top3;
2880
2881      return rest("/functions/v1/solicitar-contacto-entrenador", {
2882        method: "POST",
2883        body: {
2884          entrenador_slugs: top3.map(function (e) { return e.slug; }),
2885          nombre: nombre,
2886          email: email,
2887          telefono: telefonoE164,
2888          zona: estado.ciudad || null,
2889          objetivo: estado.objetivo || null,
2890          modalidad: estado.modalidad || null,
2891          cuando_empieza: estado.cuando || null,
2892          tipo_solicitud: "multiple",
2893          origen: "popup-recomendador"
2894        }
2895      });
2896    }).then(function (datos) {
2897      escribir(CLAVE_APAGADO, "enviado", localStorage);
2898      estado.claimToken = (datos && datos.claim_token) || null;
2899      estado.enviando = false;
2900      estado.fase = "confirmacion";
2901      pintar();
2902      medir("recomendador_solicitud_enviada", {
2903        entrenadores: estado.top3.length,
2904        ciudad: estado.ciudad || null,
2905        objetivo: estado.objetivo || "Aun no lo tengo claro",
2906        modalidad: estado.modalidad || null,
2907        cuando: estado.cuando || null,
2908        // El email es obligatorio, así que no se mide: sería siempre true. El
2909        // teléfono sí, que ahora es el opcional y su tasa es justo lo que dice
2910        // si el cambio ha costado el dato con el que el entrenador llama.
2911        con_telefono: !!telefonoE164,
2912        segundos: estado.abiertoEn ? Math.round((Date.now() - estado.abiertoEn) / 1000) : null
2913      });
2914    }).catch(function (err) {
2915      console.error("[re-recomendador] envío", err);
2916      // "sin_entrenadores" no es una avería: es demanda en una ciudad sin
2917      // cobertura. Separarlo del fallo de red es la diferencia entre arreglar
2918      // el widget y salir a captar entrenadores en esa ciudad.
2919      medir("recomendador_envio_fallido", {
2920        motivo: err.message === "sin_entrenadores" ? "sin_entrenadores" : "error",
2921        ciudad: estado.ciudad || null
2922      });
2923      estado.enviando = false;
2924      estado.error = err.message === "sin_entrenadores"
2925        ? "No hemos encontrado entrenadores para esa búsqueda. Prueba con otra ciudad."
2926        : "No hemos podido enviar tu solicitud. Inténtalo de nuevo en un momento.";
2927      pintar();
2928    });
2929  }
2930
2931  function guardarPerfil() {
2932    var btn = caja.querySelector('[data-accion="guardar-perfil"]');
2933    var errEl = document.getElementById("re-rec-p2-error");
2934    var numero = function (id) {
2935      var v = (document.getElementById(id).value || "").trim();
2936      return v === "" ? null : Number(v);
2937    };
2938
2939    btn.disabled = true;
2940    btn.textContent = "Guardando…";
2941    if (errEl) errEl.style.display = "none";
2942
2943    rest("/functions/v1/completar-perfil-usuario", {
2944      method: "POST",
2945      body: {
2946        claim_token: estado.claimToken,
2947        // El email ya viajó con la solicitud en el paso 1. Mandarlo otra vez
2948        // no aportaría nada: el RPC respeta el que la ficha ya tiene.
2949        email: null,
2950        objetivo: estado.objetivo || null,
2951        modalidad: estado.modalidad || null,
2952        zona: estado.ciudad || null,
2953        edad: numero("re-rec-edad"),
2954        peso_kg: numero("re-rec-peso"),
2955        altura_cm: numero("re-rec-altura"),
2956        disponibilidad: (document.getElementById("re-rec-disponibilidad").value || "").trim() || null,
2957        nivel_actividad: document.getElementById("re-rec-actividad").value || null,
2958        comentarios: (document.getElementById("re-rec-notas").value || "").trim() || null
2959      }
2960    }).then(function (datos) {
2961      // La cuenta se ofrece porque la ficha tiene email: lo dio en el paso 1 y
2962      // la solicitud se creó con él. Ya no se mira `email_guardado`, que desde
2963      // este cambio viene siempre a false —significa "no he escrito nada nuevo
2964      // porque la ficha ya tenía email", que es exactamente lo que pasa ahora—
2965      // y usarlo escondía la oferta de cuenta a todo el mundo.
2966      var ofreceCuenta = !!estado.email;
2967      medir("recomendador_perfil_guardado", {});
2968      caja.innerHTML = cierreHTML(true, ofreceCuenta);
2969    }).catch(function (err) {
2970      console.error("[re-recomendador] paso 2", err);
2971      if (errEl) {
2972        errEl.textContent = "No hemos podido guardarlo. Tu solicitud sí se envió, no te preocupes.";
2973        errEl.style.display = "block";
2974      }
2975      btn.disabled = false;
2976      btn.textContent = "Enviar mis datos";
2977    });
2978  }
2979
2980  function crearCuenta() {
2981    var btn = caja.querySelector('[data-accion="crear-cuenta"]');
2982    var errEl = document.getElementById("re-rec-cuenta-error");
2983    btn.disabled = true;
2984    btn.textContent = "Creando…";
2985    if (errEl) errEl.style.display = "none";
2986
2987    rest("/functions/v1/crear-cuenta-usuario", {
2988      method: "POST",
2989      body: { claim_token: estado.claimToken }
2990    }).then(function (datos) {
2991      medir("recomendador_cuenta_creada", { requiere_email: !!datos.requiere_email });
2992      // Cuenta nueva: se entra en el acto. Si ya existía, la función manda el
2993      // enlace al buzón, que ahí es lo único que prueba que el email es suyo.
2994      if (datos.requiere_email) {
2995        btn.parentElement.innerHTML =
2996          "<h4>Ya tienes cuenta</h4>" +
2997          '<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>';
2998      } else {
2999        btn.textContent = "Entrando…";
3000        window.location.href = datos.url;
3001      }
3002    }).catch(function (err) {
3003      console.error("[re-recomendador] crear cuenta", err);
3004      if (errEl) {
3005        errEl.textContent = "No hemos podido crear la cuenta. Tu solicitud sí se envió; inténtalo de nuevo en un momento.";
3006        errEl.style.display = "block";
3007      }
3008      btn.disabled = false;
3009      btn.textContent = "Crear mi cuenta";
3010    });
3011  }
3012
3013  // Amplitude no envía nada hasta que cookieyes acepte analytics; hasta
3014  // entonces los eventos se acumulan en su cola. Nunca se cuelga una conversión de
3015  // Ads de este pop-up: el activador de GTM de las landings de paid va por el
3016  // selector `.re-modal-success`, y por eso aquí no se usa esa clase.
3017  //
3018  // No hace falta encolar nada aquí: el bloque de Amplitude del head del sitio
3019  // deja un stub `{ _q: [], track }` desde el primer momento y vacía esa cola
3020  // cuando el módulo arranca, así que un `track` disparado antes del
3021  // consentimiento no se pierde. Si el consentimiento no llega nunca, la cola
3022  // se descarta sin enviarse, que es justo lo que debe pasar.
3023  function medir(evento, props) {
3024    if (!estado.midiendo) return;
3025    var datos = props || {};
3026    // Segmentar por página es lo que distingue "el pop-up funciona" de "el
3027    // pop-up funciona en las páginas de ciudad y en la home no".
3028    datos.pagina = location.pathname;
3029    try {
3030      if (window.amplitude && window.amplitude.track) window.amplitude.track(evento, datos);
3031    } catch (e) {}
3032  }
3033
3034  // Una misma pantalla se repinta varias veces —al destapar "Ver más", al
3035  // abrirse el campo de ciudad, al enseñar un error— y ninguno de esos
3036  // repintados es una pantalla nueva. Contando solo la primera, el embudo mide
3037  // personas y no renders.
3038  function medirPantalla() {
3039    var lista = pasos();
3040    var clave = estado.fase === "confirmacion" ? "confirmacion" : pasoActual();
3041    if (estado.vistas[clave]) return;
3042    estado.vistas[clave] = true;
3043    medir("recomendador_paso_visto", {
3044      paso: clave,
3045      numero: estado.fase === "confirmacion" ? lista.length + 1 : estado.indice + 1,
3046      total: lista.length
3047    });
3048  }
3049
3050  // ------------------------------------------------------------ abrir/cerrar
3051
3052  function abrir(motivo) {
3053    if (!overlay || overlay.classList.contains("re-rec-abierto")) return;
3054    escribir(CLAVE_SESION, "1", sessionStorage);
3055    estado.indice = 0;
3056    estado.fase = "preguntas";
3057    estado.vistas = {};
3058    estado.abiertoEn = Date.now();
3059    estado.midiendo = motivo !== "preview" && motivo !== "debug";
3060    // Va antes de `pintar()` para que la apertura quede por delante del primer
3061    // paso en el orden de eventos, y no detrás.
3062    medir("recomendador_popup_abierto", {
3063      motivo: motivo || "manual",
3064      ciudad_de_pagina: estado.ciudadDePagina,
3065      objetivo_de_pagina: estado.objetivoDePagina,
3066      pasos: pasos().length
3067    });
3068    pintar();
3069    overlay.classList.add("re-rec-abierto");
3070    document.body.style.overflow = "hidden";
3071    ajustarAlViewport();
3072    if (vv) {
3073      vv.addEventListener("resize", ajustarAlViewport);
3074      vv.addEventListener("scroll", ajustarAlViewport);
3075    }
3076  }
3077
3078  function cerrar() {
3079    if (!overlay) return;
3080    var estabaAbierto = overlay.classList.contains("re-rec-abierto");
3081    overlay.classList.remove("re-rec-abierto");
3082    document.body.style.overflow = "";
3083    if (vv) {
3084      vv.removeEventListener("resize", ajustarAlViewport);
3085      vv.removeEventListener("scroll", ajustarAlViewport);
3086    }
3087    soltarViewport();
3088    // El evento más útil de todos: dice en qué pantalla exacta se cae la gente.
3089    if (estabaAbierto) {
3090      medir("recomendador_cerrado", {
3091        paso: estado.fase === "preguntas" ? pasoActual() : estado.fase,
3092        habia_enviado: estado.fase !== "preguntas",
3093        segundos: estado.abiertoEn ? Math.round((Date.now() - estado.abiertoEn) / 1000) : null
3094      });
3095    }
3096    // Solo se apaga para siempre si ya había enviado; cerrar en la primera
3097    // pregunta no debería castigarle a no volver a verlo nunca en el sitio.
3098    if (estado.fase !== "preguntas") escribir(CLAVE_APAGADO, "enviado", localStorage);
3099  }
3100
3101  // Con otro modal abierto no se dispara: en la ficha del entrenador convive
3102  // con el pop-up de contacto ("Nuevo contacto v2"), y dos modales apilados
3103  // es la forma más rápida de perder el lead que ya estaba escribiendo.
3104  function hayOtroModalAbierto() {
3105    // El banner de cookies entra en la lista: a un visitante nuevo le sale
3106    // CookieYes nada más entrar, y taparlo con este pop-up es a la vez una
3107    // mala experiencia y un problema de consentimiento.
3108    var candidatos = [
3109      ".pop-up-contacto",
3110      "#re-overlay.re-open-modal-overlay",
3111      ".re-modal-overlay",
3112      "[data-re-modal-abierto]",
3113      ".cky-consent-container",
3114      ".cky-modal"
3115    ];
3116    return candidatos.some(function (sel) {
3117      var el = document.querySelector(sel);
3118      if (!el) return false;
3119      // No basta con mirar `display`: los pop-ups de Webflow los abre y cierra
3120      // IX2, que muchas veces deja el elemento en el flujo y solo le anima la
3121      // opacidad. Dando por abierto todo lo que no sea `display:none`, este
3122      // pop-up no volvería a salir nunca en las 95 fichas de entrenador — y
3123      // sin un error en consola que lo delate. Se mide lo que de verdad se ve.
3124      var estilo = window.getComputedStyle(el);
3125      if (estilo.display === "none" || estilo.visibility === "hidden") return false;
3126      if (Number(estilo.opacity) === 0) return false;
3127      var caja = el.getBoundingClientRect();
3128      return caja.width > 0 && caja.height > 0;
3129    });
3130  }
3131
3132  function puedeSalir() {
3133    if (leer(CLAVE_APAGADO, localStorage)) return false;
3134    if (leer(CLAVE_SESION, sessionStorage)) return false;
3135    if (hayOtroModalAbierto()) return false;
3136    // Si está escribiendo en cualquier formulario de la página, no se le
3137    // interrumpe: quien ya está rellenando algo no necesita que le recomienden nada.
3138    var activo = document.activeElement;
3139    if (activo && /^(INPUT|TEXTAREA|SELECT)$/.test(activo.tagName)) return false;
3140    return true;
3141  }
3142
3143  // ------------------------------------------------------------------ init
3144
3145  function excluida() {
3146    var ruta = location.pathname;
3147    return RUTAS_EXCLUIDAS.some(function (r) { return ruta.indexOf(r) === 0; });
3148  }
3149
3150  function init() {
3151    if (excluida()) return;
3152
3153    overlay = document.createElement("div");
3154    overlay.id = "re-rec-overlay";
3155    overlay.innerHTML = '<div class="re-rec-modal" role="dialog" aria-modal="true" aria-label="Te ayudamos a decidir"></div>';
3156    document.body.appendChild(overlay);
3157    caja = overlay.querySelector(".re-rec-modal");
3158
3159    estado.ciudad = ciudadDeLaPagina();
3160    estado.ciudadDePagina = !!estado.ciudad;
3161    var obj = objetivoDeLaPagina();
3162    if (obj) { estado.objetivo = obj; estado.objetivoDePagina = true; }
3163
3164    overlay.addEventListener("click", function (ev) {
3165      if (ev.target === overlay) return cerrar();
3166      var btn = ev.target.closest("[data-accion]");
3167      if (!btn) return;
3168      var accion = btn.getAttribute("data-accion");
3169      if (accion === "cerrar") cerrar();
3170      else if (accion === "opcion") elegirOpcion(btn.getAttribute("data-valor"));
3171      else if (accion === "ver-mas") {
3172        // Si mucha gente lo pulsa, las opciones visibles que fija `VISIBLES` no
3173        // son las que cubren a la gente que llega de verdad.
3174        medir("recomendador_ver_mas", { paso: pasoActual() });
3175        estado.abiertas[pasoActual()] = true;
3176        pintar();
3177      }
3178      else if (accion === "siguiente") validarYAvanzar();
3179      else if (accion === "atras") retroceder();
3180      else if (accion === "enviar") enviar();
3181      else if (accion === "guardar-perfil") guardarPerfil();
3182      else if (accion === "crear-cuenta") crearCuenta();
3183    });
3184
3185    document.addEventListener("keydown", function (ev) {
3186      if (ev.key === "Escape" && overlay.classList.contains("re-rec-abierto")) cerrar
3186();
3187    });
3188
3189    // Disparo por tiempo o por scroll, lo que llegue antes.
3190    var disparado = false;
3191    var entrada = Date.now();
3192
3193    function disparar(motivo) {
3194      if (disparado) return false;
3195      if (!puedeSalir()) return false;
3196      disparado = true;
3197      abrir(motivo);
3198      return true;
3199    }
3200
3201    // 30/08/2026: sale nada más entrar, no a los 25 segundos. Lo único que se
3202    // espera es a que CookieYes pinte y despache su banner —`puedeSalir` ya lo
3203    // cuenta como modal abierto, pero tarda unas décimas en aparecer y sin esa
3204    // espera mínima el pop-up se le adelantaba y se quedaba debajo—, así que la
3205    // espera baja de 25 segundos a segundo y medio.
3206    //
3207    // El disparo se reintenta en vez de perderse: en el primer intento puede
3208    // haber un motivo temporal para no salir (el banner de cookies sin
3209    // responder, otro modal abierto, el usuario escribiendo en un campo) y con
3210    // un `setTimeout` suelto esa única oportunidad se gastaba ahí. El sondeo
3211    // baja a 400 ms —antes 5 s— para que en cuanto se aceptan las cookies el
3212    // pop-up salga acto seguido y no hasta cinco segundos después.
3213    //
3214    // `SEGUNDOS_ESPERA` del bloque 2 se queda sin usar a propósito: reescribir
3215    // aquel embed de 34 KB solo para bajar una constante no compensa.
3216    var ESPERA_MS = 1500;
3217    var reintento = setInterval(function () {
3218      var transcurrido = Date.now() - entrada;
3219      if (disparado || transcurrido > 180000) return clearInterval(reintento);
3220      if (transcurrido >= ESPERA_MS) disparar("tiempo");
3221    }, 400);
3222
3223    window.addEventListener("scroll", function () {
3224      var alto = document.documentElement.scrollHeight - window.innerHeight;
3225      if (alto <= 0) return;
3226      if (window.scrollY / alto >= SCROLL_DISPARO) disparar("scroll");
3227    }, { passive: true });
3228
3229    // Gancho manual: para un botón "¿No lo tienes claro?" en cualquier sitio
3230    // de la página, y para poder probarlo sin esperar al disparo automático.
3231    window.reRecomendador = {
3232      abrir: function () { abrir("manual"); },
3233      cerrar: cerrar,
3234      reset: function () {
3235        try { localStorage.removeItem(CLAVE_APAGADO); sessionStorage.removeItem(CLAVE_SESION); } catch (e) {}
3236      },
3237      // Pinta la confirmación con el reparto real de una búsqueda, pero SIN
3238      // enviar nada: sirve para revisar el copy y las fichas sin avisar a tres
3239      // entrenadores de verdad cada vez que se quiere mirar esa pantalla.
3240      // `tokenFalso` sirve para ver el bloque de perfil y el de crear cuenta,
3241      // que solo se pintan cuando hay `claim_token`. Guardar desde ahí falla
3242      // (el token no existe), que es justo lo que debe pasar.
3243      previsualizarConfirmacion: function (ciudad, objetivo, modalidad, tokenFalso) {
3244        estado.ciudad = ciudad || estado.ciudad;
3245        estado.objetivo = objetivo || "";
3246        estado.modalidad = modalidad || "En un gimnasio";
3247        return buscarTop3().then(function (top3) {
3248          // abrir() reinicia el estado al primer paso, así que va antes de
3249          // dejar la fase en "confirmacion" y no después.
3250          abrir("preview");
3251          estado.top3 = top3;
3252          estado.claimToken = tokenFalso ? "token-de-prueba" : null;
3253          estado.fase = "confirmacion";
3254          pintar();
3255          return top3.map(function (e) { return e.slug; });
3256        });
3257      }
3258    };
3259
3260    if (new URLSearchParams(location.search).get("recomendador") === "1") abrir("debug");
3261  }
3262
3263  // Si en consola hay VERSION pero no LISTO, es que falta el segundo embed.
3264  window.RE_RECOMENDADOR_LISTO = true;
3265
3266  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
3267  else init();
3268})();
3269</script>
3269
3270</div><div class="w-embed w-script"><!-- RedEntrenadores - Origen de campana (embed en el componente Footer V1) -->
3271<!-- Va en el Footer V1 porque tiene que correr en las 115 paginas: el custom
3272     code de sitio haria lo mismo, pero ese campo son 20 KB y reescribirlo
3273     entero por API es justo el tamano que se corta a medias.
3274
3275     El problema que resuelve: cada formulario leia `document.referrer` en el
3276     momento de enviarse, y para entonces ya era interno. Quien llegaba por un
3277     enlace de campana, aterrizaba en una landing y contactaba desde la ficha
3278     de un entrenador nacia en `usuarios` sin origen ninguno. Y las utm de la
3279     URL de llegada se perdian en el primer salto.
3280
3281     Hace tres cosas:
3282     1. Guarda la primera llegada de la sesion y la sirve en `window.rtOrigen()`.
3283     2. Envuelve `fetch` para que toda solicitud a `solicitar-contacto-entrenador`
3284        lleve el origen. Asi el formulario de la ficha (22 KB) y el de la landing
3285        de paid quedan cubiertos sin tocarles una linea.
3286     3. Arrastra las utm a los enlaces que apuntan a /encuentra-entrenador, que
3287        es el unico sitio del que el wizard las lee.
3288
3289     Por que (3) en vez de envolver tambien la RPC del wizard: supabase-js
3290     resuelve `fetch` al construir el cliente, y ese cliente nace al parsearse
3291     su embed, antes que este. La envoltura no le llegaria.
3292
3293     Fuente versionada: widgets/widget-origen-campana.html -->
3294<script>
3295(function () {
3296  var CLAVE = 're-origen';
3297  var ENDPOINT_CONTACTO = '/functions/v1/solicitar-contacto-entrenador';
3298  var DESTINO_WIZARD = '/encuentra-entrenador';
3299
3300  // Copia en memoria: en navegacion privada o con el almacenamiento capado,
3301  // sessionStorage lanza al escribir. Sin esto el origen se perderia en el
3302  // primer salto interno justo en los navegadores mas restrictivos.
3303  var enMemoria = null;
3304
3305  function leer() {
3306    try {
3307      var crudo = sessionStorage.getItem(CLAVE);
3308      if (crudo) return JSON.parse(crudo);
3309    } catch (e) { /* almacenamiento capado: tiramos de la copia en memoria */ }
3310    return enMemoria;
3311  }
3312
3313  function guardar(o) {
3314    enMemoria = o;
3315    try { sessionStorage.setItem(CLAVE, JSON.stringify(o)); } catch (e) {}
3316  }
3317
3318  function referenteExterno() {
3319    try {
3320      if (!document.referrer) return null;
3321      var r = new URL(document.referrer);
3322      if (r.hostname === location.hostname) return null;
3323      // Sin query: el referente de un buscador lleva la consulta del usuario.
3324      return r.origin + r.pathname;
3325    } catch (e) { return null; }
3326  }
3327
3328  var params = new URLSearchParams(location.search);
3329  var llegada = {
3330    pagina: location.pathname,
3331    referrer: referenteExterno(),
3332    utm_source: params.get('utm_source'),
3333    utm_medium: params.get('utm_medium'),
3334    utm_campaign: params.get('utm_campaign')
3335  };
3336
3337  // Manda el primer toque de la sesion, con una excepcion: una llegada nueva
3338  // CON utm_source la pisa. Asi quien vuelve desde un enlace de campana se
3339  // atribuye a esa campana y no al referente con el que abrio el navegador
3340  // hace media hora.
3341  if (!leer() || llegada.utm_source) guardar(llegada);
3342
3343  // Devuelve siempre las cinco claves: quien lo consume puede volcarlo en el
3344  // payload sin comprobar nada.
3345  window.rtOrigen = function () {
3346    var o = leer() || {};
3347    return {
3348      pagina: o.pagina || location.pathname,
3349      referrer: o.referrer || null,
3350      utm_source: o.utm_source || null,
3351      utm_medium: o.utm_medium || null,
3352      utm_campaign: o.utm_campaign || null
3353    };
3354  };
3355
3356  // 2. El origen viaja con cada solicitud de contacto
3357  //
3358  // Envolver `fetch` en vez de editar los formularios: son embeds de 22 y 40
3359  // KB y reescribirlos por API no cabe. La envoltura es defensiva de arriba
3360  // abajo: cualquier fallo cae al fetch original con los argumentos tal cual,
3361  // porque aqui pasa el envio del formulario de contacto y romperlo significa
3362  // perder leads.
3363  var fetchOriginal = window.fetch;
3364
3365  if (typeof fetchOriginal === 'function') {
3366    window.fetch = function (entrada, opciones) {
3367      try {
3368        var url = typeof entrada === 'string' ? entrada : (entrada && entrada.url) || '';
3369        if (url.indexOf(ENDPOINT_CONTACTO) !== -1 && opciones && typeof opciones.body === 'string') {
3370          var cuerpo = JSON.parse(opciones.body);
3371          if (cuerpo && typeof cuerpo === 'object' && !Array.isArray(cuerpo)) {
3372            var o = window.rtOrigen();
3373            // Solo se rellena lo que falta: un formulario que ya mande su
3374            // origen manda el.
3375            if (!cuerpo.pagina_origen) cuerpo.pagina_origen = location.pathname;
3376            if (!cuerpo.referrer_externo && o.referrer) cuerpo.referrer_externo = o.referrer;
3377            if (!cuerpo.utm_source && o.utm_source) cuerpo.utm_source = o.utm_source;
3378            if (!cuerpo.utm_medium && o.utm_medium) cuerpo.utm_medium = o.utm_medium;
3379            if (!cuerpo.utm_campaign && o.utm_campaign) cuerpo.utm_campaign = o.utm_campaign;
3380
3381            var copia = {};
3382            for (var k in opciones) { if (Object.prototype.hasOwnProperty.call(opciones, k)) copia[k] = opciones[k]; }
3383            copia.body = JSON.stringify(cuerpo);
3384            return fetchOriginal.call(this, entrada, copia);
3385          }
3386        }
3387      } catch (e) { /* cuerpo ilegible o lo que sea: va el original */ }
3388      return fetchOriginal.apply(this, arguments);
3389    };
3390  }
3391
3392  // 3. Las utm llegan al wizard
3393  //
3394  // El wizard de /encuentra-entrenador lee las utm de SU url. Quien llega
3395  // desde una landing interna las habria perdido por el camino, asi que se
3396  // las pegamos a los enlaces que apuntan alli. Solo a esos: llenar de utm
3397  // todos los enlaces del sitio ensuciaria las URLs indexables y contaria
3398  // cada pagina interna como una visita de campana.
3399  function propagarAlWizard() {
3400    var o = window.rtOrigen();
3401    if (!o.utm_source && !o.utm_medium && !o.utm_campaign) return;
3402
3403    var enlaces = document.querySelectorAll('a[href*="' + DESTINO_WIZARD + '"]');
3404    Array.prototype.forEach.call(enlaces, function (a) {
3405      try {
3406        if (a.getAttribute('data-re-origen')) return;
3407        var u = new URL(a.href, location.origin);
3408        if (u.hostname !== location.hostname) return;
3409        if (u.pathname.indexOf(DESTINO_WIZARD) !== 0) return;
3410        if (o.utm_source && !u.searchParams.get('utm_source')) u.searchParams.set('utm_source', o.utm_source);
3411        if (o.utm_medium && !u.searchParams.get('utm_medium')) u.searchParams.set('utm_medium', o.utm_medium);
3412        if (o.utm_campaign && !u.searchParams.get('utm_campaign')) u.searchParams.set('utm_campaign', o.utm_campaign);
3413        a.setAttribute('href', u.pathname + u.search + u.hash);
3414        a.setAttribute('data-re-origen', '1');
3415      } catch (e) {}
3416    });
3417  }
3418
3419  if (document.readyState === 'loading') {
3420    document.addEventListener('DOMContentLoaded', propagarAlWizard);
3421  } else {
3422    propagarAlWizard();
3423  }
3424  // Los listados y el pop-up recomendador pintan enlaces despues de cargar.
3425  setTimeout(propagarAlWizard, 1500);
3426})();
3427</script>
3427</div></footer>
3427<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>
3427<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe/js/webflow.975777c1.283252e848e5c0e3.js" type="text/javascript" integrity="sha384-Zo/MMcWLLsMN+SlQPBtgBQDPWtbRCmp8ijx9WnkKyzy4q4eYV7FVR0+rAXA6bZmH" crossorigin="anonymous"></script>
3427<!-- Google Tag Manager (noscript) -->
vendor: 68 bytes, lines 3427-3428
3427
3428<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
3428GTM-KBD83QKS
vendor: 83 bytes, lines 3428-3429
3428"
3429height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
3430<!-- End Google Tag Manager (noscript) -->
3431
3432<!-- ══════════════════════════════════════════════════════════
3433     PREGUNTA AL EXPERTO — filtro por categoria + contador
3434     ══════════════════════════════════════════════════════════ -->
3435<style>
3436  .re-cats__link.is-active { background:#089F3B !important; border-color:#089F3B !important; color:#fff !important; }
3437  .re-cats__link.is-active [class*="re-cats__icon-"] { filter: brightness(0) invert(1); }
3438  .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; }
3439  .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; }
3440  .re-cats__reset:hover { background:#1B1E22; color:#fff; }
3441  .re-oq-title.is-filtered { font-size:20px !important; text-transform:none !important; line-height:1.25 !important; text-align:center; }
3442  .otras_preguntas-header:has(.re-oq-title.is-filtered) { display:block !important; }
3443  #re-oq-clear { display:block !important; width:100% !important; text-align:center !important; margin-top:8px !important; float:none !important; }
3444  /* Boton de continuidad al final del bloque */
3445  .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; }
3446  .re-oq-explorar:hover { background:#067A2D; }
3447</style>
3448<script>
3449(function () {
3450  if (window.location.pathname.indexOf('/pregunta-al-experto') !== 0) return;
3451
3452  var CAT = new URLSearchParams(window.location.search).get('categoria');
3453  var contadoresPintados = false;
3454  var yaColocado = false;
3455
3456  function textoVer(n) {
3457    if (!n) return 'Leer respuesta del entrenador →';
3458    if (n === 1) return 'Leer respuesta del entrenador →';
3459    return 'Leer las ' + n + ' respuestas →';
3460  }
3461
3462  function quitarFiltro(e) {
3463    if (e) { e.preventDefault(); e.stopPropagation(); }
3464    CAT = null;
3465    try { history.replaceState(null, '', window.location.pathname); } catch (err) {}
3466
3467    Array.prototype.forEach.call(document.querySelectorAll('.re-cats__link'), function (chip) {
3468      chip.classList.remove('is-active');
3469      var c = chip.querySelector('.re-cats__check');
3470      if (c) c.parentNode.removeChild(c);
3471      var orig = chip.getAttribute('data-re-href');
3472      if (orig) chip.setAttribute('href', orig);
3473    });
3474
3475    Array.prototype.forEach.call(document.querySelectorAll('.re-oq-card'), function (card) {
3476      var wrap = card.closest ? (card.closest('.w-dyn-item') || card) : card;
3477      wrap.style.setProperty('display', '', 'important');
3478    });
3479
3480    var header = document.querySelector('.re-oq-title');
3481    if (header) {
3482      header.removeAttribute('data-re-done');
3483      header.classList.remove('is-filtered');
3484      header.textContent = '¿Tienes prisa? Encuentra tu respuesta al instante';
3485    }
3486    var clear = document.getElementById('re-oq-clear');
3487    if (clear) clear.parentNode.removeChild(clear);
3488    var reset = document.getElementById('re-cats-reset');
3489    if (reset) reset.parentNode.removeChild(reset);
3490    var expl = document.getElementById('re-oq-explorar');
3491    if (expl) expl.style.display = 'none';
3492  }
3493
3494  function aplicar() {
3495    var chips = document.querySelectorAll('.re-cats__link');
3496    var cards = document.querySelectorAll('.re-oq-card');
3497
3498    Array.prototype.forEach.call(chips, function (chip) {
3499      if (!chip.getAttribute('data-re-href')) {
3500        chip.setAttribute('data-re-href', chip.getAttribute('href') || '');
3501      }
3502    });
3503
3504    if (CAT) {
3505      Array.prototype.forEach.call(chips, function (chip) {
3506        var orig = chip.getAttribute('data-re-href') || '';
3507        if (orig.indexOf('categoria=' + CAT) !== -1) {
3508          chip.classList.add('is-active');
3509          chip.setAttribute('title', 'Quitar filtro');
3510          if (!chip.getAttribute('data-re-toggle')) {
3511            chip.setAttribute('data-re-toggle', '1');
3512            chip.addEventListener('click', function (e) {
3513              if (chip.classList.contains('is-active')) quitarFiltro(e);
3514            });
3515          }
3516          if (!chip.querySelector('.re-cats__check')) {
3517            var c = document.createElement('span');
3518            c.className = 're-cats__check';
3519            c.textContent = '✕';
3520            chip.appendChild(c);
3521          }
3522        }
3523      });
3524
3525      var listaCats = document.querySelector('.re-cats__list');
3526      if (listaCats && !document.getElementById('re-cats-reset')) {
3527        var r = document.createElement('a');
3528        r.id = 're-cats-reset';
3529        r.className = 're-cats__reset';
3530        r.href = '#';
3531        r.textContent = 'Ver todas las categorias';
3532        r.addEventListener('click', quitarFiltro);
3533        listaCats.insertAdjacentElement('afterend', r);
3534      }
3535
3536      if (!yaColocado) {
3537        yaColocado = true;
3538        var seccionCats = document.querySelector('.re-cats');
3539        if (seccionCats && window.scrollY < 40) {
3540          var top = seccionCats.getBoundingClientRect().top + window.scrollY - 70;
3541          window.scrollTo(0, top > 0 ? top : 0);
3542        }
3543      }
3544    }
3545
3546    if (!cards.length) return false;
3547
3548    // Boton de continuidad al final del bloque de preguntas
3549    var seccionOQ = document.querySelector('.re-oq-section');
3550    var listaOQ = document.querySelector('.re-oq-section .w-dyn-list');
3551    if (seccionOQ && listaOQ && !document.getElementById('re-oq-explorar')) {
3552      var ex = document.createElement('a');
3553      ex.id = 're-oq-explorar';
3554      ex.className = 're-oq-explorar';
3555      ex.href = '/pregunta-al-experto';
3556      ex.textContent = 'Explorar toda la biblioteca de preguntas';
3557      ex.addEventListener('click', function (e) {
3558        if (CAT) { quitarFiltro(e); }
3559      });
3560      listaOQ.insertAdjacentElement('afterend', ex);
3561    }
3562    var explBtn = document.getElementById('re-oq-explorar');
3563    if (explBtn) explBtn.style.display = CAT ? 'block' : 'none';
3564
3565    if (CAT) {
3566      var filtro = CAT.trim().toLowerCase();
3567      var visibles = 0;
3568      Array.prototype.forEach.call(cards, function (card) {
3569        var h = card.querySelector('.re-oq-hidden');
3570        var cat = h ? (h.textContent || '').trim().toLowerCase() : '';
3571        var ok = (cat === filtro);
3572        var wrap = card.closest ? (card.closest('.w-dyn-item') || card) : card;
3573        wrap.style.setProperty('display', ok ? '' : 'none', 'important');
3574        if (ok) visibles++;
3575      });
3576
3577      var header = document.querySelector('.re-oq-title');
3578      if (header && !header.getAttribute('data-re-done')) {
3579        header.setAttribute('data-re-done', '1');
3580        header.classList.add('is-filtered');
3581        var nombre = filtro.replace(/-/g, ' ');
3582        nombre = nombre.charAt(0).toUpperCase() + nombre.slice(1);
3583        header.textContent = visibles > 0
3584          ? nombre + ' (' + visibles + ')'
3585          : 'Todavia no hay preguntas sobre ' + nombre.toLowerCase();
3586
3587        if (!document.getElementById('re-oq-clear')) {
3588          var a = document.createElement('a');
3589          a.id = 're-oq-clear';
3590          a.href = '#';
3591          a.textContent = 'Ver todas las preguntas →';
3592          a.style.cssText = 'display:block;width:100%;text-align:center;margin-top:8px;font-size:13px;font-weight:700;color:#089F3B;text-decoration:none;';
3593          a.addEventListener('click', quitarFiltro);
3594          var contenedor = header.parentElement || header;
3595          contenedor.insertAdjacentElement('afterend', a);
3596        }
3597      }
3598    }
3599
3600    Array.prototype.forEach.call(cards, function (card) {
3601      var detail = card.querySelector('.re-oq-detail');
3602      var toggle = card.querySelector('[data-oq-toggle]');
3603      if (!detail || !toggle || toggle.getAttribute('data-re-bound')) return;
3604      toggle.setAttribute('data-re-bound', '1');
3605      toggle.style.display = (detail.scrollHeight > detail.clientHeight + 2) ? 'block' : 'none';
3606      toggle.addEventListener('click', function (e) {
3607        e.preventDefault();
3608        e.stopPropagation();
3609        var exp = detail.classList.toggle('re-oq-detail--expanded');
3610        toggle.textContent = exp ? 'Ver menos' : 'Ver mas';
3611      });
3612    });
3613
3614    if (!contadoresPintados) {
3615      contadoresPintados = true;
3616      cargarSupabase(function (db) {
3617        // Nada de respuestas(count): PostgREST lo traduce a una referencia de
3618        // fila entera y la clave publicable tiene los permisos de `respuestas`
3619        // por columna (migracion 20260819150000), asi que devuelve 403. Se
3620        // piden los id de una columna concedida y se cuentan aqui.
3621        db.from('preguntas').select('slug, respuestas(id)').then(function (res) {
3622          if (res.error) { console.warn('[PAE] contador de respuestas:', res.error.message); return; }
3623          var mapa = {};
3624          (res.data || []).forEach(function (r) {
3625            mapa[r.slug] = (r.respuestas || []).length;
3626          });
3627          Array.prototype.forEach.call(document.querySelectorAll('.re-oq-card'), function (card) {
3628            var slug = (card.getAttribute('href') || '').split('/').filter(Boolean).pop();
3629            var v = card.querySelector('[data-oq-ver]');
3630            if (!v) return;
3631            v.textContent = textoVer(mapa[slug]);
3632          });
3633        });
3634      });
3635    }
3636
3637    return true;
3638  }
3639
3640  function cargarSupabase(cb) {
3641    function go() {
3642      try {
3643        cb(window.supabase.createClient(
3644          'https://yrzanatzshdpvypazgeb.supabase.co',
3645          'sb_publishable_O4cIYIJcDs3_GcbjNHdd9g_e2cQKOUD'
3646        ));
3647      } catch (e) {}
3648    }
3649    if (window.supabase && window.supabase.createClient) { go(); return; }
3650    var s = document.createElement('script');
3651    s.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2';
3652    s.onload = go;
3653    document.head.appendChild(s);
3654  }
3655
3656  function arrancar() {
3657    aplicar();
3658    var intentos = 0;
3659    var t = setInterval(function () {
3660      intentos++;
3661      aplicar();
3662      if (intentos > 20) clearInterval(t);
3663    }, 300);
3664
3665    var lista = document.querySelector('.w-dyn-list');
3666    if (lista && window.MutationObserver) {
3667      new MutationObserver(function () { aplicar(); }).observe(lista, { childList: true, subtree: true });
3668    }
3669  }
3670
3671  if (document.readyState === 'loading') {
3672    document.addEventListener('DOMContentLoaded', arrancar);
3673  } else {
3674    arrancar();
3675  }
3676})();
3677</script>
3677
3678
3679<!-- ══════════════════════════════════════════════════════════
3680     BOTÓN FLOTANTE DE WHATSAPP
3681     ══════════════════════════════════════════════════════════ -->
3682
3683<a
3684  href="https://wa.me/34633590498?text=Hola%2C%20me%20gustar%C3%ADa%20m%C3%A1s%20informaci%C3%B3n"
3685  target="_blank"
3686  rel="noopener noreferrer"
3687  class="wa-float-btn"
3688  aria-label="Contactar por WhatsApp"
3689>
3690  <svg viewBox="0 0 24 24" width="30" height="30" fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg">
3691    <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"/>
3692    <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"/>
3693  </svg>
3694</a>
3695
3696<style>
3697  .wa-float-btn {
3698    position: fixed;
3699    bottom: 24px;
3700    right: 24px;
3701    width: 60px;
3702    height: 60px;
3703    background-color: #25D366;
3704    border-radius: 50%;
3705    display: flex;
3706    align-items: center;
3707    justify-content: center;
3708    box-shadow: 0 4px 14px rgba(0,0,0,0.25);
3709    z-index: 9999;
3710    text-decoration: none;
3711    transition: transform 0.2s ease, box-shadow 0.2s ease;
3712  }
3713  .wa-float-btn:hover {
3714    transform: scale(1.08);
3715    box-shadow: 0 6px 18px rgba(0,0,0,0.3);
3716  }
3717  @media (max-width: 480px) {
3718    .wa-float-btn {
3719      width: 52px;
3720      height: 52px;
3721      bottom: 16px;
3722      right: 16px;
3723    }
3724  }
3725</style>
3726
3727<!-- ══════════════════════════════════════════════════════════
3728     OCULTAR SECCIONES CON LISTAS CMS VACÍAS (ciudad + objetivo)
3729     Solo listas de primer nivel: una lista anidada vacía dentro
3730     de una tarjeta (p. ej. las especialidades de un entrenador)
3731     no debe esconder la sección entera.
3732     ══════════════════════════════════════════════════════════ -->
3733<script>
3734document.querySelectorAll('.w-dyn-empty').forEach(function (emptyEl) {
3735  var lista = emptyEl.closest('.w-dyn-list');
3736  if (!lista) return;
3737
3738  // Lista anidada dentro de otra lista CMS (una tarjeta): no decide por la sección.
3739  if (lista.parentElement && lista.parentElement.closest('.w-dyn-list')) return;
3740
3741  // Webflow deja el estado vacio en el HTML aunque la lista tenga elementos:
3742  // lo tapa con w-dyn-hide. Sin esta comprobacion se escondia un bloque lleno.
3743  if (lista.querySelector('.w-dyn-item')) return;
3744
3745  // En la ficha del entrenador cada bloque es un .entrenador_sobre-m dentro de
3746  // una unica <section> que ademas lleva el formulario de contacto, asi que la
3747  // regla de abajo no llegaba y se quedaba a la vista el "No items found." de
3748  // Modalidad (Ismael Diaz, 20/08/2026). Aqui manda el bloque, no la seccion.
3749  var bloque = emptyEl.closest('.entrenador_sobre-m');
3750  if (bloque) {
3751    bloque.style.display = 'none';
3752    return;
3753  }
3754
3755  var section = emptyEl.closest('section');
3756  if (section && !section.querySelector('form')) {
3757    section.style.display = 'none';
3758  }
3759});
3760</script>
3760
3761
3762<!-- ══════════════════════════════════════════════════════════
3763     EMAIL — aviso de errata en el dominio (sugiere, no bloquea)
3764     Vale para TODOS los formularios del sitio, presentes y futuros:
3765     engancha por delegacion a cualquier <input> de email, no hay
3766     que tocar los widgets uno a uno.
3767     La regla vive en supabase/functions/_shared/validacion-contacto.ts
3768     (`sugerenciaDominioEmail`); esto es su copia de navegador.
3769     ══════════════════════════════════════════════════════════ -->
3770<style>
3771  .re-mailerrata { margin-top:6px; padding:8px 10px; border-radius:8px; background:#FEF3C7; color:#92400E; font-size:12.5px; line-height:1.35; }
3772  .re-mailerrata__fix { padding:0; border:0; background:none; font:inherit; font-weight:700; color:#92400E; text-decoration:underline; cursor:pointer; }
3773</style>
3774<script>
3775(function () {
3776  /* Dominios que damos por buenos. Un dominio de esta lista no se marca nunca,
3777     y un dominio a 1-2 letras de uno de ellos es el que se sugiere. */
3778  var DOMINIOS = [
3779    'gmail.com','googlemail.com',
3780    'hotmail.com','hotmail.es','hotmail.fr','hotmail.it','hotmail.co.uk','hotmail.com.ar','hotmail.com.mx',
3781    'outlook.com','outlook.es','outlook.fr','outlook.it',
3782    'live.com','live.com.mx','live.co.uk','msn.com',
3783    'yahoo.com','yahoo.es','yahoo.it','yahoo.fr','yahoo.co.uk','yahoo.com.ar','yahoo.com.mx',
3784    'icloud.com','me.com','mac.com',
3785    'aol.com','protonmail.com','proton.me','gmx.es','gmx.com','gmx.net','zoho.com','mail.com',
3786    'terra.es','telefonica.net','movistar.es','orange.es','wanadoo.es','ya.com','arrakis.es','euskalnet.net'
3787  ];
3788
3789  function distancia(a, b) {
3790    var m = a.length, n = b.length, i, j, fila = [], ant, tmp;
3791    for (j = 0; j <= n; j++) fila[j] = j;
3792    for (i = 1; i <= m; i++) {
3793      ant = fila[0]; fila[0] = i;
3794      for (j = 1; j <= n; j++) {
3795        tmp = fila[j];
3796        fila[j] = Math.min(fila[j] + 1, fila[j - 1] + 1, ant + (a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1));
3797        ant = tmp;
3798      }
3799    }
3800    return fila[n];
3801  }
3802
3803  /* Devuelve el email corregido o null. Solo mira el dominio, y solo cuando el
3804     email ya tiene forma valida: el "falta algo" lo dice el widget, no esto.
3805     El tope de 2 letras se reserva a dominios largos; en uno corto (ya.com,
3806     me.com) dos cambios ya son otro dominio distinto, no una errata. */
3807  function sugerencia(valor) {
3808    var limpio = String(valor == null ? '' : valor).trim().toLowerCase();
3809    if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(limpio)) return null;
3810    var corte = limpio.lastIndexOf('@');
3811    var local = limpio.slice(0, corte), dominio = limpio.slice(corte + 1);
3812    if (DOMINIOS.indexOf(dominio) !== -1) return null;
3813    var tope = dominio.length >= 8 ? 2 : 1;
3814    var mejor = null, mejorD = 3, i, d;
3815    for (i = 0; i < DOMINIOS.length; i++) {
3816      d = distancia(dominio, DOMINIOS[i]);
3817      if (d <= tope && d < mejorD) { mejorD = d; mejor = DOMINIOS[i]; }
3818    }
3819    return mejor ? local + '@' + mejor : null;
3820  }
3821
3822  function esCampoEmail(el) {
3823    if (!el || el.tagName !== 'INPUT') return false;
3824    var t = (el.getAttribute('type') || '').toLowerCase();
3825    if (t === 'email') return true;
3826    if (t && t !== 'text') return false;
3827    if ((el.getAttribute('inputmode') || '').toLowerCase() === 'email') return true;
3828    return /e-?mail|correo/i.test((el.name || '') + ' ' + (el.id || '') + ' ' + (el.getAttribute('autocomplete') || ''));
3829  }
3830
3831  function quitar(input) {
3832    var sig = input.nextElementSibling;
3833    if (sig && sig.classList && sig.classList.contains('re-mailerrata')) sig.parentNode.removeChild(sig);
3834  }
3835
3836  function medir(evento, dominio) {
3837    try { if (window.amplitude && window.amplitude.track) window.amplitude.track(evento, { dominio: dominio }); } catch (e) {}
3838  }
3839
3840  function revisar(input) {
3841    quitar(input);
3842    if (!input.isConnected) return;
3843    var propuesta = sugerencia(input.value);
3844    if (!propuesta) return;
3845
3846    var caja = document.createElement('div');
3847    caja.className = 're-mailerrata';
3848    caja.setAttribute('role', 'status');
3849    caja.appendChild(document.createTextNode('¿Quisiste decir '));
3850
3851    var btn = document.createElement('button');
3852    btn.type = 'button';
3853    btn.className = 're-mailerrata__fix';
3854    btn.textContent = propuesta;
3855    btn.addEventListener('click', function () {
3856      input.value = propuesta;
3857      try {
3858        input.dispatchEvent(new Event('input', { bubbles: true }));
3859        input.dispatchEvent(new Event('change', { bubbles: true }));
3860      } catch (e) {}
3861      quitar(input);
3862      input.focus();
3863      medir('email_errata_corregida', propuesta.split('@')[1]);
3864    });
3865
3866    caja.appendChild(btn);
3867    caja.appendChild(document.createTextNode('?'));
3868    input.insertAdjacentElement('afterend', caja);
3869    medir('email_errata_sugerida', input.value.split('@').pop().toLowerCase());
3870  }
3871
3872  /* Al escribir se propone con retardo (asi el aviso ya esta en pantalla antes
3873     de que llegue al boton de enviar) y al salir del campo, de inmediato. */
3874  var reloj = null;
3875  document.addEventListener('input', function (e) {
3876    if (!esCampoEmail(e.target)) return;
3877    quitar(e.target);
3878    clearTimeout(reloj);
3879    var input = e.target;
3880    reloj = setTimeout(function () { revisar(input); }, 500);
3881  });
3882  document.addEventListener('focusout', function (e) {
3883    if (!esCampoEmail(e.target)) return;
3884    clearTimeout(reloj);
3885    revisar(e.target);
3886  });
3887})();
3888</script>
3888
3889
3890<!-- Master Tag add just before the closing </body> tag -->
3891                    
3891<script src="https://www.dwin1.com/130189.js" type="text/javascript" defer="defer"></script>
vendor: 1 bytes, line 3891
3891
3892<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a815b2ecfc98b6d18a9afab%2Fvisitas_ficha_publica-1.0.0.js" type="text/javascript"></script>
3892<script src="https://cdn.prod.website-files.com/68c5a729c71fa3f37858fbbe%2F689e5ba67671442434f3ca35%2F6a9562a243b6dd3757592da5%2Flimpiarcamposcontacto-1.0.0.js" type="text/javascript"></script>
3892</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.