PageSourceSearch

https://lunarapp-urania-production.up.railway.app/static/js/carta-natal.js

js lunarapp-urania-production.up.railway.app collected 2026-10-03 12:13:05 UTC 47,240 bytes, 977 lines download raw bytes

1/**
2 * CARTA NATAL - Módulo completo
3 * - Búsqueda de ciudades (Nominatim)
4 * - Timezone automático
5 * - Coordenadas en formato sexagesimal (grados, minutos, segundos, hemisferio)
6 * - Huso horario editable
7 * - Guardar/cargar cartas en localStorage
8 * - Exportar/Importar JSON
9 * - Imprimir y PDF
10 * Reutiliza el modal cf-modal-carta de carta-fase.js
11 */
12
13var cnBuscando = false;
14var cnDebounceTimer = null;
15var cnUltimaCartaData = null;
16
17// ============================================================================
18// INICIALIZACIÓN
19// ============================================================================
20
21document.addEventListener('DOMContentLoaded', function() {
22    var input = document.getElementById('cn-ciudad');
23    if (!input) return;
24    
25    input.addEventListener('input', function() {
26        clearTimeout(cnDebounceTimer);
27        var query = this.value.trim();
28        if (query.length < 3) {
29            document.getElementById('cn-resultados').style.display = 'none';
30            return;
31        }
32        cnDebounceTimer = setTimeout(function() { cnBuscarCiudad(query); }, 400);
33    });
34    
35    document.addEventListener('click', function(e) {
36        if (!e.target.closest('#cn-ciudad') && !e.target.closest('#cn-resultados')) {
37            document.getElementById('cn-resultados').style.display = 'none';
38        }
39    });
40    
41    var fechaInput = document.getElementById('cn-fecha');
42    if (fechaInput) {
43        fechaInput.addEventListener('change', function() {
44            var lat = document.getElementById('cn-lat').value;
45            var lon = document.getElementById('cn-lon').value;
46            if (lat && lon) cnObtenerTimezone(parseFloat(lat), parseFloat(lon));
47        });
48    }
49
50    // Validación: solo números en campos sexagesimales
51    document.querySelectorAll('.cn-sex-input, .cn-huso-input').forEach(function(inp) {
52        inp.addEventListener('keydown', function(e) {
53            // Permitir: backspace, delete, tab, escape, enter, flechas, home, end
54            if ([8,9,13,27,46,35,36,37,38,39,40].indexOf(e.keyCode) !== -1) return;
55            // Permitir Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
56            if ((e.ctrlKey || e.metaKey) && [65,67,86,88].indexOf(e.keyCode) !== -1) return;
57            // Bloquear si no es número
58            if ((e.keyCode < 48 || e.keyCode > 57) && (e.keyCode < 96 || e.keyCode > 105)) {
59                e.preventDefault();
60            }
61        });
62        // Validar rango al perder foco
63        inp.addEventListener('blur', function() {
64            cnValidarCampoSex(this);
65        });
66    });
67});
68
69// ============================================================================
70// VALIDACIÓN SEXAGESIMAL
71// ============================================================================
72
73function cnValidarCampoSex(el) {
74    var val = parseInt(el.value);
75    if (isNaN(val) || val < 0) { el.value = ''; return; }
76    var min = parseInt(el.min) || 0;
77    var max = parseInt(el.max) || 999;
78    if (val < min) el.value = min;
79    if (val > max) el.value = max;
80}
81
82function cnSexADecimal(grados, minutos, segundos) {
83    var g = parseInt(grados) || 0;
84    var m = parseInt(minutos) || 0;
85    var s = parseInt(segundos) || 0;
86    return g + (m / 60) + (s / 3600);
87}
88
89function cnDecimalASex(decimal) {
90    var abs = Math.abs(decimal);
91    var g = Math.floor(abs);
92    var resto = (abs - g) * 60;
93    var m = Math.floor(resto);
94    var s = Math.round((resto - m) * 60);
95    if (s === 60) { m++; s = 0; }
96    if (m === 60) { g++; m = 0; }
97    return { grados: g, minutos: m, segundos: s };
98}
99
100function cnSexagesimalEditado() {
101    var latG = document.getElementById('cn-lat-grados').value;
102    var latM = document.getElementById('cn-lat-min').value;
103    var latS = document.getElementById('cn-lat-seg').value;
104    var latH = document.getElementById('cn-lat-hem').value;
105    
106    var lonG = document.getElementById('cn-lon-grados').value;
107    var lonM = document.getElementById('cn-lon-min').value;
108    var lonS = document.getElementById('cn-lon-seg').value;
109    var lonH = document.getElementById('cn-lon-hem').value;
110    
111    if (latG !== '' && lonG !== '') {
112        var latDec = cnSexADecimal(latG, latM, latS);
113        if (latH === 'S') latDec = -latDec;
114        
115        var lonDec = cnSexADecimal(lonG, lonM, lonS);
116        if (lonH === 'W') lonDec = -lonDec;
117        
118        document.getElementById('cn-lat').value = latDec.toFixed(4);
119        document.getElementById('cn-lon').value = lonDec.toFixed(4);
120        
121        // Mostrar resultado
122        var res = document.getElementById('cn-coords-resultado');
123        res.textContent = '→ Lat: ' + Math.abs(latDec).toFixed(4) + '° ' + latH + 
124            ' | Lon: ' + Math.abs(lonDec).toFixed(4) + '° ' + lonH +
125            ' (decimal: ' + latDec.toFixed(4) + ', ' + lonDec.toFixed(4) + ')';
126        res.style.display = 'block';
127    }
128}
129
130function cnAplicarHuso() {
131    var signo = document.getElementById('cn-tz-signo').value;
132    var horas = parseInt(document.getElementById('cn-tz-horas').value) || 0;
133    var minutos = parseInt(document.getElementById('cn-tz-minutos').value) || 0;
134    
135    var offset = horas + (minutos / 60);
136    if (signo === '-') offset = -offset;
137    
138    document.getElementById('cn-tz-offset').value = offset;
139    
140    // Actualizar display
141    var res = document.getElementById('cn-coords-resultado');
142    var textoActual = res.textContent || '';
143    var husoTexto = 'Huso: UTC' + (offset >= 0 ? '+' : '') + cnFormatHuso(offset);
144    
145    if (textoActual.indexOf('Lat:') >= 0) {
146        // Ya hay coordenadas, agregar huso
147        if (textoActual.indexOf('Huso:') >= 0) {
148            textoActual = textoActual.replace(/\| Huso:.*$/, '| ' + hu
148soTexto);
149        } else {
150            textoActual += ' | ' + husoTexto;
151        }
152        res.textContent = textoActual;
153    } else {
154        res.textContent = husoTexto;
155    }
156    res.style.display = 'block';
157    
158    // Actualizar info box
159    document.getElementById('cn-tz-texto').textContent = '| UTC' + (offset >= 0 ? '+' : '') + cnFormatHuso(offset);
160    document.getElementById('cn-ubicacion-info').style.display = 'block';
161}
162
163function cnFormatHuso(h) {
164    var abs = Math.abs(h);
165    var hh = Math.floor(abs);
166    var mm = Math.round((abs - hh) * 60);
167    if (mm === 0) return String(h >= 0 ? hh : -hh);
168    return (h < 0 ? '-' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm;
169}
170
171// Llenar campos sexagesimales desde decimal (cuando se selecciona ciudad)
172function cnLlenarSexDesdeDecimal(latDec, lonDec) {
173    var lat = cnDecimalASex(latDec);
174    var lon = cnDecimalASex(lonDec);
175    
176    document.getElementById('cn-lat-grados').value = lat.grados;
177    document.getElementById('cn-lat-min').value = lat.minutos;
178    document.getElementById('cn-lat-seg').value = lat.segundos;
179    document.getElementById('cn-lat-hem').value = latDec < 0 ? 'S' : 'N';
180    
181    document.getElementById('cn-lon-grados').value = lon.grados;
182    document.getElementById('cn-lon-min').value = lon.minutos;
183    document.getElementById('cn-lon-seg').value = lon.segundos;
184    document.getElementById('cn-lon-hem').value = lonDec < 0 ? 'W' : 'E';
185}
186
187// Llenar huso desde decimal
188function cnLlenarHusoDesdeDecimal(offset) {
189    document.getElementById('cn-tz-signo').value = offset < 0 ? '-' : '+';
190    var abs = Math.abs(offset);
191    document.getElementById('cn-tz-horas').value = Math.floor(abs);
192    document.getElementById('cn-tz-minutos').value = Math.round((abs - Math.floor(abs)) * 60);
193}
194
195
196// ============================================================================
197// TOGGLE COORDENADAS
198// ============================================================================
199
200function cnToggleCoords() {
201    var div = document.getElementById('cn-coords-manual');
202    var btn = document.getElementById('cn-btn-toggle-coords');
203    if (div.style.display === 'none' || div.style.display === '') {
204        div.style.display = 'block';
205        btn.classList.add('active');
206        btn.innerHTML = '⚙️ Ocultar Coordenadas y Huso Horario';
207    } else {
208        div.style.display = 'none';
209        btn.classList.remove('active');
210        btn.innerHTML = '⚙️ Ingresar / Corregir Coordenadas y Huso Horario';
211    }
212}
213
214
215// ============================================================================
216// BÚSQUEDA DE CIUDADES
217// ============================================================================
218
219async function cnBuscarCiudad(query) {
220    if (cnBuscando) return;
221    cnBuscando = true;
222    var resultadosDiv = document.getElementById('cn-resultados');
223    resultadosDiv.innerHTML = '<div style="padding:0.8rem;text-align:center;color:#888;">🔍 Buscando...</div>';
224    resultadosDiv.style.display = 'block';
225    try {
226        var url = 'https://nominatim.openstreetmap.org/search?q=' + encodeURIComponent(query) +
227            '&format=json&limit=8&accept-language=es&featuretype=city&addressdetails=1';
228        var response = await fetch(url, { headers: { 'User-Agent': 'LunarApp/1.0 (urania.ar)' } });
229        var data = await response.json();
230        if (data.length === 0) {
231            resultadosDiv.innerHTML = '<div style="padding:0.8rem;text-align:center;color:#888;">No se encontraron ciudades</div>';
232            cnBuscando = false; return;
233        }
234        var html = '';
235        data.forEach(function(lugar) {
236            var nombre = lugar.display_name.split(',').slice(0, 3).join(',');
237            var lat = parseFloat(lugar.lat).toFixed(4);
238            var lon = parseFloat(lugar.lon).toFixed(4);
239            html += '<div class="cn-ciudad-item" onclick="cnSeleccionarCiudad(\'' +
240                nombre.replace(/'/g, "\\'") + '\',' + lugar.lat + ',' + lugar.lon + ')">' +
241                '<div style="color:#d4af37;font-size:0.9rem;">' + nombre + '</div>' +
242                '<div style="color:#777;font-size:0.75rem;">Lat: ' + lat + '° | Lon: ' + lon + '°</div></div>';
243        });
244        resultadosDiv.innerHTML = html;
245    } catch(e) {
246        resultadosDiv.innerHTML = '<div style="padding:0.8rem;text-align:center;color:#ff6347;">Error en la búsqueda</div>';
247    }
248    cnBuscando = false;
249}
250
251async function cnSeleccionarCiudad(nombre, lat, lon) {
252    document.getElementById('cn-ciudad').value = nombre;
253    document.getElementById('cn-resultados').style.display = 'none';
254    
255    var latF = parseFloat(lat);
256    var lonF = parseFloat(lon);
257    
258    document.getElementById('cn-lat').value = latF.toFixed(4);
259    document.getElementById('cn-lon').value = lonF.toFixed(4);
260    
261    // Llenar campos sexagesimales
262    cnLlenarSexDesdeDecimal(latF, lonF);
263    
264    await cnObtenerTimezone(latF, lonF);
265}
266
267async function cnObtenerTimezone(lat, lon) {
268    var fecha = document.getElementById('cn-fecha').value || '';
269    var hora = document.getElementById('cn-hora').value || '12:00';
270    var ciudad = document.getElementById('cn-ciudad').value;
271    try {
272        var url = '/api/timezone?lat=' + lat + '&lon=' + lon;
273        if (fecha) url += '&fecha=' + fecha + '&hora=' + hora;
274        var response = await fetch(url);
275        var data = await response.json();
276        if (data.exito) {
277            document.getElementById('cn-tz-offset').value = data.offset_hours;
278            cnLlenarHusoDesdeDecimal(data.offset_hours);
279            document.getElementById('cn-ubicacion-texto').textContent = ciudad.split(',')[0];
280            document.getElementById('cn-tz-texto').textContent = '| ' + cnFormatOffset(data.offset_hours) + ' (' + data.timezone_id + ')';
281            document.getElementById('cn-ubicacion-info').style.display = 'block';
282        }
283    } catch(e) {
284        var offset = Math.round(lon / 15);
285        document.getElementById('cn-tz-offset').value = offset;
286        cnLlenarHusoDesdeDecimal(offset);
287        document.getElementById('cn-ubicacion-texto').textContent = ciudad.split(',')[0];
288        document.getElementById('cn-tz-texto').textContent = '| UTC' + (offset >= 0 ? '+' : '') + offset + ' (estimado)';
289        document.getElementById('cn-ubicacion-info').style.display = 'block';
290    }
291}
292
293function cnFormatOffset(h) {
294    if (h % 1 === 0) return 'UTC' + (h >= 0 ? '+' : '') + h;
295    var hh = Math.floor(Math.abs(h));
296    var mm = Math.round((Math.abs(h) - hh) * 60);
297    return 'UTC' + (h >= 0 ? '+' : '-') + hh + ':' + (mm < 10 ? '0' : '') + mm;
298}
299
300
301// ============================================================================
302// CALCULAR CARTA NATAL
303// ============================================================================
304
305async function cnCalcularCarta() {
306    var fecha = document.getElementById('cn-fecha').value;
307    var hora = document.getElementById('cn-hora').value || '12:00';
308    var lat = document.getElementById('cn-lat').value;
309    var lon = document.getElementById('cn-lon').value;
310    var tzOffset = document.getElementById('cn-tz-offset').value;
311    var ciudad = document.getElementById('cn-ciudad').value;
312    var nombre = document.getElementById('cn-nombre') ? document.getElementById('cn-nombre').value : '';
313
314    if (!fecha) { alert('Ingresá la fecha de nacimiento'); document.getElementById('cn-fecha').focus(); return; }
315    if (!lat || !lon) { 
316        alert('Ingresá un lugar de nacimiento.\nPodés buscar una ciudad o cargar las coordenadas manualmente con el botón "Ingresar / Corregir Coordenadas".'); 
317        return; 
318    }
319    if (tzOffset === '' || tzOffset === undefined || tzOffset === null) { 
320        alert('Falta el huso horario.\nSeleccioná una ciudad (se detecta automáticamente) o ingresalo manualmente.'); 
321        // Abrir panel de coordenadas si está cerrado
322        var div = document.getElementById('cn-coords-manual');
323        if (div.style.display === 'none' || div.style.display === '') cnToggleCoords();
324        return; 
325    }
326
327    if (typeof cfUserLat !== 'undefined') {
328        cfUserLat = parseFloat(lat); cfUserLon = parseFloat(lon);
329        cfUserCiudad = ciudad ? ciudad.split(',')[0] : 'Manual';
330    }
331
332    var fechaDisplay = fecha.split('-').reverse().join('/');
333    var titulo = (nombre ? nombre + ' — ' : '') + 'Carta Natal — ' + fechaDisplay + ' ' + hora + ' — ' + (ciudad ? ciudad.split(',')[0] : 'Manual');
334
335    if (typeof cfCurrentData !== 'undefined') { cfCurrentData = { jd: 0, titulo: titulo, esNatal: true }; cfAstrocartoLoaded = false; }
336
337    document.getElementById('cf-carta-titulo').textContent = titulo;
338    document.getElementById('cf-ubicacion-texto').textContent =
339        (ciudad ? ciudad.split(',')[0] : '') + ' (' + parseFloat(lat).toFixed(2) + '°, ' + parseFloat(lon).toFixed(2) + '°) ' + cnFormatOffset(parseFloat(tzOffset));
340
341    var modal = document.getElementById('cf-modal-carta');
342    modal.style.display = 'block'; document.body.style.overflow = 'hidden';
343
344    document.querySelectorAll('.cf-tab-btn').forEach(function(b) {
345        b.style.background = '#1a1a3e'; b.style.color = '#ccc'; b.style.borderColor = '#2a2a4a'; b.style.fontWeight = 'normal';
346    });
347    document.querySelectorAll('.cf-tab-content').forEach(function(c) { c.style.display = 'none'; });
348    var firstTab = document.querySelector('.cf-tab-btn[data-tab="cf-carta"]');
349    if (firstTab) { firstTab.style.background = 'linear-gradient(135deg,#d4af37,#b8962e)'; firstTab.style.color = '#0f0f1e'; firstTab.style.borderColor = '#d4af37'; firstTab.style.fontWeight = '600'; }
350    document.getElementById('cf-carta').style.display = 'block';
351    document.getElementById('cf-astrochart-container').innerHTML = '<p style="text-align:center;color:#888;padding:2rem;">⏳ Calculando carta natal...</p>';
352
353    var btn = document.getElementById('cn-btn-calcular');
354    if (btn) { btn.disabled = true; btn.textContent = '⏳ Calculando...'; }
355
356    try {
357        var url = '/api/carta-natal?fecha=' + fecha + '&hora=' + hora + '&lat=' + lat + '&lon=' + lon + '&tz_offset=' + tzOffset;
358        var r = await fetch(url);
359        var data = await r.json();
360        if (data.exito) {
361            if (typeof cfCurrentData !== 'undefined') cfCurrentData.jd = data.jd;
362            cnUltimaCartaData = { nombre: nombre, fecha: fecha, hora: hora, ciudad: ciudad, lat: parseFloat(lat), lon: parseFloat(lon), tzOffset: parseFloat(tzOffset), resultado: data };
363            if (typeof cfDibujarCarta === 'function') cfDibujarCarta(data);
364            if (typeof cfLlenarPosiciones === 'function') cfLlenarPosiciones(data.planetas);
365            if (typeof cfLlenarAspectos === 'function') cfLlenarAspectos(data.aspectos);
366            if (typeof cfLlenarCasas === 'function') cfLlenarCasas(data.casas, data.angulares);
367            if (typeof cfLlenarTecnicos === 'function') cfLlenarTecnicos(data.datos_tecnicos);
368            var acciones = document.getElementById('cn-acciones-post');
369            if (acciones) acciones.style.display = 'block';
370        } else {
371            document.getElementById('cf-astrochart-container').innerHTML = '<p style="color:#ff6347;text-align:center;padding:2rem;">❌ ' + data.error + '</p>';
372        }
373    } catch(e) {
374        document.getElementById('cf-astrochart-container').innerHTML = '<p style="color:#ff6347;text-align:center;padding:2rem;">❌ ' + e.message + '</p>';
375    }
376    if (btn) { btn.disabled = false; btn.textContent = '🔮 Calcular Carta Natal'; }
377}
378
379function cnVerCartaGuardada() {
380    if (!cnUltimaCartaData || !cnUltimaCartaData.resultado) { alert('No hay carta calculada.'); return; }
381    var data = cnUltimaCartaData;
382    if (typeof cfUserLat !== 'undefined') { cfUserLat = data.lat; cfUserLon = data.lon; cfUserCiudad = data.ciudad ? data.ciudad.split(',')[0] : 'Manual'; }
383    var fechaDisplay = data.fecha.split('-').reverse().join('/');
384    var titulo = (data.nombre ? data.nombre + ' — ' : '') + 'Carta Natal — ' + fechaDisplay + ' ' + data.hora + ' — ' + (data.ciudad ? data.ciudad.split(',')[0] : 'Manual');
385    document.getElementById('cf-carta-titulo').textContent = titulo;
386    document.getElementById('cf-ubicacion-texto').textContent = (data.ciudad ? data.ciudad.split(',')[0] : '') + ' (' + data.lat.toFixed(2) + '°, ' + data.lon.toFixed(2) + '°) ' + cnFormatOffset(data.tzOffset);
387    var modal = document.getElementById('cf-modal-carta'); modal.style.display = 'block'; document.body.style.overflow = 'hidden';
388    document.querySelectorAll('.cf-tab-btn').forEach(function(b) { b.style.background = '#1a1a3e'; b.style.color = '#ccc'; b.style.borderColor = '#2a2a4a'; b.style.fontWeight = 'normal'; });
389    document.querySelectorAll('.cf-tab-content').forEach(function(c) { c.style.display = 'none'; });
390    var firstTab = document.querySelector('.cf-tab-btn[data-tab="cf-carta"]');
391    if (firstTab) { firstTab.style.background = 'linear-gradient(135deg,#d4af37,#b8962e)'; firstTab.style.color = '#0f0f1e'; firstTab.style.borderColor = '#d4af37'; firstTab.style.fontWeight = '600'; }
392    document.getElementById('cf-carta').style.display = 'block';
393    var res = data.resultado;
394    if (typeof cfCurrentData !== 'undefined') { cfCurrentData = { jd: res.jd, titulo: titulo, esNatal: true }; cfAstrocartoLoaded = false; }
395    if (typeof cfDibujarCarta === 'function') cfDibujarCarta(res);
396    if (typeof cfLlenarPosiciones === 'function') cfLlenarPosiciones(res.planetas);
397    if (typeof cfLlenarAspectos === 'function') cfLlenarAspectos(res.aspectos);
398    if (typeof cfLlenarCasas === 'function') cfLlenarCasas(res.casas, res.angulares);
399    if (typeof cfLlenarTecnicos === 'function') cfLlenarTecnicos(res.datos_tecnicos);
400}
401
402// ============================================================================
403// GUARDAR / CARGAR CARTAS (localStorage)
404// ============================================================================
405
406function cnGuardarDatos() {
407    if (!cnUltimaCartaData) { alert('Calculá una carta primero.'); return; }
408    var nombre = document.getElementById('cn-nombre') ? document.getElementById('cn-nombre').value.trim() : '';
409    if (!nombre) {
410        nombre = prompt('Ingresá un nombre para identificar esta carta:');
411        if (!nombre) return;
412        if (document.getElementById('cn-nombre')) document.getElementById('cn-nombre').value = nombre;
413    }
414    var guardar = {
415        id: Date.now(), nombre: nombre, fecha: cnUltimaCartaData.fecha, hora: cnUltimaCartaData.hora,
416        ciudad: cnUltimaCartaData.ciudad, lat: cnUltimaCartaData.lat, lon: cnUltimaCartaData.lon,
417        tzOffset: cnUltimaCartaData.tzOffset, guardado: new Date().toISOString()
418    };
419    var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]');
420    var existente = cartas.findIndex(function(c) { return c.nombre === nombre; });
421    if (existente >= 0) {
422        if (!confirm('Ya existe "' + nombre + '". ¿Reemplazar?')) return;
423        cartas[existente] = guardar;
424    } else { cartas.push(guardar); }
425    localStorage.setItem('cn_cartas_guardadas', JSON.stringify(cartas));
426    alert('✅ Carta de "' + nombre + '" guardada.');
427    cnCargarGuardadas();
428}
429
430function cnCargarGuardadas() {
431    var lista = document.getElementById('cn-guardadas-lista');
432    if (!lista) return;
433    var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]');
434    if (cartas.length === 0) {
435        lista.innerHTML = '<p style="color:#888;font-size:0.9rem;">No hay cartas guardadas todavía.</p>';
436        return;
437    }
438    cartas.sort(function(a, b) { return b.id - a.id; });
439    var html = '';
440    cartas.forEach(function(carta) {
441        var fechaDisplay = carta.fecha.split('-').reverse().join('/');
442        var ciudadCorta = carta.ciudad ? carta.ciudad.split(',')[0] : 'Manual';
443        html += '<div class="cn-saved-item">' +
444            '<div onclick="cnCargarCarta(' + carta.id + ')" style="flex:1;cursor:pointer;">' +
445            '<div class="cn-saved-name">' + carta.nombre + '</div>' +
446            '<div class="cn-saved-details">' + fechaDisplay + ' ' + carta.hora + ' — ' + ciudadCorta + ' — ' + cnFormatOffset(carta.tzOffset) + '</div>' +
447            '</div>' +
448            '<div class="cn-saved-actions">' +
449            '<button onclick="cnCargarCarta(' + carta.id + ')" title="Calcular">📊</button>' +
450            '<button onclick="cnEliminarCarta(' + carta.id + ')" title="Eliminar">🗑️</button>' +
451            '</div></div>';
452    });
453    html += '<div style="margin-top:0.8rem;text-align:right;">' +
454        '<button class="cn-btn-secondary" onclick="cnExportarCartas()" style="font-size:0.8rem;padding:0.4rem 0.8rem;">📤 Exportar</button> ' +
455        '<button class="cn-btn-secondary" onclick="cnImportarCartas()" style="font-size:0.8rem;padding:0.4rem 0.8rem;">📥 Importar</button></div>';
456    lista.innerHTML = html;
457}
458
459function cnCargarCarta(id) {
460    var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]');
461    var carta = cartas.find(function(c) { return c.id === id; });
462    if (!carta) { alert('Carta no encontrada'); return; }
463    if (document.getElementById('cn-nombre')) document.getElementById('cn-nombre').value = carta.nombre;
464    document.getElementById('cn-fecha').value = carta.fecha;
465    document.getElementById('cn-hora').value = carta.hora;
466    document.getElementById('cn-ciudad').value = carta.ciudad || '';
467    document.getElementById('cn-lat').value = carta.lat;
468    document.getElementById('cn-lon').value = carta.lon;
469    document.getElementById('cn-tz-offset').value = carta.tzOffset;
470    
471    // Llenar sexagesimales
472    cnLlenarSexDesdeDecimal(carta.lat, carta.lon);
473    cnLlenarHusoDesdeDecimal(carta.tzOffset);
474    
475    var ciudadCorta = carta.ciudad ? carta.ciudad.split(',')[0] : 'Manual';
476    document.getElementById('cn-ubicacion-texto').textContent = ciudadCorta;
477    document.getElementById('cn-tz-texto').textContent = '| ' + cnFormatOffset(carta.tzOffset);
478    document.getElementById('cn-ubicacion-info').style.display = 'block';
479    cnCalcularCarta();
480}
481
482function cnEliminarCarta(id) {
483    if (!confirm('¿Eliminar esta carta guardada?')) return;
484    var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]');
485    cartas = cartas.filter(function(c) { return c.id !== id; });
486    localStorage.setItem('cn_cartas_guardadas', JSON.stringify(cartas));
487    cnCargarGuardadas();
488}
489
490// ============================================================================
491// EXPORTAR / IMPORTAR
492// ============================================================================
493
494function cnExportarCartas() {
495    var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]');
496    if (cartas.length === 0) { alert('No hay cartas para exportar.'); return; }
497    var blob = new Blob([JSON.stringify(cartas, null, 2)], { type: 'application/json' });
498    var url = URL.createObjectURL(blob);
499    var a = document.createElement('a'); a.href = url;
500    a.download = 'cartas-natales-' + new Date().toISOString().split('T')[0] + '.json';
501    document.body.appendChild(a); a.click(); document.body.removeChild(a);
502    URL.revokeObjectURL(url);
503}
504
505function cnImportarCartas() {
506    var input = document.createElement('input'); input.type = 'file'; input.accept = '.json';
507    input.onchange = function(e) {
508        var file = e.target.files[0]; if (!file) return;
509        var reader = new FileReader();
510        reader.onload = function(ev) {
511            try {
512                var importadas = JSON.parse(ev.target.result);
513                if (!Array.isArray(importadas)) throw new Error('Formato inválido');
514                var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]');
515                var agregadas = 0;
516                importadas.forEach(function(carta) {
517                    if (carta.nombre && carta.fecha && carta.lat !== undefined) {
518                        var existe = cartas.find(function(c) { return c.nombre === carta.nombre; });
519                        if (!existe) { if (!carta.id) carta.id = Date.now() + agregadas; cartas.push(carta); agregadas++; }
520                    }
521                });
522                localStorage.setItem('cn_cartas_guardadas', JSON.stringify(cartas));
523                alert('✅ Se importaron ' + agregadas + ' cartas nuevas.');
524                cnCargarGuardadas();
525            } catch(err) { alert('Error al importar: ' + err.message); }
526        };
527        reader.readAsText(file);
528    };
529    input.click();
530}
531
532// ============================================================================
533// OVERRIDE cfLlenarTecnicos
534// ============================================================================
535
536cfLlenarTecnicos = function(d) {
537    var c = document.getElementById('cf-datos-tecnicos');
538    if (!d) { c.innerHTML = ''; return; }
539    var row = function(label, value) {
540        return '<div style="display:flex;justify-content:space-between;padding:0.6rem 0;border-bottom:1px solid rgba(255,255,255,0.06);">' +
541            '<span style="color:#888;">' + label + '</span><span style="color:#d4af37;font-weight:600;">' + value + '</span></div>';
542    };
543    var h = '';
544    if (d.tipo === 'natal') {
545        h += row('Tipo', '🔮 Carta Natal');
546        if (d.fecha_local) h += row('Fecha y Hora Local', d.fecha_local);
547        h += row('Fecha UTC', d.fecha_utc);
548        h += row('Día Juliano', d.jd.toFixed(6));
549        if (d.tz_offset !== undefined) h += row('Zona Horaria', cnFormatOffset(d.tz_offset));
550        h += row('Ubicación', d.ubicacion_calculo.lat.toFixed(4) + '°, ' + d.ubicacion_calculo.lon.toFixed(4) + '°');
551    } else {
552        h += row('Fecha UTC', d.fecha_utc);
553        h += row('Día Juliano', d.jd.toFixed(4));
554        h += row('Ubicación', d.ubicacion_calculo.lat.toFixed(4) + '°, ' + d.ubicacion_calculo.lon.toFixed(4) + '°');
555    }
556    c.innerHTML = h;
557};
558
559// ============================================================================
560// IMPRIMIR - Layout profesional A4 una sola página
561// ============================================================================
562
563// Mapeos de símbolos astrológicos
564var CN_PLANET_SYMBOLS = {
565    'Sol':'☉','Luna':'☽','Mercurio':'☿','Venus':'♀','Marte':'♂',
566    'Jupiter':'♃','Júpiter':'♃','Saturno':'♄','Urano':'♅','Neptuno':'♆',
567    'Pluton':'♇','Plutón':'♇','Nodo Norte':'☊','Nodo Sur':'☋'
568};
569var CN_SIGN_SYMBOLS = {
570    'Aries':'♈','Tauro':'♉','Geminis':'♊','Géminis':'♊',
571    'Cancer':'♋','Cáncer':'♋','Leo':'♌','Virgo':'♍',
572    'Libra':'♎','Escorpio':'♏','Escorpión':'♏',
573    'Sagitario':'♐','Capricornio':'♑','Acuario':'♒','Piscis':'♓'
574};
575
576
577// ============================================================================
578// IMPRESION Y PDF - Layout A4 una sola pagina
579// ============================================================================
580
581function cnGetBrightness(color) {
582    if (!color) return 999;
583    color = color.toLowerCase().trim();
584    if (color === 'none' || color === 'transparent' || color === '') return 999;
585    if (color.indexOf('#') === 0) {
586        var hex = color.replace('#','');
587        if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
588        var r = parseInt(hex.substr(0,2),16), g = parseInt(hex.substr(2,2),16), b = parseInt(hex.substr(4,2),16);
589        return (r*299+g*587+b*114)/1000;
590    }
591    if (color.indexOf('rgb') === 0) {
592        var p = color.match(/\d+/g);
593        if (p && p.length >= 3) return (parseInt(p[0])*299+parseInt(p[1])*587+parseInt(p[2])*114)/1000;
594    }
595    return 999;
596}
597
598function cnIsSaturated(color) {
599    if (!color || color.indexOf('#') !== 0) return false;
600    var hex = color.replace('#','').toLowerCase();
601    if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
602    var r = parseInt(hex.substr(0,2),16), g = parseInt(hex.substr(2,2),16), b = parseInt(hex.substr(4,2),16);
603    return (Math.max(r,g,b) - Math.min(r,g,b)) > 50;
604}
605
606function cnModificarSvgParaImpresion(svg, maxPx) {
607    // Asegurar viewBox para escalado proporcional (mismo que carta-fase.js)
608    if (!svg.getAttribute('viewBox')) {
609        // Detectar tamaño original del SVG
610        var origW = parseFloat(svg.getAttribute('width')) || 600;
611        var origH = parseFloat(svg.getAttribute('height')) || 600;
612        svg.setAttribute('viewBox', '0 0 ' + origW + ' ' + origH);
613    }
614    svg.removeAttribute('width');
615    svg.removeAttribute('height');
616    var mp = maxPx || 540;
617    svg.style.width = '100%';
618    svg.style.maxWidth = mp + 'px';
619    svg.style.height = 'auto';
620    svg.style.display = 'block';
621    svg.style.margin = '0 auto';
622
623    var zodiacSyms = '\u2648\u2649\u264A\u264B\u264C\u264D\u264E\u264F\u2650\u2651\u2652\u2653';
624    var planetSyms = '\u2609\u263D\u263F\u2640\u2642\u2643\u2644\u2645\u2646\u2647\u260A\u260B';
625
626    // Circulos y rects: oscuros no-saturados → blanco (fondo, anillo planetas)
627    svg.querySelectorAll('circle, path, rect').forEach(function(el) {
628        var fill = (el.getAttribute('fill') || '').trim();
629        if (!fill || fill === 'none' || fill === 'transparent') return;
630        if (cnGetBrightness(fill) < 130 && !cnIsSaturated(fill)) {
631            el.setAttribute('fill', '#ffffff');
632            if (el.tagName.toLowerCase() === 'circle') {
633                el.setAttribute('stroke', '#bbbbbb');
634                el.setAttribute('stroke-width', '0.5');
635            }
636        }
637    });
638
639    // Textos
640    svg.querySelectorAll('text').forEach(function(t) {
641        var c = t.textContent.trim();
642        if (zodiacSyms.indexOf(c) >= 0) {
643            t.setAttribute('fill', '#ffffff');
644            t.setAttribute('font-weight', 'bold');
645            t.setAttribute('stroke', '#ffffff');
646            t.setAttribute('stroke-width', '0.3');
647        } else if (planetSyms.indexOf(c) >= 0) {
648            t.setAttribute('fill', '#000000');
649            t.setAttribute('font-weight', 'bold');
650            t.setAttribute('stroke', '#000000');
651            t.setAttribute('stroke-width', '0.3');
652        } else {
653            t.setAttribute('fill', '#222222');
654            t.setAttribute('font-weight', 'bold');
655        }
656    });
657
658    // Lineas: asegurar visibilidad
659    svg.querySelectorAll('line').forEach(function(l) {
660        var sw = parseFloat(l.getAttribute('stroke-width') || 1);
661        if (sw < 0.8) l.setAttribute('stroke-width', '0.8');
662        var sc = (l.getAttribute('stroke') || '').toLowerCase();
663        if (sc === '#fff' || sc === '#ffffff' || sc === 'white') l.setAttribute('stroke', '#cccccc');
664    });
665    return svg;
666}
667
668function cnBuildPrintPlanetas(planetas) {
669    var h = '<table style="width:100%;border-collapse:collapse;font-size:8.5pt;">';
670    h += '<tr><th style="text-align:left;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;" colspan="2">PLANETAS</th>';
671    h += '<th style="text-align:right;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;">POSICI\u00D3N</th></tr>';
672    var orden = ['Sol','Luna','Mercurio','Venus','Marte','Jupiter','J\u00FApiter','Saturno','Urano','Neptuno','Pluton','Plut\u00F3n','Nodo Norte','Nodo Sur'];
673    orden.forEach(function(nombre) {
674        var p = planetas[nombre];
675        if (!p) return;
676        var sym = CN_PLANET_SYMBOLS[nombre] || '';
677        var sigSym = CN_SIGN_SYMBOLS[p.signo] || p.signo;
678        var retro = p.retrogrado ? ' \u211E' : '';
679        var gradoStr = p.grado || '';
680        var pos = gradoStr.split("'")[0] + "'";
681        pos = pos.replace(/\u00B0/, '\u00B0' + sigSym);
682        h += '<tr><td style="padding:0 2px;font-size:10pt;width:16px;line-height:1.2;">' + sym + '</td>';
683        h += '<td style="padding:0 2px;font-size:8.5pt;line-height:1.2;">' + nombre + retro + '</td>';
684        h += '<td style="padding:0 2px;text-align:right;font-family:monospace;font-size:8.5pt;line-height:1.2;">' + pos + '</td></tr>';
685    });
686    h += '</table>';
687    return h;
688}
689
690function cnBuildPrintCasas(casas, angulares) {
691    var items = [];
692    if (angulares && angulares['ASC']) items.push({l:'ASC', s:angulares['ASC'].signo, g:angulares['ASC'].grado});
693    else if (casas['Casa 1']) items.push({l:'ASC', s:casas['Casa 1'].signo, g:casas['Casa 1'].grado});
694    if (casas['Casa 2']) items.push({l:'II', s:casas['Casa 2'].signo, g:casas['Casa 2'].grado});
695    if (casas['Casa 3']) items.push({l:'III', s:casas['Casa 3'].signo, g:casas['Casa 3'].grado});
696    if (angulares && angulares['MC']) items.push({l:'MC', s:angulares['MC'].signo, g:angulares['MC'].grado});
697    else if (casas['Casa 10']) items.push({l:'MC', s:casas['Casa 10'].signo, g:casas['Casa 10'].grado});
698    if (casas['Casa 11']) items.push({l:'XI', s:casas['Casa 11'].signo, g:casas['Casa 11'].grado});
699    if (casas['Casa 12']) items.push({l:'XII', s:casas['Casa 12'].signo, g:casas['Casa 12'].grado});
700
701    var h = '<table style="width:100%;border-collapse:collapse;font-size:8.5pt;">';
702    h += '<tr><th style="text-align:left;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;">CASA</th>';
703    h += '<th style="text-align:right;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;">C\u00DASPIDE</th></tr>';
704    items.forEach(function(c) {
705        var sigSym = CN_SIGN_SYMBOLS[c.s] || c.s;
706        var pos = (c.g || '').split("'")[0] + "'";
707        pos = pos.replace(/\u00B0/, '\u00B0' + sigSym);
708        var bold = (c.l === 'ASC' || c.l === 'MC') ? 'font-weight:bold;' : '';
709        h += '<tr><td style="padding:0 2px;line-height:1.2;' + bold + '">' + c.l + '</td>';
710        h += '<td style="padding:0 2px;text-align:right;font-family:monospace;line-height:1.2;' + bold + '">' + pos + '</td></tr>';
711    });
712    h += '</table>';
713    return h;
714}
715
716function cnBuildPrintHeader(data) {
717    var fd = data.fecha.split('-').reverse().join('/');
718    var city = data.ciudad ? data.ciudad.split(',')[0].trim() : 'Manual';
719    var latS = cnDecimalASex(data.lat), lonS = cnDecimalASex(data.lon);
720    var latStr = latS.grados + '\u00B0' + (latS.minutos<10?'0':'') + latS.minutos + "'" + (data.lat<0?'S':'N');
721    var lonStr = lonS.grados + '\u00B0' + (lonS.minutos<10?'0':'') + lonS.minutos + "'" + (data.lon<0?'W':'E');
722    return '<div style="text-align:center;margin-bottom:2px;padding-bottom:2px;border-bottom:2px solid #333;">' +
723        '<div style="font-size:16pt;font-weight:bold;letter-spacing:2px;">CARTA NATAL</div>' +
724        (data.nombre ? '<div style="font-size:13pt;font-weight:bold;color:#222;margin:1px 0;">' + data.nombre + '</div>' : '') +
725        '<div style="font-size:8.5pt;color:#444;">' + fd + ' \u2014 ' + data.hora + ' hs \u2014 ' + city +
726        ' \u2014 ' + latStr + ' ' + lonStr + ' \u2014 ' + cnFormatOffset(data.tzOffset) + '</div></div>';
727}
728
729function cnBuildPrintFooter() {
730    return '<div style="text-align:center;border-top:1px solid #333;padding-top:2px;margin-top:3px;font-size:7pt;color:#555;">' +
731        '<div style="font-size:9pt;font-weight:bold;letter-spacing:1px;">La Naci\u00F3n de Urania</div>' +
732        '<div>Organizaci\u00F3n Astrol\u00F3gica \u2014 Nestor Echarte</div>' +
733        '<div>[email protected] \u2014 WhatsApp: +54 9 294 431 2096 \u2014 naciondeurania.com</div></div>';
734}
735
736function cnBuildFullLayout(data, svgClone, maxSvgPx) {
737    var wrap = document.createElement('div');
738
739    var hdr = document.createElement('div');
740    hdr.innerHTML = cnBuildPrintHeader(data);
741    wrap.appendChild(hdr.firstChild);
742
743    var chartWrap = document.createElement('div');
744    chartWrap.style.cssText = 'text-align:center;margin:2px auto;max-width:' + maxSvgPx + 'px;';
745    chartWrap.appendChild(svgClone);
746    wrap.appendChild(chartWrap);
747
748    var cols = document.createElement('div');
749    cols.style.cssText = 'display:flex;gap:12px;margin-top:1px;';
750    var colP = document.createElement('div');
751    colP.style.cssText = 'flex:1;';
752    colP.innerHTML = cnBuildPrintPlanetas(data.resultado.planetas);
753    var colC = document.createElement('div');
754    colC.style.cssText = 'flex:0 0 140px;';
755    colC.innerHTML = cnBuildPrintCasas(data.resultado.casas, data.resultado.angulares);
756    cols.appendChild(colP);
757    cols.appendChild(colC);
758    wrap.appendChild(cols);
759
760    var foot = document.createElement('div');
761    foot.innerHTML = cnBuildPrintFooter();
762    wrap.appendChild(foot.firstChild);
763
764    return wrap;
765}
766
767// ============================================================================
768// IMPRIMIR
769// ============================================================================
770
771function cfImprimirCarta() {
772    if (!cnUltimaCartaData || !cnUltimaCartaData.resultado) {
773        alert('No hay carta calculada para imprimir.'); return;
774    }
775
776    // En movil, generar PDF directamente (funciona perfecto y el usuario lo imprime desde el visor)
777    var esMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
778    if (esMobile) {
779        cfGuardarPDF();
780        return;
781    }
782
783    // PC: iframe oculto
784    var svgOrig = document.querySelector('#cf-astrochart-container svg');
785    if (!svgOrig) { alert('No se encontro el grafico.'); return; }
786    var svgClone = svgOrig.cloneNode(true);
787    cnModificarSvgParaImpresion(svgClone, 540);
788
789    var layout = cnBuildFullLayout(cnUltimaCartaData, svgClone, 540);
790    var tmpDiv = document.createElement('div');
791    tmpDiv.appendChild(layout);
792
793    var fullHTML = '<!DOCTYPE html><html><head><meta charset="UTF-8">' +
794        '<title>Carta Natal</title>' +
795        '<style>' +
796        '  @page { size: A4 portrait; margin: 8mm 10mm; }' +
797        '  * { box-sizing: border-box; margin: 0; padding: 0; }' +
798        '  body { padding: 10px 20px; background: white;' +
799        '    font-family: "Segoe UI", Arial, sans-serif; color: #222; }' +
800        '  svg { max-width: 540px; width: 100%; height: auto; display: block; margin: 0 auto; }' +
801        '</style></head><body>' +
802        '<div style="max-width:660px;margin:0 auto;">' + tmpDiv.innerHTML + '</div>' +
803        '</body></html>';
804
805    var oldFrame = document.getElementById('cn-print-iframe');
806    if (oldFrame) oldFrame.remove();
807
808    var iframe = document.createElement('iframe');
809    iframe.id = 'cn-print-iframe';
810    iframe.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:800px;height:1100px;border:none;';
811    document.body.appendChild(iframe);
812
813    var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
814    iframeDoc.open();
815    iframeDoc.write(fullHTML);
816    iframeDoc.close();
817
818    iframe.onload = function() {
819        setTimeout(function() {
820            try {
821                iframe.contentWindow.print();
822            } catch(e) {
823                window.print();
824            }
825            setTimeout(function() {
826                var f = document.getElementById('cn-print-iframe');
827                if (f) f.remove();
828            }, 2000);
829        }, 400);
830    };
831}
832
833// ============================================================================
834// GUARDAR PDF
835// ============================================================================
836
837var cfPdfLibsLoaded = false;
838function cfCargarLibsPDF() {
839    return new Promise(function(resolve) {
840        if (cfPdfLibsLoaded) { resolve(); return; }
841        var s1 = document.createElement('script');
842        s1.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
843        s1.onload = function() {
844            var s2 = document.createElement('script');
845            s2.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
846            s2.onload = function() { cfPdfLibsLoaded = true; resolve(); };
847            document.head.appendChild(s2);
848        };
849        document.head.appendChild(s1);
850    });
851}
852
853async function cfGuardarPDF() {
854    if (!cnUltimaCartaData || !cnUltimaCartaData.resultado) {
855        alert('No hay carta calculada para exportar.'); return;
856    }
857    var btn = event.target.closest('button');
858    var orig = btn.innerHTML; btn.innerHTML = '\u23F3'; btn.disabled = true;
859    try {
860        await cfCargarLibsPDF();
861        var data = cnUltimaCartaData;
862        var svgOrig = document.querySelector('#cf-astrochart-container svg');
863        if (!svgOrig) { alert('No se encontro el grafico.'); btn.innerHTML = orig; btn.disabled = false; return; }
864        var svgClone = svgOrig.cloneNode(true);
865
866        // 1) Asegurar viewBox con las dimensiones originales del chart (600x600)
867        var vb = svgClone.getAttribute('viewBox');
868        if (!vb) {
869            var origW = parseFloat(svgClone.getAttribute('width')) || 600;
870            var origH = parseFloat(svgClone.getAttribute('height')) || 600;
871            svgClone.setAttribute('viewBox', '0 0 ' + origW + ' ' + origH);
872        }
873        // Fijar tamaño de renderizado
874        svgClone.setAttribute('width', '860');
875        svgClone.setAttribute('height', '860');
876        svgClone.style.cssText = '';
877
878        // 2) Modificar colores: fondo blanco, planetas oscuros
879        var zodiacSyms = '\u2648\u2649\u264A\u264B\u264C\u264D\u264E\u264F\u2650\u2651\u2652\u2653';
880        var planetSyms = '\u2609\u263D\u263F\u2640\u2642\u2643\u2644\u2645\u2646\u2647\u260A\u260B';
881
882        svgClone.querySelectorAll('circle, path, rect').forEach(function(el) {
883            var fill = (el.getAttribute('fill') || '').trim();
884            if (!fill || fill === 'none' || fill === 'transparent') return;
885            if (cnGetBrightness(fill) < 130 && !cnIsSaturated(fill)) {
886                el.setAttribute('fill', '#ffffff');
887                if (el.tagName.toLowerCase() === 'circle') {
888                    el.setAttribute('stroke', '#bbbbbb');
889                    el.setAttribute('stroke-width', '0.5');
890                }
891            }
892        });
893        svgClone.querySelectorAll('text').forEach(function(t) {
894            var c = t.textContent.trim();
895            if (zodiacSyms.indexOf(c) >= 0) {
896                t.setAttribute('fill', '#ffffff');
897                t.setAttribute('font-weight', 'bold');
898                t.setAttribute('stroke', '#ffffff');
899                t.setAttribute('stroke-width', '0.3');
900            } else if (planetSyms.indexOf(c) >= 0) {
901                t.setAttribute('fill', '#000000');
902                t.setAttribute('font-weight', 'bold');
903                t.setAttribute('stroke', '#000000');
904                t.setAttribute('stroke-width', '0.3');
905            } else {
906                t.setAttribute('fill', '#222222');
907                t.setAttribute('font-weight', 'bold');
908            }
909        });
910        svgClone.querySelectorAll('line').forEach(function(l) {
911            var sw = parseFloat(l.getAttribute('stroke-width') || 1);
912            if (sw < 0.8) l.setAttribute('stroke-width', '0.8');
913            var sc = (l.getAttribute('stroke') || '').toLowerCase();
914            if (sc === '#fff' || sc === '#ffffff' || sc === 'white') l.setAttribute('stroke', '#cccccc');
915        });
916
917        // 3) Rasterizar SVG → Canvas → PNG (html2canvas no escala viewBox bien)
918        var svgData = new XMLSerializer().serializeToString(svgClone);
919        var svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
920        var svgUrl = URL.createObjectURL(svgBlob);
921
922        var chartImg = await new Promise(function(resolve, reject) {
923            var img = new Image();
924            img.onload = function() { resolve(img); };
925            img.onerror = function() { reject(new Error('Error rasterizando SVG')); };
926            img.src = svgUrl;
927        });
928        URL.revokeObjectURL(svgUrl);
929
930        // Dibujar en canvas a alta resolución
931        var rasterCanvas = document.createElement('canvas');
932        rasterCanvas.width = 860;
933        rasterCanvas.height = 860;
934        var ctx = rasterCanvas.getContext('2d');
935        ctx.fillStyle = '#ffffff';
936        ctx.fillRect(0, 0, 860, 860);
937        ctx.drawImage(chartImg, 0, 0, 860, 860);
938        var chartPngUrl = rasterCanvas.toDataURL('image/png');
939
940        // 4) Crear <img> con el PNG (esto html2canvas lo maneja perfecto)
941        var imgEl = document.createElement('img');
942        imgEl.src = chartPngUrl;
943        imgEl.style.cssText = 'width:430px;height:430px;
943display:block;margin:0 auto;';
944
945        // 5) Construir layout A4
946        var pdfDiv = document.createElement('div');
947        pdfDiv.id = 'cn-pdf-layout';
948        pdfDiv.style.cssText = 'position:fixed;top:-9999px;left:0;width:595px;height:842px;padding:20px 28px;background:white;font-family:"Segoe UI",Arial,sans-serif;color:#222;z-index:-1;box-sizing:border-box;overflow:hidden;';
949
950        var layout = cnBuildFullLayout(data, imgEl, 430);
951        layout.style.cssText = 'max-width:539px;margin:0 auto;';
952        pdfDiv.appendChild(layout);
953        document.body.appendChild(pdfDiv);
954
955        await new Promise(function(r) { setTimeout(r, 400); });
956
957        var canvas = await html2canvas(pdfDiv, {
958            backgroundColor: '#ffffff',
959            scale: 2,
960            useCORS: true,
961            logging: false,
962            width: 595,
963            height: 842,
964            windowWidth: 595
965        });
966        document.body.removeChild(pdfDiv);
967
968        var jsPDF = window.jspdf.jsPDF;
969        var pdf = new jsPDF('p', 'mm', 'a4');
970        var imgData = canvas.toDataURL('image/jpeg', 0.95);
971        pdf.addImage(imgData, 'JPEG', 0, 0, 210, 297);
972
973        var titulo = data.nombre || 'carta-natal';
974        pdf.save(titulo.replace(/[^a-zA-Z0-9\u00E1\u00E9\u00ED\u00F3\u00FA\u00F1\u00D1\s-]/g, '').replace(/\s+/g, '_').substring(0,50) + '.pdf');
975    } catch(e) { alert('Error generando PDF: ' + e.message); }
976    btn.innerHTML = orig; btn.disabled = false;
977}

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.