PageSourceSearch

https://www.forohotusaexplora.com/templates/cadenas/microsite2/js/scripts-fhe.js?

js forohotusaexplora.com collected 2026-10-07 00:04:34 UTC 44,006 bytes, 1,345 lines download raw bytes

1/* ==========================================================================
2   Foro Hotusa Explora — JavaScript
3   Una IIFE por componente, vanilla, con guard clause al inicio.
4   ========================================================================== */
5
6
7/** sta-fondoOlas-fhe -- INICIO */
8/* Fondo animado de olas de puntos, agnóstico del módulo.
9   Uso: añadir la clase .sta-fondoOlas-fhe a cualquier contenedor.
10   Opcionales, por data-* en ese mismo contenedor:
11     data-olas-color        "255, 255, 255" (RGB sin alfa) · por defecto blanco.
12                            Sobre módulos de fondo claro hay que pasar un color
13                            oscuro, ej. "47, 86, 162", o no se ven los puntos.
14     data-olas-separacion   distancia entre puntos en px · por defecto 18
15     data-olas-amplitud     desplazamiento vertical de la ola en px · por defecto 4.5 */
16(function (lista) {
17  if (!lista.length) return;
18
19  // Sin canvas o si se prefiere no animar, los bloques se quedan con su fondo plano
20  if (!document.createElement('canvas').getContext) return;
21  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
22
23  var numero = function (valor, porDefecto) {
24    var n = parseFloat(valor);
25    return isNaN(n) ? porDefecto : n;
26  };
27
28  var montar = function (o) {
29    var capa = document.createElement('div');
30    capa.className = 'sta-fondoOlas-fhe_capa';
31    capa.setAttribute('aria-hidden', 'true');
32
33    var canvas = document.createElement('canvas');
34    var ctx = canvas.getContext('2d');
35    capa.appendChild(canvas);
36    o.insertBefore(capa, o.firstChild);
37
38    var SEPARACION = Math.max(4, numero(o.getAttribute('data-olas-separacion'), 18));
39    var AMPLITUD = numero(o.getAttribute('data-olas-amplitud'), 4.5);
40    var COLOR = o.getAttribute('data-olas-color') || '255, 255, 255';
41
42    var RADIO = 1.3;        // radio base del punto
43    var RADIO_VAR = 0.8;    // cuánto crece con la cresta de la ola
44    var ALFA_MIN = 0.35;
45    var ALFA_MAX = 0.85;
46
47    var dpr = 1;
48    var ancho = 0;
49    var alto = 0;
50    var puntos = [];
51    var tiempo = 0;
52    var ultimo = 0;
53    var rafId = 0;
54    var visible = false;
55
56    // Rejilla de puntos, con una variación fija por punto para romper la uniformidad
57    var construir = function () {
58      var cols = Math.ceil(ancho / SEPARACION) + 1;
59      var filas = Math.ceil(alto / SEPARACION) + 1;
60      var offsetX = (ancho - (cols - 1) * SEPARACION) * 0.5;
61      var offsetY = (alto - (filas - 1) * SEPARACION) * 0.5;
62
63      puntos = [];
64      for (var ix = 0; ix < cols; ix++) {
65        for (var iy = 0; iy < filas; iy++) {
66          puntos.push({
67            x: offsetX + ix * SEPARACION,
68            y: offsetY + iy * SEPARACION,
69            r: Math.random()
70          });
71        }
72      }
73    };
74
75    var medir = function () {
76      var w = Math.max(1, Math.round(o.offsetWidth));
77      var h = Math.max(1, Math.round(o.offsetHeight));
78      if (w === ancho && h === alto) return false;
79
80      ancho = w;
81      alto = h;
82      dpr = Math.min(window.devicePixelRatio || 1, 2);
83
84      canvas.width = Math.round(ancho * dpr);
85      canvas.height = Math.round(alto * dpr);
86      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
87
88      construir();
89      return true;
90    };
91
92    var pintar = function () {
93      ctx.clearRect(0, 0, ancho, alto);
94
95      for (var i = 0; i < puntos.length; i++) {
96        var p = puntos[i];
97
98        // Cuatro ondas cruzadas, como en el fondo de referencia
99        var e = Math.sin(p.x * 0.010 + tiempo * 0.9) * 0.55
100              + Math.sin(p.y * 0.013 - tiempo * 0.7) * 0.45
101              + Math.sin((p.x + p.y) * 0.008 + tiempo * 1.1) * 0.40
102              + Math.sin((p.x - p.y) * 0.011 - tiempo * 1.3) * 0.30;
103
104        var h = e * 0.5 + 0.5;
105        if (h < 0) h = 0;
106        if (h > 1) h = 1;
107
108        var alfa = (ALFA_MIN + (ALFA_MAX - ALFA_MIN) * h) * (0.9 + p.r * 0.15);
109
110        ctx.beginPath();
111        ctx.arc(p.x, p.y + e * AMPLITUD, RADIO + h * RADIO_VAR, 0, 6.2832);
112        ctx.fillStyle = 'rgba(' + COLOR + ',' + alfa.toFixed(3) + ')';
113        ctx.fill();
114      }
115    };
116
117    var animar = function (ahora) {
118      rafId = window.requestAnimationFrame(animar);
119      var delta = ultimo ? (ahora - ultimo) / 1000 : 0;
120      ultimo = ahora;
121      tiempo += Math.min(delta, 0.05);
122      pintar();
123    };
124
125    var arrancar = function () {
126      if (rafId) return;
127      ultimo = 0;
128      rafId = window.requestAnimationFrame(animar);
129    };
130
131    var parar = function () {
132      if (!rafId) return;
133      window.cancelAnimationFrame(rafId);
134      rafId = 0;
135    };
136
137    medir();
138    pintar();
139
140    var alRedimensionar = function () {
141      if (medir() && !rafId) pintar();
142    };
143
144    if (window.ResizeObserver) {
145      new ResizeObserver(alRedimensionar).observe(o);
146    } else {
147      window.addEventListener('resize', alRedimensionar);
148    }
149
150    // Sólo se anima mientras el módulo está en pantalla
151    if (window.IntersectionObserver) {
152      new IntersectionObserver(function (entradas) {
153        visible = entradas[0].isIntersecting;
154        if (visible && !document.hidden) arrancar();
155        else parar();
156      }, { threshold: 0 }).observe(o);
157    } else {
158      visible = true;
159      arrancar();
160    }
161
162    document.addEventListener('visibilitychange', function () {
163      if (document.hidden) parar();
164      else if (visible) arrancar();
165    });
166  };
167
168  Array.prototype.forEach.call(lista, montar);
169})(document.querySelectorAll('.sta-fondoOlas-fhe'));
170/** sta-fondoOlas-fhe -- FIN */
171
172
173/** tpl-header-fhe -- INICIO */
174(function (o) {
175  if (!o) return;
176
177  var abrir = o.querySelector('.fn-headerAbrir');
178  var panel = o.querySelector('#sta-header-fhe_panel');
179  if (!abrir || !panel) return;
180
181  var cerrar = panel.querySelector('.fn-headerCerrar');
182  var foco = 'a[href], button:not([disabled]), input, select, [tabindex]:not([tabindex="-1"])';
183  var previo = null;
184
185  var enfocables = function () {
186    return Array.prototype.filter.call(panel.querySelectorAll(foco), function (el) {
187      return el.offsetParent !== null;
188    });
189  };
190
191  var abrirPanel = function () {
192    previo = document.activeElement;
193    panel.hidden = false;
194    o.classList.add('sta-abierto');
195    document.documentElement.classList.add('sta-noScroll');
196    abrir.setAttribute('aria-expanded', 'true');
197    var lista = enfocables();
198    if (lista.length) lista[0].focus();
199  };
200
201  var cerrarPanel = function () {
202    panel.hidden = true;
203    o.classList.remove('sta-abierto');
204    document.documentElement.classList.remove('sta-noScroll');
205    abrir.setAttribute('aria-expanded', 'false');
206    if (previo && previo.focus) previo.focus();
207  };
208
209  abrir.addEventListener('click', abrirPanel);
210  if (cerrar) cerrar.addEventListener('click', cerrarPanel);
211
212  panel.addEventListener('keydown', function (e) {
213    if (e.key === 'Escape') {
214      cerrarPanel();
215      return;
216    }
217    if (e.key !== 'Tab') return;
218
219    var lista = enfocables();
220    if (!lista.length) return;
221
222    var primero = lista[0];
223    var ultimo = lista[lista.length - 1];
224
225    if (e.shiftKey && document.activeElement === primero) {
226      e.preventDefault();
227      ultimo.focus();
228    } else if (!e.shiftKey && document.activeElement === ultimo) {
229      e.preventDefault();
230      primero.focus();
231    }
232  });
233
234  // Cierra el panel si se pasa a desktop con el menú abierto
235  window.addEventListener('resize', function () {
236    if (window.innerWidth >= 1024 && !panel.hidden) cerrarPanel();
237  });
238
239  /* Enlace de la sección activa.
240     Mismo criterio que los headers del CMS (fnActual/fnControl en
241     tpl-header2-eh_*.html): se compara location.origin + location.pathname
242     contra el href ya resuelto por el navegador, con el caso especial de "/".
243     El motor puede marcar además el <li> con sta-actual vía {_XT_CLASE_HEADER};
244     esto cubre las páginas en las que ese tag no viene informado. */
245  var urlActual = location.origin + location.pathname;
246
247  var fnControl = function (link) {
248    if (link === '/') link = location.origin + '/';
249    return link.split('?')[0].split('#')[0] === urlActual;
250  };
251
252  /* Coincidencia por sección.
253     El enlace del menú no apunta a un índice padre sino a la edición
254     destacada (p.ej. "Ediciones Anteriores" -> /ediciones-anteriores/2026/),
255     así que /ediciones-anteriores/2024/ es una rama HERMANA, no descendiente:
256     comparar por prefijo no sirve. Se compara el primer tramo de ruta tras la
257     raíz del idioma ("ediciones-anteriores"), que sí es común a toda la
258     sección y cambia con el idioma (en: "previous-editions"). La raíz del
259     idioma se deduce del logo ({_URL_INDICE}), que el motor ya resuelve. */
260  var raiz = (function () {
261    var logo = o.querySelector('.sta-header-fhe_logo');
262    var ruta = logo ? logo.pathname : '/';
263    return ruta.replace(/[^/]*$/, '');
264  })();
265
266  var fnSeccion = function (link) {
267    var ruta = link.split('?')[0].split('#')[0].replace(location.origin, '');
268    if (ruta.indexOf(raiz) !== 0) return '';
269    return ruta.slice(raiz.length).split('/')[0];
270  };
271
272  var fnActual = function () {
273    var enlaces = o.querySelectorAll('.sta-header-fhe_enlace, .sta-header-fhe_panelNav a');
274    var marcar = function (a) {
275      a.classList.add('sta-active');
276      var li = a.parentNode;
277      while (li && li.nodeName !== 'LI') li = li.parentNode;
278      if (li) li.classList.add('sta-actual');
279    };
280
281    var candidatos = Array.prototype.filter.call(enlaces, function (a) {
282      var href = a.getAttribute('href');
283      // Sin destino propio ("#", anclas, vacío): nunca es la sección actual.
284      return href && href.charAt(0) !== '#';
285    });
286
287    var exactos = candidatos.filter(function (a) {
288      return fnControl(a.getAttribute('href') === '/' ? '/' : a.href);
289    });
290
291    if (exactos.length) {
292      exactos.forEach(marcar);
293      return;
294    }
295
296    // Nada coincide exactamente: se marca el enlace de la misma sección.
297    // La home (sección vacía) queda fuera: sólo se marca por coincidencia exacta.
298    var actual = fnSeccion(urlActual);
299    if (!actual) return;
300
301    candidatos.forEach(function (a) {
302      if (fnSeccion(a.href) === actual) marcar(a);
303    });
304  };
305
306  fnActual();
307})(document.querySelector('.tpl-header-fhe'));
308/** tpl-header-fhe -- FIN */
309
310/** scrollSeccion -- INICIO */
311/* Salto a un módulo concreto mediante el parámetro "?scroll=<id>".
312   El CMS marca cada módulo destino con la clase ".scroll_<id>" (p.ej.
313   "scroll_62655"), así que el ancla no es un #hash sino esa clase: el
314   navegador no puede resolverla por sí mismo y hay que posicionarla aquí.
315
316   Cubre los dos casos:
317     1. Carga con ?scroll= en la URL (enlace externo o entre páginas).
318     2. Click en un enlace de la propia página cuyo href lleva ?scroll=:
319        se evita la recarga y se hace el desplazamiento suave.
320
321   El header de la remaquetación es position:relative, no tapa contenido,
322   así que no se resta ninguna altura; si en el futuro pasara a fixed o
323   sticky, fnDesplazamiento() devolvería su altura. */
324(function () {
325  var raiz = document.documentElement;
326
327  /* Altura a descontar por cabeceras superpuestas. Hoy 0: sólo cuenta un
328     header realmente fijo o pegajoso, no el flujo normal. */
329  var fnDesplazamiento = function () {
330    var header = document.querySelector('.tpl-header-fhe');
331    if (!header) return 0;
332    var pos = getComputedStyle(header).position;
333    if (pos !== 'fixed' && pos !== 'sticky') return 0;
334    return header.getBoundingClientRect().height;
335  };
336
337  var fnDestino = function (id) {
338    // Se acepta sólo el formato que emite el CMS, para no inyectar
339    // selectores arbitrarios desde la query string.
340    if (!id || !/^[\w-]+$/.test(id)) return null;
341    return document.querySelector('.scroll_' + id);
342  };
343
344  var fnParametro = function (url) {
345    var busqueda = url.indexOf('?') >= 0 ? url.slice(url.indexOf('?') + 1) : '';
346    var partes = busqueda.split('#')[0].split('&');
347    for (var i = 0; i < partes.length; i++) {
348      var par = partes[i].split('=');
349      if (par[0] === 'scroll') return decodeURIComponent(par[1] || '');
350    }
351    return '';
352  };
353
354  var fnIr = function (destino, suave) {
355    var y = window.pageYOffset + destino.getBoundingClientRect().top - fnDesplazamiento();
356    if (y < 0) y = 0;
357
358    // scroll-behavior:smooth en el CSS del usuario o prefers-reduced-motion
359    // mandan sobre la animación; window.scrollTo con "auto" salta directo.
360    var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
361    var modo = (suave && !reduce) ? 'smooth' : 'auto';
362
363    if ('scrollBehavior' in raiz.style) {
364      window.scrollTo({ top: y, behavior: modo });
365    } else {
366      window.scrollTo(0, y);
367    }
368
369    // El destino no suele ser enfocable: se le da tabindex temporal para que
370    // el foco de teclado y los lectores de pantalla sigan al salto.
371    var previo = destino.getAttribute('tabindex');
372    if (previo === null) destino.setAttribute('tabindex', '-1');
373    destino.focus({ preventScroll: true });
374    if (previo === null) {
375      destino.addEventListener('blur', function quitar() {
376        destino.removeAttribute('tabindex');
377        destino.removeEventListener('blur', quitar);
378      });
379    }
380  };
381
382  /* 1. Parámetro en la URL de entrada.
383     Se desactiva la restauración de scroll del navegador para que no pelee
384     con el salto, y se espera a que carguen imágenes y fuentes: sin eso el
385     offset se calcula sobre un layout que todavía va a crecer. */
386  var idInicial = fnParametro(location.search);
387  var destinoInicial = fnDestino(idInicial);
388
389  if (destinoInicial) {
390    if (window.history && 'scrollRestoration' in history) {
391      history.scrollRestoration = 'manual';
392    }
393
394    var fnSaltar = function () {
395      // Recalcula el destino: los módulos con canvas o carruseles montan
396      // después del parseo y cambian la altura de la página.
397      var destino = fnDestino(idInicial) || destinoInicial;
398      fnIr(destino, false);
399    };
400
401    var fnArrancar = function () {
402      fnSaltar();
403      // Segunda pasada tras el siguiente frame por si algún módulo ha
404      // terminado de montarse justo después.
405      requestAnimationFrame(function () {
406        requestAnimationFrame(fnSaltar);
407      });
408    };
409
410    if (document.readyState === 'complete') {
411      fnArrancar();
412    } else {
413      window.addEventListener('load', fnArrancar);
414    }
415  }
416
417  /* 2. Enlaces de esta misma página que llevan ?scroll=.
418     Delegado en el documento para cubrir también el menú del panel móvil y
419     cualquier enlace que se inyecte más tarde. */
420  document.addEventListener('click', function (e) {
421    var a = e.target.closest ? e.target.closest('a[href]') : null;
422    if (!a) return;
423    if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
424    if (a.target && a.target !== '_self') return;
425
426    var href = a.getAttribute('href');
427    if (!href || href.indexOf('scroll=') < 0) return;
428
429    // Sólo si el enlace apunta a la página en la que ya estamos.
430    if (a.origin !== location.origin || a.pathname !== location.pathname) return;
431
432    var destino = fnDestino(fnParametro(a.search));
433    if (!destino) return;
434
435    e.preventDefault();
436    fnIr(destino, true);
437
438    // Se refleja el destino en la URL sin recargar, para que el enlace sea
439    // compartible y el atrás del navegador funcione.
440    if (window.history && history.pushState) {
441      history.pushState(null, '', a.href);
442    }
443
444    // Si el salto se ha lanzado desde el menú móvil, se cierra.
445    var panel = document.querySelector('.tpl-header-fhe .sta-header-fhe_panel');
446    if (panel && !panel.hidden && panel.contains(a)) {
447      var cerrar = panel.querySelector('.fn-headerCerrar');
448      if (cerrar) cerrar.click();
449    }
450  });
451})();
452/** scrollSeccion -- FIN */
453
454
455/** tpl-navEdiciones-fhe -- INICIO */
456(function (o) {
457  if (!o) return;
458
459  var scroll = o.querySelector('.fn-navEdicionesScroll');
460  var prev = o.querySelector('.fn-navEdicionesPrev');
461  var next = o.querySelector('.fn-navEdicionesNext');
462  if (!scroll || !prev || !next) return;
463
464  var estado = function () {
465    var sobra = scroll.scrollWidth - scroll.clientWidth;
466    prev.classList.toggle('sta-inicio', scroll.scrollLeft <= 1);
467    next.classList.toggle('sta-fin', scroll.scrollLeft >= sobra - 1);
468  };
469
470  var mover = function (signo) {
471    scroll.scrollBy({ left: signo * Math.round(scroll.clientWidth * 0.7), behavior: 'smooth' });
472  };
473
474  prev.addEventListener('click', function () { mover(-1); });
475  next.addEventListener('click', function () { mover(1); });
476  scroll.addEventListener('scroll', estado);
477  window.addEventListener('resize', estado);
478
479  // Arrastre con el ratón. Con el dedo ya se desliza solo (overflow-x del CSS),
480  // esto es para poder hacer lo mismo pulsando y arrastrando con el puntero.
481  var arrastrando = false;
482  var movido = false;
483  var xInicio = 0;
484  var scrollInicio = 0;
485
486  // El CSS deja el scroll en smooth para que las flechas animen, pero mientras
487  // se arrastra estorba: la lista iría por detrás del puntero.
488  var soltar = function () {
489    if (!arrastrando) return;
490    arrastrando = false;
491    scroll.style.scrollBehavior = '';
492    o.classList.remove('sta-arrastrando');
493  };
494
495  scroll.addEventListener('mousedown', function (e) {
496    // Sólo botón principal, y nada de secuestrar la selección de texto del navegador
497    if (e.button !== 0) return;
498    arrastrando = true;
499    movido = false;
500    xInicio = e.clientX;
501    scrollInicio = scroll.scrollLeft;
502    scroll.style.scrollBehavior = 'auto';
503    o.classList.add('sta-arrastrando');
504    e.preventDefault();
505  });
506
507  document.addEventListener('mousemove', function (e) {
508    if (!arrastrando) return;
509    var dx = e.clientX - xInicio;
510    // Umbral pequeño para distinguir un clic en un año de un arrastre real
511    if (Math.abs(dx) > 3) movido = true;
512    scroll.scrollLeft = scrollInicio - dx;
513  });
514
515  document.addEventListener('mouseup', soltar);
516  scroll.addEventListener('mouseleave', soltar);
517
518  // Si se ha arrastrado, el mouseup genera igualmente un clic sobre el enlace
519  // que hubiera debajo: se anula para no navegar sin querer a otra edición.
520  scroll.addEventListener('click', function (e) {
521    if (!movido) return;
522    e.preventDefault();
523    e.stopPropagation();
524    movido = false;
525  }, true);
526
527  // Marca la edición activa comparando el año de la URL actual con el de cada enlace
528  var anoPagina = (location.pathname.match(/(?:19|20)\d{2}/) || [])[0];
529  if (anoPagina) {
530    var enlaces = scroll.querySelectorAll('.sta-navEdiciones-fhe_item a');
531    for (var i = 0; i < enlaces.length; i++) {
532      var li = enlaces[i].parentNode;
533      var texto = enlaces[i].textContent.replace(/\D/g, '');
534      var href = (enlaces[i].getAttribute('href') || '').match(/(?:19|20)\d{2}/);
535      var anoItem = texto.length === 4 ? texto : (href ? href[0] : '');
536      li.classList.toggle('sta-actual', anoItem === anoPagina);
537    }
538  }
539
540  // Centra la edición activa al cargar
541  var actual = scroll.querySelector('.sta-actual');
542  if (actual) {
543    scroll.scrollLeft = actual.offsetLeft - (scroll.clientWidth - actual.offsetWidth) / 2;
544  }
545
546  estado();
547})(document.querySelector('.tpl-navEdiciones-fhe'));
548/** tpl-navEdiciones-fhe -- FIN */
549
550
551/** tpl-videos-fhe -- INICIO */
552(function (o) {
553  if (!o) return;
554
555  var marco = o.querySelector('.fn-videosMarco');
556  if (!marco) return;
557
558  var pistas = o.querySelectorAll('.fn-videosPista');
559
560  var montar = function (youtube, wistia, segundos) {
561    var src = '';
562
563    if (youtube) {
564      src = 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(youtube) +
565            '?autoplay=1&rel=0' + (segundos ? '&start=' + segundos : '');
566    } else if (wistia) {
567      src = 'https://fast.wistia.net/embed/iframe/' + encodeURIComponent(wistia) + '?autoPlay=true';
568    } else {
569      return;
570    }
571
572    var viejo = marco.querySelector('iframe');
573    if (viejo) viejo.parentNode.removeChild(viejo);
574
575    var iframe = document.createElement('iframe');
576    iframe.src = src;
577    iframe.title = o.querySelector('.sta-h2-fhe') ? o.querySelector('.sta-h2-fhe').textContent.trim() : 'video';
578    iframe.setAttribute('allow', 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture');
579    iframe.setAttribute('allowfullscreen', '');
580    iframe.setAttribute('frameborder', '0');
581    marco.appendChild(iframe);
582    marco.classList.add('sta-reproduciendo');
583  };
584
585  // Convierte "01:23:45" o "12:30" en segundos
586  var aSegundos = function (txt) {
587    if (!txt) return 0;
588    var partes = txt.trim().split(':').map(Number);
589    if (partes.some(isNaN)) return 0;
590    return partes.reduce(function (total, n) { return total * 60 + n; }, 0);
591  };
592
593  var activar = function (item) {
594    Array.prototype.forEach.call(o.querySelectorAll('.sta-videos-fhe_item'), function (li) {
595      li.classList.toggle('sta-actual', li === item);
596    });
597  };
598
599  Array.prototype.forEach.call(pistas, function (pista) {
600    pista.addEventListener('click', function () {
601      var item = pista.closest('.sta-videos-fhe_item');
602      activar(item);
603      montar(pista.dataset.youtube, pista.dataset.wistia, 0);
604    });
605
606    var item = pista.closest('.sta-videos-fhe_item');
607    if (!item) return;
608
609    Array.prototype.forEach.call(item.querySelectorAll('.fn-videosCapitulo'), function (cap) {
610      cap.addEventListener('click', function () {
611        activar(item);
612        montar(pista.dataset.youtube, pista.dataset.wistia, aSegundos(cap.dataset.minuto));
613      });
614    });
615  });
616
617  // El botón grande reproduce la primera pista
618  var play = o.querySelector('.fn-videosPlay');
619  if (play && pistas.length) {
620    play.addEventListener('click', function () { pistas[0].click(); });
621  }
622})(document.querySelector('.tpl-videos-fhe'));
623/** tpl-videos-fhe -- FIN */
624
625
626/** tpl-galeria-fhe -- INICIO */
627(function (o, datos) {
628  if (!o) return;
629
630  var mosaico = o.querySelector('.fn-galeriaMosaico');
631  var boton = o.querySelector('.fn-galeriaAbrir');
632  var cuantas = o.querySelector('.fn-galeriaCuantas');
633  if (!mosaico || !boton) return;
634
635  // Coge el array de fotos de este ítem del CMS
636  var clave = Object.keys(datos || {})[0];
637  var fotos = (datos && clave) ? datos[clave] : [];
638  fotos = (fotos || []).filter(function (url) { return typeof url === 'string' && url.trim(); });
639
640  if (!fotos.length) {
641    o.hidden = true;
642    return;
643  }
644
645  var VISIBLES = 5;
646  var enMosaico = fotos.slice(0, VISIBLES);
647  var restantes = fotos.length - enMosaico.length;
648
649  enMosaico.forEach(function (url, i) {
650    var li = document.createElement('li');
651    li.className = 'sta-galeria-fhe_celda' + (i === 0 ? ' sta-grande' : '');
652
653    var ultima = (i === enMosaico.length - 1) && restantes > 0;
654
655    var img = document.createElement('img');
656    img.src = url + '?width=' + (i === 0 ? 1200 : 600);
657    img.alt = '';
658    img.setAttribute('role', 'presentation');
659    img.loading = 'lazy';
660    li.appendChild(img);
661
662    if (ultima) {
663      // La última celda aloja el botón de "ver todas"
664      li.appendChild(boton);
665      boton.hidden = false;
666      if (cuantas) cuantas.textContent = '+' + restantes;
667    } else {
668      var abrir = document.createElement('button');
669      abrir.type = 'button';
670      abrir.setAttribute('aria-label', boton.textContent.trim() || 'ver');
671      abrir.addEventListener('click', function () { pedirModal(i); });
672      li.appendChild(abrir);
673    }
674
675    mosaico.appendChild(li);
676  });
677
678  function pedirModal(indice) {
679    document.dispatchEvent(new CustomEvent('galeriaAbrir', {
680      detail: { fotos: fotos, indice: indice }
681    }));
682  }
683
684  boton.addEventListener('click', function () { pedirModal(enMosaico.length - 1); });
685}
685)(document.querySelector('.tpl-galeria-fhe'), window.json_galeria);
686/** tpl-galeria-fhe -- FIN */
687
688
689/** tpl-modalGaleria-fhe -- INICIO */
690(function (o) {
691  if (!o) return;
692
693  var img = o.querySelector('.fn-modalGaleriaImg');
694  var actual = o.querySelector('.fn-modalGaleriaActual');
695  var total = o.querySelector('.fn-modalGaleriaTotal');
696  var prev = o.querySelector('.fn-modalGaleriaPrev');
697  var next = o.querySelector('.fn-modalGaleriaNext');
698  if (!img || !prev || !next) return;
699
700  var fotos = [];
701  var i = 0;
702  var previo = null;
703  var foco = 'button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
704
705  var pintar = function () {
706    if (!fotos.length) return;
707    o.classList.add('sta-cargando');
708    img.onload = function () { o.classList.remove('sta-cargando'); };
709    img.src = fotos[i] + '?width=1600';
710    img.alt = '';
711    if (actual) actual.textContent = String(i + 1);
712    if (total) total.textContent = String(fotos.length);
713    var uno = fotos.length < 2;
714    prev.hidden = uno;
715    next.hidden = uno;
716  };
717
718  var mover = function (paso) {
719    i = (i + paso + fotos.length) % fotos.length;
720    pintar();
721  };
722
723  var abrir = function (lista, indice) {
724    fotos = lista || [];
725    if (!fotos.length) return;
726    i = Math.min(Math.max(indice || 0, 0), fotos.length - 1);
727    previo = document.activeElement;
728    o.hidden = false;
729    o.classList.add('sta-abierto');
730    document.documentElement.classList.add('sta-noScroll');
731    pintar();
732    var cerrarBtn = o.querySelector('.sta-modalGaleria-fhe_cerrar');
733    if (cerrarBtn) cerrarBtn.focus();
734  };
735
736  var cerrar = function () {
737    o.hidden = true;
738    o.classList.remove('sta-abierto');
739    document.documentElement.classList.remove('sta-noScroll');
740    if (previo && previo.focus) previo.focus();
741  };
742
743  document.addEventListener('galeriaAbrir', function (e) {
744    abrir(e.detail && e.detail.fotos, e.detail && e.detail.indice);
745  });
746
747  Array.prototype.forEach.call(o.querySelectorAll('.fn-modalGaleriaCerrar'), function (btn) {
748    btn.addEventListener('click', cerrar);
749  });
750
751  prev.addEventListener('click', function () { mover(-1); });
752  next.addEventListener('click', function () { mover(1); });
753
754  o.addEventListener('keydown', function (e) {
755    if (e.key === 'Escape') { cerrar(); return; }
756    if (e.key === 'ArrowLeft') { mover(-1); return; }
757    if (e.key === 'ArrowRight') { mover(1); return; }
758    if (e.key !== 'Tab') return;
759
760    var lista = Array.prototype.filter.call(o.querySelectorAll(foco), function (el) {
761      return !el.hidden && el.offsetParent !== null;
762    });
763    if (!lista.length) return;
764
765    var primero = lista[0];
766    var ultimo = lista[lista.length - 1];
767
768    if (e.shiftKey && document.activeElement === primero) {
769      e.preventDefault();
770      ultimo.focus();
771    } else if (!e.shiftKey && document.activeElement === ultimo) {
772      e.preventDefault();
773      primero.focus();
774    }
775  });
776})(document.querySelector('.tpl-modalGaleria-fhe'));
777/** tpl-modalGaleria-fhe -- FIN */
778
779
780/** tpl-formInscripcion-fhe -- INICIO */
781(function (o, h) {
782  if (!o) return;
783
784  var form = o.querySelector('.fn-formInscripcion');
785  if (!form) return;
786
787  var bloque = form.querySelector('.fn-formBloqueAcompanante');
788  var asistentes = form.querySelector('.fn-formAsistentes');
789  var aviso = form.querySelector('.fn-formAviso');
790  var enviar = form.querySelector('[type="submit"]');
791  var radios = form.querySelectorAll('.fn-formAcompanante');
792
793  // Muestra u oculta el bloque del acompañante y sincroniza num_asistentes,
794  // que es lo que espera el backend antiguo.
795  var sincronizar = function () {
796    var acompanado = false;
797    Array.prototype.forEach.call(radios, function (r) {
798      if (r.checked && r.value === '1') acompanado = true;
799    });
800
801    if (bloque) {
802      bloque.classList.toggle('sta-oculto', !acompanado);
803      Array.prototype.forEach.call(bloque.querySelectorAll('input'), function (input) {
804        input.required = acompanado;
805        if (!acompanado) {
806          input.value = '';
807          input.removeAttribute('aria-invalid');
808          limpiarError(input);
809        }
810      });
811    }
812
813    if (asistentes) asistentes.value = acompanado ? '2' : '1';
814  };
815
816  // El checkbox de condiciones no vive en un _campo sino en el bloque _legal,
817  // asi que se contemplan los dos: si no, pintarError salia por el return y el
818  // envio se bloqueaba sin decirle nada al usuario.
819  var campoDe = function (input) {
820    return input.closest('.sta-formInscripcion-fhe_campo, .sta-formInscripcion-fhe_legal');
821  };
822
823  var limpiarError = function (input) {
824    var campo = campoDe(input);
825    if (!campo) return;
826    var hueco = campo.querySelector('.sta-formInscripcion-fhe_error');
827    if (hueco) hueco.textContent = '';
828  };
829
830  var pintarError = function (input, mensaje) {
831    input.setAttribute('aria-invalid', 'true');
832    var campo = campoDe(input);
833    if (!campo) return;
834    var hueco = campo.querySelector('.sta-formInscripcion-fhe_error');
835    if (hueco) hueco.textContent = mensaje;
836  };
837
838  var validar = function () {
839    var primero = null;
840
841    Array.prototype.forEach.call(form.querySelectorAll('input, select'), function (input) {
842      if (input.type === 'hidden') return;
843
844      input.removeAttribute('aria-invalid');
845      limpiarError(input);
846
847      if (!input.required) return;
848
849      var vacio = (input.type === 'checkbox' || input.type === 'radio')
850        ? !form.querySelector('[name="' + input.name + '"]:checked')
851        : !input.value.trim();
852
853      var malMail = input.type === 'email' && input.value.trim() &&
854        (h && h.mail ? !h.mail(input.value.trim()) : !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(input.value.trim()));
855
856      if (vacio || malMail) {
857        pintarError(input, input.validationMessage || '');
858        if (!primero) primero = input;
859      }
860    });
861
862    if (primero) {
863      primero.focus();
864      return false;
865    }
866    return true;
867  };
868
869  Array.prototype.forEach.call(radios, function (r) {
870    r.addEventListener('change', sincronizar);
871  });
872
873  Array.prototype.forEach.call(form.querySelectorAll('input, select'), function (input) {
874    input.addEventListener('input', function () {
875      input.removeAttribute('aria-invalid');
876      limpiarError(input);
877    });
878  });
879
880  var dato = function (nombre) {
881    var el = form.querySelector('[name="' + nombre + '"]');
882    return el && el.value ? el.value.trim() : '';
883  };
884
885  var acompanado = function () {
886    return !!form.querySelector('.fn-formAcompanante:checked[value="1"]');
887  };
888
889  // Arrays paralelos: cada título se corresponde con el valor de su misma posición
890  var construirJson = function () {
891    var mail = dato('mail').toLowerCase();
892    var con = acompanado();
893
894    var title = [
895      'Mail',
896      'Nombre',
897      'Apellidos',
898      'Telefono',
899      'Empresa',
900      'Cargo',
901      'Como nos ha conocido',
902      'Acompanante',
903      'Nombre acompanante',
904      'Apellidos acompanante',
905      'Empresa acompanante',
906      'Cargo acompanante',
907      'Num asistentes',
908      'Idioma',
909      'ID'
910    ];
911
912    var values = [
913      mail,
914      dato('name'),
915      dato('last'),
916      dato('tel'),
917      dato('comp'),
918      dato('cargo'),
919      dato('conocido') || '---',
920      con ? '1' : '0',
921      con ? (dato('name_2') || '---') : '---',
922      con ? (dato('last_2') || '---') : '---',
923      con ? (dato('comp_2') || '---') : '---',
924      con ? (dato('cargo_2') || '---') : '---',
925      asistentes ? asistentes.value : (con ? '2' : '1'),
926      dato('idioma') || document.documentElement.lang,
927      'foro-hotusa-explora-2026-000'
928    ];
929
930    // El backend espera { id, title, new, nl }: title y new son dos arrays
931    // paralelos (cabecera y valor de cada columna) y nl, el mail de confirmación.
932    var json = {
933      id: 'foro-hotusa-explora-2026-000',
934      title: title,
935      new: values,
936      // Solo se manda la NL interna de aviso a secretaría (rutaH). Sin clave "ruta"
937      // el inscrito no recibe correo de confirmación.
938      nl: {
939        nlH: '[email protected]',
940        rutaH: '/images/comun/hotusaexplora/FHE2026/2026_ForoHotusaExplora_interna.html',
941        from: '[email protected]',
942        subject: 'Nueva inscripción a Foro Hotusa Explora',
943        // Cada clave alimenta un {tag} de la plantilla de la NL interna
944        tags: {
945          name: dato('name'),
946          surname: dato('last'),
947          mail: mail,
948          movil: dato('tel'),
949          business: dato('comp'),
950          cargo: dato('cargo'),
951          conocido: dato('conocido') || '---',
952          acompanante: con ? 'SI' : 'NO',
953          acom_name: con ? (dato('name_2') || '---') : '---',
954          acom_surname: con ? (dato('last_2') || '---') : '---',
955          acom_business: con ? (dato('comp_2') || '---') : '---',
956          acom_cargo: con ? (dato('cargo_2') || '---') : '---',
957          asistentes: asistentes ? asistentes.value : (con ? '2' : '1'),
958          idioma: dato('idioma') || document.documentElement.lang,
959          id: 'foro-hotusa-explora-2026-000',
960          campane: 'foro-hotusa-explora-2026-000',
961          url: location.origin + location.pathname,
962          modificado: 'NO'
963        }
964      }
965    };
966
967    return json;
968  };
969
970  var estado = function (ok) {
971    if (!aviso) return;
972    aviso.classList.toggle('sta-enviado', ok);
973    aviso.classList.toggle('sta-fallo', !ok);
974  };
975
976  var terminar = function () {
977    if (enviar) enviar.disabled = false;
978    form.classList.remove('sta-enviando');
979  };
980
981  // El endpoint responde con un código numérico: 200 ok, 401 duplicado,
982  // 402 error de bbdd, 403 plantilla no encontrada, 410 inscripciones cerradas
983  var responder = function (codigo) {
984    if (codigo === 200) {
985      estado(true);
986      form.reset();
987      sincronizar();
988      return;
989    }
990    if (codigo === 403) console.warn('Inscripcion: ruta de plantilla no encontrada');
991    estado(false);
992  };
993
994  form.addEventListener('submit', function (e) {
995    if (!validar()) {
996      e.preventDefault();
997      return;
998    }
999
1000    // Con action definido en el CMS se deja el envío nativo del navegador
1001    if (form.getAttribute('action')) return;
1002
1003    e.preventDefault();
1004
1005    if (enviar) enviar.disabled = true;
1006    form.classList.add('sta-enviando');
1007    if (aviso) aviso.classList.remove('sta-enviado', 'sta-fallo');
1008
1009    var cuerpo = JSON.stringify(construirJson());
1010
1011    fetch('https://nl.booking-channel.com/CLIENTES/nl/php/inscripcion2.0.php', { method: 'POST', body: cuerpo })
1012      .then(function (r) { return r.json(); })
1013      .then(responder)
1014      .catch(function () { estado(false); })
1015      .then(terminar);
1016  });
1017
1018  sincronizar();
1019})(document.querySelector('.tpl-formInscripcion-fhe'), window.hotusa && hotusa());
1020/** tpl-formInscripcion-fhe -- FIN */
1021
1022
1023/** tpl-cabecera-fhe -- INICIO */
1024(function (o) {
1025  if (!o) return;
1026
1027  // Scroll suave en el botón de ancla, respetando quien prefiere no animar
1028  var boton = o.querySelector('.sta-cabecera-fhe_boton.sta-primario');
1029  if (!boton) return;
1030
1031  var href = boton.getAttribute('href') || '';
1032  if (href.charAt(0) !== '#' || href.length < 2) return;
1033
1034  boton.addEventListener('click', function (e) {
1035    var destino = document.querySelector(href);
1036    if (!destino) return;
1037
1038    e.preventDefault();
1039    var suave = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
1040    destino.scrollIntoView({ behavior: suave ? 'smooth' : 'auto', block: 'start' });
1041    destino.setAttribute('tabindex', '-1');
1042    destino.focus({ preventScroll: true });
1043  });
1044})(document.querySelector('.tpl-cabecera-fhe'));
1045/** tpl-cabecera-fhe -- FIN */
1046
1047
1048
1049/** tpl-boxes-fhe -- INICIO */
1050(function (o) {
1051  if (!o) return;
1052
1053  var capa = o.querySelector('.sta-boxes-fhe_fondo');
1054  if (!capa) return;
1055
1056  // Sin canvas o si se prefiere no animar, el bloque se queda con su fondo plano
1057  var canvas = document.createElement('canvas');
1058  if (!canvas.getContext) return;
1059
1060  var menos = window.matchMedia('(prefers-reduced-motion: reduce)');
1061  if (menos.matches) return;
1062
1063  var ctx = canvas.getContext('2d');
1064  capa.appendChild(canvas);
1065
1066  var CUANTOS = 3;          // anillos, todos del mismo tamaño
1067  var RADIO = 190;          // radio de referencia, se escala con el módulo
1068  var GROSOR = 0.22;        // grosor del trazo respecto al radio (54/246 del SVG)
1069  var DESBORDE = 0.60;      // cuánto puede salir del módulo: 60% de su radio
1070  var ALFA = 0.58;          // igual que el stroke-opacity del SVG
1071  var VELOCIDAD = 0.35;     // px por frame a 60fps
1072  var COLOR_ARRIBA = '#4C958E';
1073  var COLOR_ABAJO = '#2F56A2';
1074
1075  var dpr = 1;
1076  var ancho = 0;
1077  var alto = 0;
1078  var anillos = [];
1079  var ultimo = 0;
1080  var rafId = 0;
1081  var visible = false;
1082
1083  // Distancia mínima del centro al borde: con DESBORDE = 0.6 el anillo
1084  // asoma un 60% de su radio fuera del módulo, que lo recorta por overflow
1085  var margen = function (a) {
1086    return a.golpe * (1 - DESBORDE);
1087  };
1088
1089  // El radio de colisión es el borde exterior del trazo, no la línea media
1090  var crear = function (radio) {
1091    var grosor = radio * GROSOR;
1092    return {
1093      r: radio,
1094      grosor: grosor,
1095      golpe: radio + grosor * 0.5,
1096      x: 0,
1097      y: 0,
1098      vx: 0,
1099      vy: 0
1100    };
1101  };
1102
1103  // Reparte los anillos sin solaparse entre sí, con reintentos
1104  var colocar = function (a, puestos) {
1105    var m = margen(a);
1106
1107    for (var intento = 0; intento < 300; intento++) {
1108      a.x = m + Math.random() * Math.max(1, ancho - m * 2);
1109      a.y = m + Math.random() * Math.max(1, alto - m * 2);
1110
1111      var libre = true;
1112      for (var i = 0; i < puestos.length; i++) {
1113        var p = puestos[i];
1114        var dx = p.x - a.x;
1115        var dy = p.y - a.y;
1116        var min = p.golpe + a.golpe;
1117        if (dx * dx + dy * dy < min * min) { libre = false; break; }
1118      }
1119      if (libre) return true;
1120    }
1121    return false;
1122  };
1123
1124  var construir = function () {
1125    // Un único radio, escalado para que en móvil no tapen el contenido
1126    var ref = Math.min(ancho, alto);
1127    var escala = Math.max(0.35, Math.min(1, ref / 700));
1128    var radio = RADIO * escala;
1129
1130    anillos = [];
1131    for (var i = 0; i < CUANTOS; i++) {
1132      var a = crear(radio);
1133      if (!colocar(a, anillos)) continue;
1134
1135      var angulo = Math.random() * 6.2832;
1136      var v = VELOCIDAD * (0.6 + Math.random() * 0.8);
1137      a.vx = Math.cos(angulo) * v;
1138      a.vy = Math.sin(angulo) * v;
1139      anillos.push(a);
1140    }
1141  };
1142
1143  // Movimiento, rebote contra los bordes y choques elásticos entre anillos.
1144  // Al ser todos del mismo tamaño, el choque intercambia la componente normal
1145  var mover = function (dt) {
1146    var i, j;
1147
1148    for (i = 0; i < anillos.length; i++) {
1149      var a = anillos[i];
1150      a.x += a.vx * dt;
1151      a.y += a.vy * dt;
1152
1153      var m = margen(a);
1154      if (a.x < m) { a.x = m; a.vx = Math.abs(a.vx); }
1155      if (a.x > ancho - m) { a.x = ancho - m; a.vx = -Math.abs(a.vx); }
1156      if (a.y < m) { a.y = m; a.vy = Math.abs(a.vy); }
1157      if (a.y > alto - m) { a.y = alto - m; a.vy = -Math.abs(a.vy); }
1158    }
1159
1160    for (i = 0; i < anillos.length; i++) {
1161      for (j = i + 1; j < anillos.length; j++) {
1162        var p = anillos[i];
1163        var q = anillos[j];
1164
1165        var dx = q.x - p.x;
1166        var dy = q.y - p.y;
1167        var min = p.golpe + q.golpe;
1168        var d2 = dx * dx + dy * dy;
1169        if (d2 >= min * min || d2 === 0) continue;
1170
1171        var d = Math.sqrt(d2);
1172        var nx = dx / d;
1173        var ny = dy / d;
1174
1175        // Se separan a partes iguales porque comparten radio
1176        var solape = (min - d) * 0.5;
1177        p.x -= nx * solape;
1178        p.y -= ny * solape;
1179        q.x += nx * solape;
1180        q.y += ny * solape;
1181
1182        var sep = (q.vx - p.vx) * nx + (q.vy - p.vy) * ny;
1183        if (sep > 0) continue;
1184
1185        p.vx += sep * nx;
1186        p.vy += sep * ny;
1187        q.vx -= sep * nx;
1188        q.vy -= sep * ny;
1189      }
1190    }
1191  };
1192
1193  var medir = function () {
1194    var w = Math.max(1, Math.round(o.offsetWidth));
1195    var h = Math.max(1, Math.round(o.offsetHeight));
1196    if (w === ancho && h === alto) return false;
1197
1198    ancho = w;
1199    alto = h;
1200    dpr = Math.min(window.devicePixelRatio || 1, 2);
1201
1202    canvas.width = Math.round(ancho * dpr);
1203    canvas.height = Math.round(alto * dpr);
1204    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1205
1206    construir();
1207    return true;
1208  };
1209
1210  var pintar = function () {
1211    ctx.clearRect(0, 0, ancho, alto);
1212    ctx.globalAlpha = ALFA;
1213
1214    for (var i = 0; i < anillos.length; i++) {
1215      var a = anillos[i];
1216
1217      // Degradado vertical de borde a borde del anillo, como en el SVG
1218      var grad = ctx.createLinearGradient(a.x, a.y - a.golpe, a.x, a.y + a.golpe);
1219      grad.addColorStop(0, COLOR_ARRIBA);
1220      grad.addColorStop(1, COLOR_ABAJO);
1221
1222      ctx.beginPath();
1223      ctx.arc(a.x, a.y, a.r, 0, 6.2832);
1224      ctx.lineWidth = a.grosor;
1225      ctx.strokeStyle = grad;
1226      ctx.stroke();
1227    }
1228
1229    ctx.globalAlpha = 1;
1230  };
1231
1232  var animar = function (ahora) {
1233    rafId = window.requestAnimationFrame(animar);
1234
1235    // dt en frames de 60fps, acotado para no saltar al volver de otra pestaña
1236    var delta = ultimo ? (ahora - ultimo) : 16.7;
1237    ultimo = ahora;
1238    mover(Math.min(delta / 16.7, 3));
1239    pintar();
1240  };
1241
1242  var arrancar = function () {
1243    if (rafId) return;
1244    ultimo = 0;
1245    rafId = window.requestAnimationFrame(animar);
1246  };
1247
1248  var parar = function () {
1249    if (!rafId) return;
1250    window.cancelAnimationFrame(rafId);
1251    rafId = 0;
1252  };
1253
1254  medir();
1255  pintar();
1256
1257  var alRedimensionar = function () {
1258    if (medir() && !rafId) pintar();
1259  };
1260
1261  if (window.ResizeObserver) {
1262    new ResizeObserver(alRedimensionar).observe(o);
1263  } else {
1264    window.addEventListener('resize', alRedimensionar);
1265  }
1266
1267  // Sólo se anima mientras el módulo está en pantalla
1268  if (window.IntersectionObserver) {
1269    new IntersectionObserver(function (entradas) {
1270      visible = entradas[0].isIntersecting;
1271      if (visible && !document.hidden) arrancar();
1272      else parar();
1273    }, { threshold: 0 }).observe(o);
1274  } else {
1275    visible = true;
1276    arrancar();
1277  }
1278
1279  document.addEventListener('visibilitychange', function () {
1280    if (document.hidden) parar();
1281    else if (visible) arrancar();
1282  });
1283})(document.querySelector('.tpl-boxes-fhe'));
1284/** tpl-boxes-fhe -- FIN */
1285
1286
1287/** tpl-cookies-fhe -- INICIO */
1288/* Puente entre el botón "Configurar mis cookies" de la landing de cookies y el
1289   panel del script global (/ja/globalCookies/), que es quien manda aquí.
1290
1291   head-fhe sólo carga ese script cuando NO hay cookie `controlCookies`: si el
1292   usuario ya eligió, en la página no hay banner ninguno y hay que inyectarlo
1293   antes de abrirlo. El panel se controla con dos checkboxes que pinta el propio
1294   script: #sta-cookies (banner visible) y #sta-cookies_config (pestaña de
1295   configuración desplegada). */
1296(function (boton) {
1297  if (!boton) return;
1298
1299  var RUTA_CSS = '/ja/globalCookies/style.css';
1300  var RUTA_JS = '/ja/globalCookies/controlCookiesVanilla.js';
1301
1302  var abrirPanel = function () {
1303    var banner = document.getElementById('sta-cookies');
1304    var config = document.getElementById('sta-cookies_config');
1305    if (!banner) return false;
1306
1307    banner.checked = true;
1308    if (config) config.checked = true;
1309
1310    var dialogo = document.querySelector('.tpl-cookie');
1311    if (dialogo) dialogo.focus();
1312    return true;
1313  };
1314
1315  // El script ya inyectó el banner: basta con abrirlo
1316  var yaCargado = function () {
1317    return !!document.querySelector('.tpl-cookie');
1318  };
1319
1320  var cargar = function () {
1321    if (!document.querySelector('link[href^="' + RUTA_CSS + '"]')) {
1322      var css = document.createElement('link');
1323      css.rel = 'stylesheet';
1324      css.href = RUTA_CSS;
1325      document.body.appendChild(css);
1326    }
1327
1328    var js = document.createElement('script');
1329    js.src = RUTA_JS;
1330    // El script se autoejecuta al cargar; el panel se abre cuando ya está el DOM
1331    js.onload = abrirPanel;
1332    document.body.appendChild(js);
1333  };
1334
1335  boton.addEventListener('click', function () {
1336    if (yaCargado()) {
1337      abrirPanel();
1338      return;
1339    }
1340    // Sin window.g_cookies_content no hay textos y el script rompe: lo pinta head-fhe
1341    if (!window.g_cookies_content) return;
1342    cargar();
1343  });
1344})(document.querySelector('.fn-abrirCookies'));
1345/** tpl-cookies-fhe -- FIN */

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.