main
js 1,021 lines 49.8 KB
Raw
1 /**
2 * MAPS Connect - Estilos de la página
3 * Permite elegir un estilo visual global (skin) que afecta a toda la
4 * aplicación (sidebar, topbar, tarjetas, botones, formularios, fondo...).
5 * Guarda la preferencia en localStorage y la aplica como atributo
6 * "data-estilo" sobre <html>; frontend/css/estilos.css define los skins
7 * bajo html[data-estilo="..."].
8 *
9 * Para añadir un estilo nuevo solo hay que:
10 * 1) Agregar su entrada en ESTILOS (id, nombre, descripcion, preview).
11 * 2) Escribir su bloque CSS en estilos.css bajo html[data-estilo="id"].
12 * 3) Si el estilo inyecta su propio fondo (como "cyber"), añadir la
13 * lógica de inyección en _ponFondo/_limpiarFondo.
14 */
15 const Estilos = {
16 KEY: 'estilo-maps',
17
18 STYLES: [
19 {
20 id: 'clasico',
21 nombre: 'Clásico',
22 descripcion: 'Estilo predeterminado de MAPS Connect con la paleta institucional.',
23 preview: 'linear-gradient(135deg, #087527 0%, #3fb950 60%, #a3e635 100%)'
24 },
25 {
26 id: 'cyber',
27 nombre: 'Cyberpunk Network',
28 descripcion: 'Red neuronal de neón cian y magenta sobre fondo de terminal oscura.',
29 preview: 'linear-gradient(135deg, #00f0ff 0%, #1a0033 50%, #ff007f 100%)'
30 },
31 {
32 id: 'medieval',
33 nombre: 'Hogwarts Medieval',
34 descripcion: 'Oro y pergamino sobre la fortaleza mágica (experiencia 3D tipo Hogwarts).',
35 preview: 'linear-gradient(135deg, #d4af37 0%, #18110b 50%, #5a1414 100%)'
36 },
37 {
38 id: 'saiyan',
39 nombre: 'Saiyan Power Edition',
40 descripcion: 'Nube Kinto, estelas de ki y aura dorada sobre el dojo de los Z-Warriors (naranja intenso).',
41 preview: 'linear-gradient(135deg, #f97316 0%, #1e3a8a 50%, #facc15 100%)'
42 },
43 {
44 id: 'noir',
45 nombre: 'Cyber Noir Edition',
46 descripcion: 'Callejón nocturno con neón rojo, lluvia digital y expedientes confidenciales (gótico cyber).',
47 preview: 'linear-gradient(135deg, #e11d48 0%, #080305 50%, #3a0011 100%)'
48 },
49 {
50 id: 'alchemy',
51 nombre: 'Alchemical Codex Edition',
52 descripcion: 'Astrolabio, geometría sagrada y oro de filósofo sobre la cámara hermetista (esmeralda).',
53 preview: 'linear-gradient(135deg, #10b981 0%, #020d08 50%, #fbbf24 100%)'
54 },
55 {
56 id: 'minimal',
57 nombre: 'Minimalist Tricolor Edition',
58 descripcion: 'Geometría suiza azul, rojo y blanco con retícula fina y parallax pausado (estilo internacional).',
59 preview: 'linear-gradient(135deg, #2563eb 0%, #f8fafc 50%, #dc2626 100%)'
60 },
61 {
62 id: 'invernadero',
63 nombre: 'Invernadero',
64 descripcion: 'Cubierta vegetal, foliaje mecido y luz tamizada sobre una experiencia académica que crece (verde institucional).',
65 preview: 'linear-gradient(135deg, #3E6B4F 0%, #E7EEDA 50%, #C97B4A 100%)'
66 }
67 ],
68
69 _VARIABLES_ACCENTO: ['--accent', '--accent-dark', '--accent-mid', '--accent-bright',
70 '--accent-deep', '--accent-soft', '--accent-soft-border', '--accent-contrast'],
71
72 /** Textos personalizados por skin (botones, enlaces y placeholders).
73 Las claves son el texto original exacto y los valores el texto temático. */
74 TEXTOS_ESTILO: {
75 cyber: {},
76 medieval: {
77 'Publicar duda': 'Enviar al Palacio',
78 'Publicar Duda': 'Enviar al Palacio',
79 'Publicar': 'Enviar al Palacio',
80 'Publicar de todos modos': 'Enviar de todos modos',
81 'Cerrar sesión': 'Abandonar el Castillo',
82 'Guardar': 'Guardar en el Pergamino',
83 'Enviar Mensaje Privado': 'Enviar por Búho',
84 '+ Crear sesión de repaso': '+ Convocar Círculo de Estudio',
85 'Crear sesión': 'Convocar Estudio',
86 'Unirme': 'Unirme al Círculo',
87 '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Lanza tu pregón académico a la Comunidad del Castillo...'
88 },
89 saiyan: {
90 'Publicar duda': 'Lanzar Ataque Ki',
91 'Publicar Duda': 'Lanzar Ataque Ki',
92 'Publicar': 'Lanzar Ataque Ki',
93 'Publicar de todos modos': 'Lanzar de todos modos',
94 'Cerrar sesión': 'Descansar / Salir',
95 'Guardar': 'Guardar Energía',
96 'Enviar Mensaje Privado': 'Telepatía Instantánea',
97 '+ Crear sesión de repaso': '+ Crear Sala de Entrenamiento',
98 'Crear sesión': 'Crear Entrenamiento',
99 'Unirme': 'Entrar al Dojo',
100 '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Lanza tu duda o reto técnico al campo de entrenamiento...'
101 },
102 noir: {
103 'Publicar duda': 'Registrar Pista',
104 'Publicar Duda': 'Registrar Pista',
105 'Publicar': 'Registrar Pista',
106 'Publicar de todos modos': 'Registrar de todos modos',
107 'Cerrar sesión': 'Cerrar Expediente',
108 'Guardar': 'Sellar en Archivo',
109 'Enviar Mensaje Privado': 'Enviar Nota Cifrada',
110 '+ Crear sesión de repaso': '+ Convocar Ronda de Estudio',
111 'Crear sesión': 'Convocar Ronda',
112 'Unirme': 'Entrar a la Ronda',
113 '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Inscribe una pista, duda o cabo suelto al expediente...'
114 },
115 alchemy: {
116 'Publicar duda': 'Liberar Sello',
117 'Publicar Duda': 'Liberar Sello',
118 'Publicar': 'Liberar Sello',
119 'Publicar de todos modos': 'Liberar de todos modos',
120 'Cerrar sesión': 'Cerrar Grimorio / Salir',
121 'Guardar': 'Sellar en Papiro',
122 'Enviar Mensaje Privado': 'Enviar Sello Privado',
123 '+ Crear sesión de repaso': '+ Convocar Círculo Esotérico',
124 'Crear sesión': 'Convocar Círculo',
125 'Unirme': 'Unirme al Círculo',
126 '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Inscribe una consulta o runa técnica al Códice...'
127 },
128 minimal: {
129 'Publicar duda': 'Enviar Solicitud',
130 'Publicar Duda': 'Enviar Solicitud',
131 'Publicar': 'Enviar Solicitud',
132 'Publicar de todos modos': 'Enviar de todos modos',
133 'Cerrar sesión': 'Cerrar Sesión',
134 'Guardar': 'Guardar Cambios',
135 'Enviar Mensaje Privado': 'Enviar Mensaje',
136 '+ Crear sesión de repaso': '+ Programar Sesión',
137 'Crear sesión': 'Programar Sesión',
138 'Unirme': 'Unirme a la Sesión',
139 '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Registra tu duda académica en el sistema...'
140 },
141 invernadero: {
142 'Publicar duda': 'Plantar la consulta',
143 'Publicar Duda': 'Plantar la consulta',
144 'Publicar': 'Plantar la consulta',
145 'Publicar de todos modos': 'Plantar de todos modos',
146 'Cerrar sesión': 'Regar y cerrar sesión',
147 'Guardar': 'Guardar en el semillero',
148 'Enviar Mensaje Privado': 'Enviar rama privada',
149 '+ Crear sesión de repaso': '+ Crear sesión de cultivo',
150 'Crear sesión': 'Crear sesión de cultivo',
151 'Unirme': 'Unirme al cultivo',
152 '¿Tienes una duda académica? Pregunta a tu comunidad...': 'Siembra una duda o nota para tu comunidad...'
153 }
154 },
155
156 guardado() {
157 try { return localStorage.getItem(this.KEY) || 'clasico'; } catch (e) { return 'clasico'; }
158 },
159
160 /** Estilo activo en este momento (sin consultar storage). */
161 activo() {
162 return document.documentElement.getAttribute('data-estilo') || 'clasico';
163 },
164
165 /* ------------------------------------------------------------
166 Fondos de estilo (elementos inyectados al final de <body>)
167 ------------------------------------------------------------ */
168
169 _cyberSVG() {
170 return `
171 <svg class="cyber-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true">
172 <rect width="1400" height="900" fill="#010204"/>
173 <g class="data-link">
174 <line x1="200" y1="200" x2="500" y2="350"/>
175 <line x1="500" y1="350" x2="700" y2="200"/>
176 <line x1="700" y1="200" x2="1000" y2="300"/>
177 <line x1="1000" y1="300" x2="1200" y2="600"/>
178 <line x1="500" y1="350" x2="600" y2="650"/>
179 <line x1="600" y1="650" x2="300" y2="750"/>
180 <line x1="300" y1="750" x2="200" y2="200"/>
181 <line x1="700" y1="200" x2="850" y2="500"/>
182 <line x1="850" y1="500" x2="1200" y2="600"/>
183 <line x1="500" y1="350" x2="850" y2="500"/>
184 </g>
185 <circle class="data-node" cx="200" cy="200" r="6"/>
186 <circle class="data-node" cx="1000" cy="300" r="5"/>
187 <circle class="data-node" cx="600" cy="650" r="6"/>
188 <circle class="data-node" cx="300" cy="750" r="5"/>
189 <circle class="data-node" cx="1200" cy="600" r="7"/>
190 <circle class="core-node" cx="700" cy="350" r="16"/>
191 <circle class="data-node" cx="500" cy="350" r="8"/>
192 <circle class="data-node" cx="700" cy="200" r="8"/>
193 <circle class="data-node" cx="850" cy="500" r="9"/>
194 </svg>`;
195 },
196
197 /** Mapa de página -> posición de cámara (pan/zoom) sobre el skyline del
198 castillo. dx/dy en píxeles sobre la capa de 2600x1100 y escala. */
199 _castilloZonas() {
200 return {
201 'inicio.html': { dx: 0, dy: 40, scale: 0.62 },
202 'perfil.html': { dx: 1000, dy: -60, scale: 1.15 },
203 'certificados.html': { dx: 650, dy: -40, scale: 1.1 },
204 'comunidad.html': { dx: 300, dy: -20, scale: 1.05 },
205 'mensajes.html': { dx: -300, dy: -30, scale: 1.15 },
206 'empresarial.html': { dx: -650, dy: -10, scale: 1.05 },
207 'circulos.html': { dx: -950, dy: -30, scale: 1.1 },
208 'ajustes.html': { dx: -1200, dy: -20, scale: 1.2 },
209 'admin.html': { dx: 0, dy: 60, scale: 1.25 },
210 'docente.html': { dx: 0, dy: 40, scale: 1.15 }
211 };
212 },
213
214 /** Skyline panorámico del castillo (2600x1100) con zonas por sección. */
215 _castleSkyline() {
216 return `
217 <svg class="castle-layer" id="castleLayer" viewBox="0 0 2600 1100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
218 <defs>
219 <linearGradient id="skyGrad" x1="0" y1="0" x2="0" y2="1">
220 <stop offset="0%" stop-color="#1C0F0B"/>
221 <stop offset="100%" stop-color="#0E0806"/>
222 </linearGradient>
223 <linearGradient id="stoneGrad" x1="0" y1="0" x2="0" y2="1">
224 <stop offset="0%" stop-color="#4A2E24"/>
225 <stop offset="100%" stop-color="#2A1A15"/>
226 </linearGradient>
227 </defs>
228
229 <rect width="2600" height="1100" fill="url(#skyGrad)"/>
230 <circle cx="1300" cy="180" r="90" fill="#3A2C1E" opacity="0.5"/>
231
232 <g fill="#180E0A" opacity="0.7">
233 <polygon points="0,500 200,380 400,500 600,410 800,500 1000,430 1200,500 1400,420 1600,500 1800,440 2000,500 2200,410 2400,500 2600,440 2600,700 0,700"/>
234 </g>
235
236 <g id="zone-perfil">
237 <rect x="220" y="480" width="160" height="420" fill="url(#stoneGrad)"/>
238 <polygon points="200,480 300,380 400,480" fill="#3E1717"/>
239 <rect x="270" y="560" width="60" height="90" fill="#0E0806"/>
240 <rect x="245" y="700" width="30" height="50" fill="#0E0806"/>
241 <rect x="325" y="700" width="30" height="50" fill="#0E0806"/>
242 <rect x="290" y="360" width="20" height="40" fill="#A9834B"/>
243 <polygon points="290,360 300,335 310,360" fill="#A9834B"/>
244 </g>
245
246 <g id="zone-ruta">
247 <rect x="560" y="440" width="180" height="460" fill="url(#stoneGrad)"/>
248 <polygon points="540,440 650,320 760,440" fill="#3E1717"/>
249 <circle cx="650" cy="520" r="34" fill="#0E0806"/>
250 <circle cx="650" cy="520" r="24" fill="#A9834B" opacity="0.3"/>
251 <rect x="600" y="720" width="30" height="55" fill="#0E0806"/>
252 <rect x="670" y="720" width="30" height="55" fill="#0E0806"/>
253 <rect x="640" y="300" width="20" height="35" fill="#A9834B"/>
254 <polygon points="640,300 650,278 660,300" fill="#A9834B"/>
255 <path d="M615,470 L685,470 M650,470 L650,510" stroke="#A9834B" stroke-width="2" opacity="0.4"/>
256 </g>
257
258 <g id="zone-comunidad">
259 <rect x="900" y="560" width="420" height="340" fill="url(#stoneGrad)"/>
260 <polygon points="880,560 1110,470 1340,560" fill="#3E1717"/>
261 <circle cx="960" cy="640" r="26" fill="#0E0806"/>
262 <circle cx="1040" cy="640" r="26" fill="#0E0806"/>
263 <circle cx="1120" cy="640" r="26" fill="#0E0806"/>
264 <circle cx="1200" cy="640" r="26" fill="#0E0806"/>
265 <circle cx="1280" cy="640" r="26" fill="#0E0806"/>
266 <rect x="950" y="760" width="45" height="60" fill="#0E0806"/>
267 <rect x="1240" y="760" width="45" height="60" fill="#0E0806"/>
268 </g>
269
270 <g id="zone-inicio">
271 <rect x="1180" y="330" width="240" height="570" fill="url(#stoneGrad)"/>
272 <polygon points="1150,330 1300,180 1450,330" fill="#3E1717"/>
273 <rect x="1270" y="150" width="60" height="60" fill="#3E1717"/>
274 <polygon points="1270,150 1300,110 1330,150" fill="#5C2323"/>
275 <rect x="1290" y="90" width="10" height="40" fill="#A9834B"/>
276 <polygon points="1300,90 1330,105 1300,110" fill="#5C2323"/>
277 <circle cx="1300" cy="430" r="46" fill="#0E0806"/>
278 <circle cx="1300" cy="430" r="32" fill="#A9834B" opacity="0.25"/>
279 <rect x="1225" y="560" width="60" height="90" fill="#0E0806"/>
280 <rect x="1315" y="560" width="60" height="90" fill="#0E0806"/>
281 <rect x="1250" y="760" width="100" height="140" fill="#0E0806"/>
282 <polygon points="1250,760 1300,715 1350,760" fill="#180E0A"/>
283 <rect x="900" y="850" width="800" height="30" fill="#241512"/>
284 <g fill="#241512">
285 <rect x="900" y="826" width="20" height="24"/><rect x="940" y="826" width="20" height="24"/>
286 <rect x="980" y="826" width="20" height="24"/><rect x="1020" y="826" width="20" height="24"/>
287 <rect x="1060" y="826" width="20" height="24"/><rect x="1100" y="826" width="20" height="24"/>
288 <rect x="1140" y="826" width="20" height="24"/><rect x="1180" y="826" width="20" height="24"/>
289 <rect x="1220" y="826" width="20" height="24"/><rect x="1260" y="826" width="20" height="24"/>
290 <rect x="1300" y="826" width="20" height="24"/><rect x="1340" y="826" width="20" height="24"/>
291 <rect x="1380" y="826" width="20" height="24"/><rect x="1420" y="826" width="20" height="24"/>
292 <rect x="1460" y="826" width="20" height="24"/><rect x="1500" y="826" width="20" height="24"/>
293 <rect x="1540" y="826" width="20" height="24"/><rect x="1580" y="826" width="20" height="24"/>
294 <rect x="1620" y="826" width="20" height="24"/><rect x="1660" y="826" width="20" height="24"/>
295 </g>
296 </g>
297
298 <g id="zone-mensajes">
299 <rect x="1700" y="470" width="150" height="430" fill="url(#stoneGrad)"/>
300 <polygon points="1680,470 1775,370 1870,470" fill="#3E1717"/>
301 <rect x="1745" y="540" width="24" height="70" fill="#0E0806"/>
302 <rect x="1785" y="540" width="24" height="70" fill="#0E0806"/>
303 <rect x="1730" y="700" width="30" height="50" fill="#0E0806"/>
304 <rect x="1795" y="700" width="30" height="50" fill="#0E0806"/>
305 <path d="M1700,500 L1660,470 M1870,500 L1910,470" stroke="#A9834B" stroke-width="2" opacity="0.5"/>
306 <ellipse cx="1660" cy="465" rx="10" ry="6" fill="#A9834B" opacity="0.5"/>
307 <ellipse cx="1910" cy="465" rx="10" ry="6" fill="#A9834B" opacity="0.5"/>
308 </g>
309
310 <g id="zone-empresarial">
311 <rect x="1970" y="600" width="130" height="300" fill="url(#stoneGrad)"/>
312 <rect x="2110" y="560" width="130" height="340" fill="url(#stoneGrad)"/>
313 <rect x="2250" y="600" width="130" height="300" fill="url(#stoneGrad)"/>
314 <polygon points="1960,600 2035,545 2110,600" fill="#3E1717"/>
315 <polygon points="2100,560 2175,500 2250,560" fill="#5C2323"/>
316 <polygon points="2240,600 2315,545 2390,600" fill="#3E1717"/>
317 <rect x="2000" y="700" width="30" height="45" fill="#0E0806"/>
318 <rect x="2150" y="680" width="30" height="45" fill="#0E0806"/>
319 <rect x="2300" y="700" width="30" height="45" fill="#0E0806"/>
320 <rect x="1990" y="900" width="420" height="14" fill="#A9834B" opacity="0.35"/>
321 </g>
322
323 <g id="zone-circulos">
324 <rect x="2280" y="500" width="220" height="400" fill="url(#stoneGrad)"/>
325 <circle cx="2390" cy="480" r="120" fill="#3E1717"/>
326 <circle cx="2390" cy="480" r="90" fill="#2A1512"/>
327 <circle cx="2390" cy="480" r="16" fill="#A9834B" opacity="0.4"/>
328 <rect x="2330" y="720" width="30" height="55" fill="#0E0806"/>
329 <rect x="2420" y="720" width="30" height="55" fill="#0E0806"/>
330 </g>
331
332 <g id="zone-ajustes">
333 <rect x="2470" y="560" width="110" height="340" fill="url(#stoneGrad)"/>
334 <polygon points="2455,560 2525,480 2595,560" fill="#3E1717"/>
335 <rect x="2500" y="620" width="50" height="60" fill="#0E0806"/>
336 <rect x="2495" y="750" width="26" height="40" fill="#0E0806"/>
337 <rect x="2534" y="750" width="26" height="40" fill="#0E0806"/>
338 <rect x="2515" y="450" width="18" height="34" fill="#A9834B"/>
339 <polygon points="2515,450 2524,430 2533,450" fill="#A9834B"/>
340 </g>
341
342 <g fill="#A9834B" opacity="0.4">
343 <circle cx="150" cy="90" r="2"/><circle cx="480" cy="60" r="2"/>
344 <circle cx="900" cy="100" r="2"/><circle cx="1700" cy="70" r="2"/>
345 <circle cx="2100" cy="110" r="2"/><circle cx="2450" cy="80" r="2"/>
346 </g>
347 </svg>`;
348 },
349
350 /** Posiciona la cámara del castillo según la sección activa del menú. */
351 _panCastilloZona() {
352 const layer = document.querySelector('.castle-layer');
353 if (!layer) return;
354 const activo = document.querySelector('#sidebar .sidebar-nav a.active, .sidebar-nav a.active');
355 const href = activo ? activo.getAttribute('href') : 'inicio.html';
356 const zona = (this._castilloZonas()[href] || this._castilloZonas()['inicio.html']);
357 layer.style.transform = `translate(${zona.dx}px, ${zona.dy}px) scale(${zona.scale})`;
358 },
359
360 /** Re-encuadra la cámara del castillo cuando el menú queda listo, al
361 cambiar de skin y al navegar (la página activa determina la zona). */
362 _bindCastilloPan() {
363 if (this._castlePanBinded) {
364 this._panCastilloZona();
365 return;
366 }
367 this._castlePanBinded = true;
368 document.addEventListener('estilos:cambio', () => this._panCastilloZona());
369 if (document.readyState === 'loading') {
370 document.addEventListener('DOMContentLoaded', () => this._panCastilloZona(), { once: true });
371 } else {
372 this._panCastilloZona();
373 }
374 },
375
376 _saiyanSVG() {
377 return `
378 <svg class="saiyan-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true">
379 <rect width="1400" height="900" fill="#f97316"/>
380
381 <path class="kintoun-cloud" d="M300,500 Q350,420 450,450 Q520,380 620,430 Q700,390 780,460 Q880,440 920,520 Q1000,530 980,620 Q900,680 300,680 Z"/>
382
383 <path class="ki-energy-trail" d="M100,100 L1300,800"/>
384 <path class="ki-energy-trail" d="M200,800 L1200,100"/>
385 <path class="ki-energy-trail" d="M0,450 L1400,450"/>
386 </svg>`;
387 },
388
389 _noirSVG() {
390 return `
391 <svg class="noir-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true">
392 <rect width="1400" height="900" fill="#080305"/>
393 <path d="M0,700 L250,500 L450,550 L750,380 L1050,480 L1400,400 L1400,900 L0,900 Z" fill="#0d0407"/>
394 <path class="neon-sign" d="M300,320 L350,280 L400,320 L350,360 Z"/>
395 <path class="neon-sign" d="M950,250 L1020,250 L1020,310 L950,310 Z"/>
396 <g class="rain-streak">
397 <line x1="100" y1="0" x2="60" y2="150"/>
398 <line x1="300" y1="0" x2="260" y2="150"/>
399 <line x1="500" y1="0" x2="460" y2="150"/>
400 <line x1="700" y1="0" x2="660" y2="150"/>
401 <line x1="900" y1="0" x2="860" y2="150"/>
402 <line x1="1100" y1="0" x2="1060" y2="150"/>
403 <line x1="1300" y1="0" x2="1260" y2="150"/>
404 </g>
405 </svg>`;
406 },
407
408 _alchemySVG() {
409 return `
410 <svg class="alchemy-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true">
411 <rect width="1400" height="900" fill="#020d08"/>
412 <g transform="translate(700, 450)">
413 <circle class="sacred-geometry" cx="0" cy="0" r="350" />
414 <circle class="sacred-geometry" cx="0" cy="0" r="220" />
415 <circle class="sacred-geometry" cx="0" cy="0" r="100" />
416 <polygon class="sacred-geometry" points="0,-350 303,175 -303,175" />
417 <polygon class="sacred-geometry" points="0,350 -303,-175 303,-175" />
418 <circle class="alchemical-ring" cx="0" cy="0" r="280" />
419 </g>
420 </svg>`;
421 },
422
423 _minimalSVG() {
424 return `
425 <svg class="minimal-svg-layer" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true">
426 <rect width="1400" height="900" fill="#f8fafc"/>
427 <rect class="accent-block-blue" x="80" y="60" width="420" height="300" />
428 <rect class="accent-block-red" x="940" y="420" width="360" height="330" />
429 <g class="geo-line">
430 <line x1="0" y1="200" x2="1400" y2="200" />
431 <line x1="0" y1="600" x2="1400" y2="600" />
432 <line x1="340" y1="0" x2="340" y2="900" />
433 <line x1="1060" y1="0" x2="1060" y2="900" />
434 </g>
435 </svg>`;
436 },
437
438 /** Capas de follaje del invernadero: el dosel de hojas se divide en tres
439 profundidades (atrás / medio / frente) que se mecen con el viento. */
440 _greenhouseSVG() {
441 return `
442 <div class="greenhouse-canopy">
443 <svg class="canopy-svg canopy-layer-back leaf-sway" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
444 <g fill="#A9C29B">
445 <ellipse cx="90" cy="30" rx="60" ry="34" transform="rotate(30 90 30)"/>
446 <ellipse cx="180" cy="10" rx="50" ry="28" transform="rotate(-15 180 10)"/>
447 <ellipse cx="1300" cy="20" rx="65" ry="36" transform="rotate(-25 1300 20)"/>
448 <ellipse cx="1220" cy="50" rx="45" ry="26" transform="rotate(20 1220 50)"/>
449 <ellipse cx="70" cy="870" rx="70" ry="38" transform="rotate(-20 70 870)"/>
450 <ellipse cx="1330" cy="860" rx="60" ry="34" transform="rotate(15 1330 860)"/>
451 </g>
452 </svg>
453 <svg class="canopy-svg canopy-layer-mid leaf-sway-rev" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
454 <g stroke="#3E6B4F" stroke-width="3" fill="none" stroke-linecap="round">
455 <path d="M0,0 C40,60 10,140 60,200 C110,260 70,340 120,400"/>
456 <path d="M1400,0 C1360,70 1390,150 1330,210 C1270,270 1310,350 1250,410"/>
457 <path d="M0,900 C50,830 20,750 70,690 C120,630 80,560 130,500"/>
458 <path d="M1400,900 C1350,830 1380,750 1320,690 C1260,630 1300,560 1250,500"/>
459 </g>
460 <g fill="#5E8A63">
461 <ellipse cx="60" cy="200" rx="20" ry="11" transform="rotate(35 60 200)"/>
462 <ellipse cx="120" cy="400" rx="18" ry="10" transform="rotate(-20 120 400)"/>
463 <ellipse cx="1330" cy="210" rx="20" ry="11" transform="rotate(-35 1330 210)"/>
464 <ellipse cx="1250" cy="410" rx="18" ry="10" transform="rotate(20 1250 410)"/>
465 <ellipse cx="70" cy="690" rx="19" ry="10" transform="rotate(-30 70 690)"/>
466 <ellipse cx="130" cy="500" rx="17" ry="9" transform="rotate(25 130 500)"/>
467 <ellipse cx="1320" cy="690" rx="19" ry="10" transform="rotate(30 1320 690)"/>
468 <ellipse cx="1250" cy="500" rx="17" ry="9" transform="rotate(-25 1250 500)"/>
469 </g>
470 </svg>
471 <svg class="canopy-svg canopy-layer-front leaf-drift" viewBox="0 0 1400 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
472 <g fill="#3E6B4F">
473 <ellipse cx="30" cy="0" rx="90" ry="55" transform="rotate(20 30 0)"/>
474 <ellipse cx="1370" cy="0" rx="95" ry="58" transform="rotate(-18 1370 0)"/>
475 <ellipse cx="10" cy="900" rx="85" ry="50" transform="rotate(-22 10 900)"/>
476 <ellipse cx="1390" cy="900" rx="88" ry="52" transform="rotate(18 1390 900)"/>
477 </g>
478 <g fill="#2F5340" opacity="0.5">
479 <circle cx="120" cy="70" r="6"/>
480 <circle cx="1280" cy="80" r="5"/>
481 <circle cx="90" cy="830" r="5"/>
482 <circle cx="1310" cy="820" r="6"/>
483 </g>
484 </svg>
485 </div>
486 <div class="light-dapple"></div>`;
487 },
488
489 _limpiarFondo() {
490 document.querySelectorAll('.cyber-bg-viewport, .medieval-bg-viewport, .saiyan-bg-viewport, .noir-bg-viewport, .alchemy-bg-viewport, .minimal-bg-viewport, .invernadero-bg-viewport, .greenhouse-canopy, .light-dapple, .atmosphere-overlay, .castle-vignette, .cyber-blade-transition, .med-blade-transition, .saiyan-aura-transition, .noir-blade-transition, .alchemy-blade-transition, .minimal-blade-transition, .invernadero-leaf-transition, .heraldic-transition').forEach(el => el.remove());
491 },
492
493 _limpiarAccentoInline() {
494 const r = document.documentElement;
495 for (const k of this._VARIABLES_ACCENTO) r.style.removeProperty(k);
496 },
497
498 /** Carga las fuentes decorativas (Cinzel/Bangers/Montserrat) cuando el
499 skin activo las usa; evita cargarlas en skins que no las necesitan. */
500 _cargarFuentes() {
501 if (this._fuentesCargadas) return;
502 this._fuentesCargadas = true;
503 const id = 'estilos-fuentes';
504 if (document.getElementById(id)) return;
505 const link = document.createElement('link');
506 link.id = id;
507 link.rel = 'stylesheet';
508 link.href = 'https://fonts.googleapis.com/css2?family=Bangers&family=Cinzel:wght@400;700;900&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@500;600;700;900&family=Share+Tech+Mono&family=Special+Elite&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Work+Sans:wght@400;500;600;700&display=swap';
509 document.head.appendChild(link);
510 },
511
512 _ponFondo() {
513 const estilo = this.activo();
514 if (estilo === 'medieval') return this._ponFondoMedieval();
515 if (estilo === 'saiyan') return this._ponFondoSaiyan();
516 if (estilo === 'noir') return this._ponFondoNoir();
517 if (estilo === 'alchemy') return this._ponFondoAlchemy();
518 if (estilo === 'minimal') return this._ponFondoMinimal();
519 if (estilo === 'invernadero') return this._ponFondoInvernadero();
520 return this._ponFondoCyber();
521 },
522
523 _ponFondoCyber() {
524 if (document.querySelector('.cyber-bg-viewport')) return;
525 const inyectar = () => {
526 const viewport = document.createElement('div');
527 viewport.className = 'cyber-bg-viewport';
528 viewport.innerHTML = this._cyberSVG();
529
530 const atmosfera = document.createElement('div');
531 atmosfera.className = 'atmosphere-overlay';
532
533 // Cortina de transición "acordeón neural" al navegar (mockup).
534 const blade = document.createElement('div');
535 blade.className = 'cyber-blade-transition';
536 blade.id = 'accordionBlade';
537 blade.innerHTML = `
538 <div class="accordion-panel"></div>
539 <div class="accordion-panel"></div>
540 <div class="accordion-panel"></div>
541 <div class="accordion-panel"></div>`;
542
543 document.body.insertBefore(viewport, document.body.firstChild);
544 document.body.insertBefore(atmosfera, document.body.firstChild);
545 document.body.insertBefore(blade, document.body.firstChild);
546 this._registrarParallax(viewport.querySelector('.cyber-svg-layer'), 'cyber');
547 this._bindBlade();
548 };
549
550 if (document.body) {
551 inyectar();
552 } else {
553 // El script corre en <head> (anti-FOUC): el fondo se inyecta al
554 // terminar de cargar el DOM, y de paso se vuelve a liberar el
555 // acento por si colores.js aplicó sus variables inline.
556 document.addEventListener('DOMContentLoaded', () => {
557 this._limpiarAccentoInline();
558 inyectar();
559 }, { once: true });
560 }
561 },
562
563 _ponFondoMedieval() {
564 if (document.querySelector('.medieval-bg-viewport')) return;
565 this._cargarFuentes();
566 const inyectar = () => {
567 const viewport = document.createElement('div');
568 viewport.className = 'medieval-bg-viewport';
569 viewport.innerHTML = this._castleSkyline();
570
571 // Viñeta nocturna del castillo.
572 const vineta = document.createElement('div');
573 vineta.className = 'castle-vignette';
574
575 // Cortina heráldica "sello del castillo" al navegar (mockup El Castillo).
576 const heraldico = document.createElement('div');
577 heraldico.className = 'heraldic-transition';
578 heraldico.id = 'heraldicTransition';
579
580 document.body.insertBefore(viewport, document.body.firstChild);
581 document.body.insertBefore(vineta, document.body.firstChild);
582 document.body.insertBefore(heraldico, document.body.firstChild);
583 this._bindCastilloPan();
584 this._bindBlade();
585 };
586
587 if (document.body) {
588 inyectar();
589 } else {
590 document.addEventListener('DOMContentLoaded', () => {
591 this._limpiarAccentoInline();
592 inyectar();
593 }, { once: true });
594 }
595 },
596
597 _ponFondoSaiyan() {
598 if (document.querySelector('.saiyan-bg-viewport')) return;
599 this._cargarFuentes();
600 const inyectar = () => {
601 const viewport = document.createElement('div');
602 viewport.className = 'saiyan-bg-viewport';
603 viewport.innerHTML = this._saiyanSVG();
604
605 const atmosfera = document.createElement('div');
606 atmosfera.className = 'atmosphere-overlay';
607
608 // Cortina de transición "aura de ki" al navegar (mockup Saiyan).
609 const blade = document.createElement('div');
610 blade.className = 'saiyan-aura-transition';
611
612 document.body.insertBefore(viewport, document.body.firstChild);
613 document.body.insertBefore(atmosfera, document.body.firstChild);
614 document.body.insertBefore(blade, document.body.firstChild);
615 this._registrarParallax(viewport.querySelector('.saiyan-svg-layer'), 'saiyan');
616 this._bindBlade();
617 };
618
619 if (document.body) {
620 inyectar();
621 } else {
622 document.addEventListener('DOMContentLoaded', () => {
623 this._limpiarAccentoInline();
624 inyectar();
625 }, { once: true });
626 }
627 },
628
629 _ponFondoNoir() {
630 if (document.querySelector('.noir-bg-viewport')) return;
631 this._cargarFuentes();
632 const inyectar = () => {
633 const viewport = document.createElement('div');
634 viewport.className = 'noir-bg-viewport';
635 viewport.innerHTML = this._noirSVG();
636
637 const atmosfera = document.createElement('div');
638 atmosfera.className = 'atmosphere-overlay';
639
640 // Cortina de transición "barrido" del callejón noir al navegar (mockup Noir).
641 const blade = document.createElement('div');
642 blade.className = 'noir-blade-transition';
643
644 document.body.insertBefore(viewport, document.body.firstChild);
645 document.body.insertBefore(atmosfera, document.body.firstChild);
646 document.body.insertBefore(blade, document.body.firstChild);
647 this._registrarParallax(viewport.querySelector('.noir-svg-layer'), 'noir');
648 this._bindBlade();
649 };
650
651 if (document.body) {
652 inyectar();
653 } else {
654 document.addEventListener('DOMContentLoaded', () => {
655 this._limpiarAccentoInline();
656 inyectar();
657 }, { once: true });
658 }
659 },
660
661 _ponFondoAlchemy() {
662 if (document.querySelector('.alchemy-bg-viewport')) return;
663 this._cargarFuentes();
664 const inyectar = () => {
665 const viewport = document.createElement('div');
666 viewport.className = 'alchemy-bg-viewport';
667 viewport.innerHTML = this._alchemySVG();
668
669 const atmosfera = document.createElement('div');
670 atmosfera.className = 'atmosphere-overlay';
671
672 // Cortina de transmutación al navegar (mockup Alchemical Codex).
673 const blade = document.createElement('div');
674 blade.className = 'alchemy-blade-transition';
675
676 document.body.insertBefore(viewport, document.body.firstChild);
677 document.body.insertBefore(atmosfera, document.body.firstChild);
678 document.body.insertBefore(blade, document.body.firstChild);
679 this._registrarParallax(viewport.querySelector('.alchemy-svg-layer'), 'alchemy');
680 this._bindBlade();
681 };
682
683 if (document.body) {
684 inyectar();
685 } else {
686 document.addEventListener('DOMContentLoaded', () => {
687 this._limpiarAccentoInline();
688 inyectar();
689 }, { once: true });
690 }
691 },
692
693 _ponFondoMinimal() {
694 if (document.querySelector('.minimal-bg-viewport')) return;
695 this._cargarFuentes();
696 const inyectar = () => {
697 const viewport = document.createElement('div');
698 viewport.className = 'minimal-bg-viewport';
699 viewport.innerHTML = this._minimalSVG();
700
701 // Cortina tricolor (azul→blanco→rojo) al navegar (mockup Swiss).
702 const blade = document.createElement('div');
703 blade.className = 'minimal-blade-transition';
704
705 document.body.insertBefore(viewport, document.body.firstChild);
706 document.body.insertBefore(blade, document.body.firstChild);
707 this._registrarParallax(viewport.querySelector('.minimal-svg-layer'), 'minimal');
708 this._bindBlade();
709 };
710
711 if (document.body) {
712 inyectar();
713 } else {
714 document.addEventListener('DOMContentLoaded', () => {
715 this._limpiarAccentoInline();
716 inyectar();
717 }, { once: true });
718 }
719 },
720
721 _ponFondoInvernadero() {
722 if (document.querySelector('.invernadero-bg-viewport')) return;
723 this._cargarFuentes();
724 const inyectar = () => {
725 const viewport = document.createElement('div');
726 viewport.className = 'invernadero-bg-viewport';
727 viewport.innerHTML = this._greenhouseSVG();
728
729 // Cortina de transición "hoja" al navegar (mockup invernadero).
730 const leaf = document.createElement('div');
731 leaf.className = 'invernadero-leaf-transition';
732 leaf.id = 'leafTransition';
733
734 document.body.insertBefore(viewport, document.body.firstChild);
735 document.body.insertBefore(leaf, document.body.firstChild);
736 this._bindBlade();
737 };
738
739 if (document.body) {
740 inyectar();
741 } else {
742 document.addEventListener('DOMContentLoaded', () => {
743 this._limpiarAccentoInline();
744 inyectar();
745 }, { once: true });
746 }
747 },
748
749 /**
750 * Motor de parallax unificado: un único listener de mousemove comparte el
751 * cursor y una única RAF por frame aplica el transform al fondo del skin
752 * activo. En vez de 6 listeners y una escritura de estilo por evento,
753 * hay 1 listener y como máximo 1 write por frame (lo esencial para
754 * suavidad en pantallas de 60-120 Hz).
755 */
756 _registrarParallax(layer, color, extra) {
757 if (!layer || !color || layer.dataset.parallax) return;
758 layer.dataset.parallax = '1';
759 if (!this._parallaxCapas) this._parallaxCapas = {};
760 this._parallaxCapas[color] = { layer, extra: extra || null };
761 if (this._parallaxListo) return;
762 this._parallaxListo = true;
763 window.addEventListener('mousemove', e => {
764 this._parallaxX = e.clientX;
765 this._parallaxY = e.clientY;
766 if (this._parallaxRAFId) return;
767 this._parallaxRAFId = requestAnimationFrame(() => {
768 this._parallaxRAFId = 0;
769 this._aplicarParallax();
770 });
771 }, { passive: true });
772 },
773
774 /** Aplica el parallax del skin activo con su geometría específica. */
775 _aplicarParallax() {
776 const estilo = document.documentElement.getAttribute('data-estilo');
777 const capa = this._parallaxCapas && this._parallaxCapas[estilo];
778 if (!capa) return;
779 const layer = capa.layer;
780 // Quita la transición CSS en el primer movimiento para que el
781 // parallax siga el cursor de forma inmediata y no se sienta "muerto".
782 if (layer.style.transition !== 'none') layer.style.transition = 'none';
783 const x = (this._parallaxX / window.innerWidth) - 0.5;
784 const y = (this._parallaxY / window.innerHeight) - 0.5;
785 let t = '';
786 switch (estilo) {
787 case 'cyber':
788 t = `translate3d(${x * 30}px, ${y * 22}px, 0) scale(1.05) rotateX(${15 - y * 8}deg)`;
789 break;
790 case 'medieval':
791 t = `translate3d(${x * 24}px, ${y * 18}px, 0) scale(${1 + y * 0.05}) rotateY(${x * 4}deg)`;
792 if (capa.extra) capa.extra.style.transform = `translateX(${x * -35}px) translateY(${y * -22}px)`;
793 break;
794 case 'saiyan':
795 t = `translate3d(${x * -30}px, ${y * -20}px, 0) scale(${1 + y * 0.06})`;
796 break;
797 case 'noir':
798 t = `translate3d(${x * 22}px, ${y * 16}px, 0) scale(${1 + y * 0.05}) rotateX(${8 - y * 5}deg)`;
799 break;
800 case 'alchemy':
801 t = `translate3d(${x * 30}px, ${y * 22}px, 0) scale(${1 + y * 0.05}) rotateX(${15 - y * 8}deg)`;
802 break;
803 case 'minimal':
804 t = `translate3d(${x * 18}px, ${y * 14}px, 0) scale(${1 + y * 0.03}) rotateX(${4 - y * 2}deg)`;
805 break;
806 }
807 if (t) layer.style.transform = t;
808 },
809
810 /** Cortina de transición al navegar entre páginas del sidebar. */
811 _bindBlade() {
812 if (this._bladeBinded) return;
813 this._bladeBinded = true;
814
815 document.addEventListener('click', (e) => {
816 const estilo = document.documentElement.getAttribute('data-estilo');
817 if (estilo !== 'cyber' && estilo !== 'medieval' && estilo !== 'saiyan' && estilo !== 'noir' && estilo !== 'alchemy' && estilo !== 'minimal' && estilo !== 'invernadero') return;
818 if (!(e.target instanceof Element)) return;
819
820 const enlace = e.target.closest('#sidebar a[href$=".html"], .sidebar-nav a[href$=".html"]');
821 if (!enlace) return;
822 if (e.defaultPrevented) return;
823 if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (e.button !== undefined && e.button !== 0)) return;
824
825 const href = enlace.getAttribute('href');
826 if (!href || href === '#') return;
827
828 if (estilo === 'medieval') {
829 // Transición heráldica del sello del castillo (mockup El Castillo).
830 const heraldico = document.querySelector('.heraldic-transition');
831 if (!heraldico) return;
832 e.preventDefault();
833 this._triggerHeraldico(() => { window.location.href = href; });
834 return;
835 }
836
837 if (estilo === 'cyber') {
838 // Transición de "acordeón neural": cierra los pliegues, cambia la
839 // vista en el punto medio y luego abre los pliegues nuevamente.
840 const accordion = document.querySelector('.cyber-blade-transition');
841 if (!accordion) return;
842 e.preventDefault();
843 this._triggerAccordion(() => { window.location.href = href; });
844 return;
845 }
846
847 if (estilo === 'invernadero') {
848 // Cortina de círculo "hoja creciendo" (mockup invernadero).
849 const leaf = document.querySelector('.invernadero-leaf-transition');
850 if (!leaf) return;
851 e.preventDefault();
852 this._triggerLeaf(() => { window.location.href = href; });
853 return;
854 }
855
856 const blade = document.querySelector(estilo === 'noir' ? '.noir-blade-transition' : estilo === 'alchemy' ? '.alchemy-blade-transition' : estilo === 'minimal' ? '.minimal-blade-transition' : '.saiyan-aura-transition');
857 if (!blade) return;
858
859 e.preventDefault();
860 blade.classList.add('active');
861 setTimeout(() => { window.location.href = href; }, estilo === 'noir' ? 340 : estilo === 'alchemy' ? 340 : estilo === 'minimal' ? 350 : 350);
862 }, true);
863 },
864
865 /** Dispara la cortina de acordeón: cierra los pliegues, ejecuta onComplete
866 en el punto medio (400 ms) y abre los pliegues nuevamente. */
867 _triggerAccordion(onComplete) {
868 const accordion = document.querySelector('.cyber-blade-transition');
869 if (!accordion) {
870 if (typeof onComplete === 'function') onComplete();
871 return;
872 }
873
874 accordion.classList.add('active');
875
876 setTimeout(() => {
877 if (typeof onComplete === 'function') onComplete();
878 accordion.classList.remove('active');
879 }, 400);
880 },
881
882 /** Cortina de círculo "hoja creciendo" del invernadero: se abre un círculo
883 de follaje, se cambia la vista en el pico y se cierra de nuevo. */
884 _triggerLeaf(onComplete) {
885 const leaf = document.querySelector('.invernadero-leaf-transition');
886 if (!leaf) {
887 if (typeof onComplete === 'function') onComplete();
888 return;
889 }
890 leaf.classList.add('active');
891 setTimeout(() => {
892 if (typeof onComplete === 'function') onComplete();
893 leaf.classList.remove('active');
894 }, 320);
895 },
896
897 /** Dispara la cortina heráldica del sello: se abre un círculo desde un
898 vértice superior y ejecuta onComplete en el punto medio, luego se limpia. */
899 _triggerHeraldico(onComplete) {
900 const container = document.querySelector('.heraldic-transition');
901 if (!container) {
902 if (typeof onComplete === 'function') onComplete();
903 return;
904 }
905
906 container.classList.add('active');
907 this._panCastilloZona();
908
909 setTimeout(() => {
910 if (typeof onComplete === 'function') onComplete();
911 }, 260);
912
913 setTimeout(() => {
914 container.classList.remove('active');
915 }, 620);
916 },
917
918 /* ------------------------------------------------------------
919 Aplicación
920 ------------------------------------------------------------ */
921
922 /** Aplica un estilo visual y (por defecto) lo guarda como preferencia. */
923 set(id, guardar = true) {
924 const estilo = this.STYLES.some(s => s.id === id) ? id : 'clasico';
925 const r = document.documentElement;
926 r.setAttribute('data-estilo', estilo);
927
928 if (estilo === 'clasico') {
929 this._limpiarFondo();
930 // El color de acento vuelve a manos de la personalización (js/colores.js).
931 if (typeof Colores !== 'undefined') Colores.aplicar();
932 } else {
933 // El estilo define su propia paleta: libera las variables inline de
934 // la personalización para que las del skin (estilos.css) manden.
935 this._limpiarAccentoInline();
936 this._ponFondo();
937 }
938
939 if (guardar) {
940 try { localStorage.setItem(this.KEY, estilo); } catch (e) { /* almacenamiento no disponible */ }
941 }
942 document.dispatchEvent(new CustomEvent('estilos:cambio', { detail: { estilo } }));
943 this._aplicarTextos();
944 if (guardar) this._mostrarRecarga();
945 return estilo;
946 },
947
948 /** Aplica los textos temáticos del skin activo sobre botones, enlaces y
949 placeholders (coincidencia exacta e idempotente). */
950 _aplicarTextos() {
951 const mapa = this.TEXTOS_ESTILO[this.activo()];
952 if (!mapa || !document.body) return;
953 const normalizar = t => (t || '').replace(/\s+/g, ' ').trim();
954 document.querySelectorAll('button, a').forEach(el => {
955 const txt = normalizar(el.textContent);
956 if (mapa[txt]) el.textContent = mapa[txt];
957 });
958 document.querySelectorAll('input, textarea').forEach(el => {
959 const ph = el.getAttribute('placeholder');
960 if (ph && mapa[ph]) el.setAttribute('placeholder', mapa[ph]);
961 });
962 },
963
964 /** Aviso con botón de recarga al cambiar de estilo en caliente: evita que
965 algunos elementos se queden con aspecto raro hasta el próximo full-load. */
966 _mostrarRecarga() {
967 if (document.querySelector('.estilos-recarga-overlay')) return;
968 if (!document.body) {
969 document.addEventListener('DOMContentLoaded', () => this._mostrarRecarga(), { once: true });
970 return;
971 }
972 const acento = { clasico: '#3fb950', cyber: '#00f0ff', medieval: '#A9834B', saiyan: '#facc15', noir: '#e11d48', alchemy: '#10b981', minimal: '#2563eb', invernadero: '#3E6B4F' }[this.activo()] || '#3fb950';
973 const ov = document.createElement('div');
974 ov.className = 'estilos-recarga-overlay';
975 ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.72);backdrop-filter:blur(6px);z-index:999999;display:flex;align-items:center;justify-content:center;padding:1rem;overflow:auto;';
976 ov.innerHTML = `
977 <div class="estilos-recarga-card" style="background:#161b22;border:2px solid ${acento};box-shadow:0 0 45px rgba(0,0,0,0.7);padding:30px 34px;max-width:430px;width:100%;max-height:calc(100vh - 2rem);overflow:auto;text-align:center;flex-shrink:0;margin:auto;">
978 <div style="font-size:2.2rem;line-height:1;">⚡</div>
979 <h3 style="margin:12px 0 8px;font-size:1.3rem;font-weight:900;color:#ffffff;">Estilo aplicado</h3>
980 <p style="margin:0 0 20px;font-size:0.92rem;color:#8b949e;line-height:1.5;">Para que el nuevo estilo se aplique por completo y no se vea raro, recarga la página.</p>
981 <div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
982 <button type="button" class="est-recargar-btn" style="background:${acento};color:#0b1220;border:none;padding:11px 22px;font-weight:800;font-size:0.95rem;cursor:pointer;">Recargar ahora</button>
983 <button type="button" class="est-quedarme-btn" style="background:transparent;color:#c9d1d9;border:1px solid #30363d;padding:10px 18px;font-weight:600;font-size:0.9rem;cursor:pointer;">Quedarme aquí</button>
984 </div>
985 </div>`;
986 document.body.appendChild(ov);
987
988 const cerrar = () => { ov.remove(); };
989 ov.querySelector('.est-recargar-btn').addEventListener('click', () => window.location.reload(), { once: true });
990 ov.querySelector('.est-quedarme-btn').addEventListener('click', cerrar, { once: true });
991 document.addEventListener('keydown', e => { if (e.key === 'Escape') cerrar(); }, { once: true });
992 },
993
994 /** Re-aplica los textos cuando se re-renderiza contenido dinámico
995 (posts, modales, resultados de búsqueda...). */
996 _vigilarTextos() {
997 if (this._texVigila) return;
998 this._texVigila = true;
999 this._aplicarTextos();
1000 if (!('MutationObserver' in window)) return;
1001 if (!this.TEXTOS_ESTILO[this.activo()]) return;
1002 let timer = null;
1003 new MutationObserver(() => {
1004 clearTimeout(timer);
1005 timer = setTimeout(() => this._aplicarTextos(), 350);
1006 }).observe(document.body, { childList: true, subtree: true });
1007 },
1008
1009 /** Aplica el estilo guardado al cargar la página (sin volver a guardar). */
1010 init() {
1011 this.set(this.guardado(), false);
1012 if (document.body) {
1013 this._vigilarTextos();
1014 } else {
1015 document.addEventListener('DOMContentLoaded', () => this._vigilarTextos(), { once: true });
1016 }
1017 }
1018 };
1019
1020 /* Se aplica en cuanto se carga el script, igual que Tema y Colores (evita parpadeo). */
1021 Estilos.init();