Rafael Lopez

documentacion

...@@ -27,6 +27,8 @@ claude.md ...@@ -27,6 +27,8 @@ claude.md
27 27
28 # Docs internos 28 # Docs internos
29 static/ESTILOS_*.md 29 static/ESTILOS_*.md
30 +static/docs.md
31 +BASES_DE_DATOS.md
30 .Rproj.user 32 .Rproj.user
31 33
32 # Data 34 # Data
......
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
10 "dependencies": { 10 "dependencies": {
11 "@supabase/supabase-js": "^2.98.0", 11 "@supabase/supabase-js": "^2.98.0",
12 "d3": "^7.9.0", 12 "d3": "^7.9.0",
13 - "fuse.js": "^7.1.0" 13 + "fuse.js": "^7.1.0",
14 + "marked": "^17.0.4"
14 }, 15 },
15 "devDependencies": { 16 "devDependencies": {
16 "@sveltejs/adapter-auto": "^7.0.0", 17 "@sveltejs/adapter-auto": "^7.0.0",
...@@ -2335,6 +2336,18 @@ ...@@ -2335,6 +2336,18 @@
2335 "@jridgewell/sourcemap-codec": "^1.5.5" 2336 "@jridgewell/sourcemap-codec": "^1.5.5"
2336 } 2337 }
2337 }, 2338 },
2339 + "node_modules/marked": {
2340 + "version": "17.0.4",
2341 + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
2342 + "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
2343 + "license": "MIT",
2344 + "bin": {
2345 + "marked": "bin/marked.js"
2346 + },
2347 + "engines": {
2348 + "node": ">= 20"
2349 + }
2350 + },
2338 "node_modules/mrmime": { 2351 "node_modules/mrmime": {
2339 "version": "2.0.1", 2352 "version": "2.0.1",
2340 "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", 2353 "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
21 "dependencies": { 21 "dependencies": {
22 "@supabase/supabase-js": "^2.98.0", 22 "@supabase/supabase-js": "^2.98.0",
23 "d3": "^7.9.0", 23 "d3": "^7.9.0",
24 - "fuse.js": "^7.1.0" 24 + "fuse.js": "^7.1.0",
25 + "marked": "^17.0.4"
25 } 26 }
26 } 27 }
......
...@@ -58,23 +58,32 @@ ...@@ -58,23 +58,32 @@
58 gearMenuOpen = !gearMenuOpen; 58 gearMenuOpen = !gearMenuOpen;
59 } 59 }
60 60
61 - function closeGearMenu() {
62 - setTimeout(() => { gearMenuOpen = false; }, 150);
63 - }
64 -
65 onMount(() => { 61 onMount(() => {
66 setTimeout(() => { mounted = true; }, 80); 62 setTimeout(() => { mounted = true; }, 80);
67 initIndex(); 63 initIndex();
68 // Pre-calentar Supabase en background para evitar cold start 64 // Pre-calentar Supabase en background para evitar cold start
69 warmupSupabase(); 65 warmupSupabase();
66 +
67 + // Cerrar gear menu al hacer click fuera
68 + const handleClickOutside = (e) => {
69 + if (gearMenuOpen && !e.target.closest('.gear-wrap')) {
70 + gearMenuOpen = false;
71 + }
72 + };
73 + document.addEventListener('click', handleClickOutside);
74 + return () => document.removeEventListener('click', handleClickOutside);
70 }); 75 });
71 76
72 - // Auto-focus en el buscador cuando el índice esté listo (solo una vez) 77 + // Auto-focus en el buscador cuando el índice esté listo (solo desktop)
73 $: if ($indexLoaded && searchInput && !autoFocused) { 78 $: if ($indexLoaded && searchInput && !autoFocused) {
74 autoFocused = true; 79 autoFocused = true;
75 - setTimeout(() => { 80 + // No auto-focus en móvil para evitar que aparezca el teclado
76 - searchInput?.focus(); 81 + const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
77 - }, 50); 82 + if (!isMobile) {
83 + setTimeout(() => {
84 + searchInput?.focus();
85 + }, 50);
86 + }
78 } 87 }
79 88
80 function toggleMobileSettings() { 89 function toggleMobileSettings() {
...@@ -85,7 +94,7 @@ ...@@ -85,7 +94,7 @@
85 $: accent = isD ? '#4A8B6E' : '#C9A751'; 94 $: accent = isD ? '#4A8B6E' : '#C9A751';
86 $: placeholder = isD 95 $: placeholder = isD
87 ? 'Busca una entidad, un ministerio, un municipio...' 96 ? 'Busca una entidad, un ministerio, un municipio...'
88 - : 'Busca en 7 clasificadores: partidas, rubros, fuentes...'; 97 + : 'Busca instituciones, gastos, ingresos, sectores...';
89 $: examples = isD 98 $: examples = isD
90 ? [ 99 ? [
91 { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, 100 { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
...@@ -93,11 +102,11 @@ ...@@ -93,11 +102,11 @@
93 { label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, 102 { label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
94 ] 103 ]
95 : [ 104 : [
96 - { label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962' }, 105 + { label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962', href: '/objeto/11700' },
97 - { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, 106 + { label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0', href: '/rubro/14000' },
98 { label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' }, 107 { label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' },
99 - { label: 'Consultorías', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, 108 + { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
100 - { label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0' }, 109 + { label: 'Publicidad', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
101 ]; 110 ];
102 111
103 function switchMode(m) { 112 function switchMode(m) {
...@@ -205,9 +214,9 @@ ...@@ -205,9 +214,9 @@
205 ]; 214 ];
206 215
207 const VISUALIZACIONES = [ 216 const VISUALIZACIONES = [
208 - { color: '#4A8B6E', title: 'Evolución del gasto en salud', desc: 'Cómo cambiaron las prioridades en salud a lo largo de dos décadas.', type: 'Serie temporal' }, 217 + { color: '#4A8B6E', title: 'Evolución del gasto en salud', desc: 'Cómo cambiaron las prioridades en salud a lo largo de dos décadas.', type: 'Serie temporal', href: null },
209 - { color: '#C9A751', title: 'Treemap del presupuesto 2025', desc: 'Explora cada peso del presupuesto actual.', type: 'Treemap' }, 218 + { color: '#C9A751', title: '¿En qué se gasta?', desc: 'Explora el presupuesto por objeto de gasto.', type: 'Treemap', href: '/clasificadores/objeto-gasto?modo=mapa' },
210 - { color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa' }, 219 + { color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa', href: null },
211 ]; 220 ];
212 221
213 /* Reveal action */ 222 /* Reveal action */
...@@ -295,7 +304,7 @@ ...@@ -295,7 +304,7 @@
295 <div class="hero-search anim" style="--d:520ms"> 304 <div class="hero-search anim" style="--d:520ms">
296 <div class="search-wrap"> 305 <div class="search-wrap">
297 <!-- Search bar unificado --> 306 <!-- Search bar unificado -->
298 - <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && ($results.length > 0 || ($query.length >= 2 && $results.length === 0) || $isLoading)} style="--accent:{accent}"> 307 + <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && $query.length >= 2 && ($results.length > 0 || $results.length === 0 || $isLoading)} style="--accent:{accent}">
299 <!-- Toggle integrado (desktop) --> 308 <!-- Toggle integrado (desktop) -->
300 <div class="toggle-bar toggle-desktop"> 309 <div class="toggle-bar toggle-desktop">
301 <button class="toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => switchMode('archivo')}> 310 <button class="toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => switchMode('archivo')}>
...@@ -338,7 +347,7 @@ ...@@ -338,7 +347,7 @@
338 /> 347 />
339 {#if !searchVal} 348 {#if !searchVal}
340 <div class="search-placeholder"> 349 <div class="search-placeholder">
341 - <span class="blink-cursor" style="background:{accent}"></span> 350 + <span class="blink-cursor" class:cursor-diario={isD} class:cursor-archivo={!isD}></span>
342 <span class="placeholder-text placeholder-desktop">{placeholder}</span> 351 <span class="placeholder-text placeholder-desktop">{placeholder}</span>
343 <span class="placeholder-text placeholder-mobile">{isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...'}</span> 352 <span class="placeholder-text placeholder-mobile">{isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...'}</span>
344 </div> 353 </div>
...@@ -350,8 +359,8 @@ ...@@ -350,8 +359,8 @@
350 {/if} 359 {/if}
351 </div> 360 </div>
352 <!-- Gear button with filter dropdown (desktop) --> 361 <!-- Gear button with filter dropdown (desktop) -->
353 - <div class="gear-wrap gear-desktop"> 362 + <div class="gear-wrap gear-desktop" on:click|stopPropagation>
354 - <button class="gear-btn" class:gear-active={gearMenuOpen} on:click={toggleGearMenu} on:blur={closeGearMenu}> 363 + <button class="gear-btn" class:gear-active={gearMenuOpen} on:click={toggleGearMenu}>
355 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={gearMenuOpen ? accent : '#B8B5AD'} stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"> 364 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={gearMenuOpen ? accent : '#B8B5AD'} stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
356 <circle cx="12" cy="12" r="3"/> 365 <circle cx="12" cy="12" r="3"/>
357 <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> 366 <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
...@@ -359,21 +368,42 @@ ...@@ -359,21 +368,42 @@
359 </button> 368 </button>
360 {#if gearMenuOpen} 369 {#if gearMenuOpen}
361 <div class="gear-menu"> 370 <div class="gear-menu">
362 - <div class="gear-menu-title">Buscar en:</div> 371 + <div class="gear-menu-title">Ajusta tu búsqueda</div>
363 - <button class="gear-filter-btn" class:filter-active={searchFilters.entidad} on:mousedown={() => toggleFilter('entidad')}> 372 + <button class="gear-row" on:click={() => toggleFilter('entidad')}>
364 - <span class="filter-dot" style="background:#4A8B6E"></span> 373 + <div class="gear-check" class:gear-check-on={searchFilters.entidad}>
365 - <span>Entidades</span> 374 + {#if searchFilters.entidad}
366 - <span class="filter-check">{searchFilters.entidad ? '✓' : ''}</span> 375 + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
376 + {/if}
377 + </div>
378 + <div class="gear-row-content">
379 + <span class="gear-row-question">¿Quién?</span>
380 + <span class="gear-row-label" class:gear-row-label-on={searchFilters.entidad}>Instituciones</span>
381 + </div>
382 + <span class="gear-row-count">756</span>
367 </button> 383 </button>
368 - <button class="gear-filter-btn" class:filter-active={searchFilters.objeto_gasto} on:mousedown={() => toggleFilter('objeto_gasto')}> 384 + <button class="gear-row" on:click={() => toggleFilter('objeto_gasto')}>
369 - <span class="filter-dot" style="background:#C9A751"></span> 385 + <div class="gear-check" class:gear-check-on={searchFilters.objeto_gasto}>
370 - <span>Objetos de gasto</span> 386 + {#if searchFilters.objeto_gasto}
371 - <span class="filter-check">{searchFilters.objeto_gasto ? '✓' : ''}</span> 387 + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
388 + {/if}
389 + </div>
390 + <div class="gear-row-content">
391 + <span class="gear-row-question">¿En qué?</span>
392 + <span class="gear-row-label" class:gear-row-label-on={searchFilters.objeto_gasto}>Objetos de gasto</span>
393 + </div>
394 + <span class="gear-row-count">324</span>
372 </button> 395 </button>
373 - <button class="gear-filter-btn" class:filter-active={searchFilters.rubro} on:mousedown={() => toggleFilter('rubro')}> 396 + <button class="gear-row" on:click={() => toggleFilter('rubro')}>
374 - <span class="filter-dot" style="background:#8B6E9B"></span> 397 + <div class="gear-check" class:gear-check-on={searchFilters.rubro}>
375 - <span>Rubros</span> 398 + {#if searchFilters.rubro}
376 - <span class="filter-check">{searchFilters.rubro ? '✓' : ''}</span> 399 + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
400 + {/if}
401 + </div>
402 + <div class="gear-row-content">
403 + <span class="gear-row-question">¿De dónde?</span>
404 + <span class="gear-row-label" class:gear-row-label-on={searchFilters.rubro}>Rubros de ingreso</span>
405 + </div>
406 + <span class="gear-row-count">324</span>
377 </button> 407 </button>
378 </div> 408 </div>
379 {/if} 409 {/if}
...@@ -381,7 +411,7 @@ ...@@ -381,7 +411,7 @@
381 </div> 411 </div>
382 412
383 <!-- Search Results Dropdown --> 413 <!-- Search Results Dropdown -->
384 - {#if $isLoading} 414 + {#if $isLoading && $query.length >= 2}
385 <div class="search-results"> 415 <div class="search-results">
386 <div class="search-loading">Buscando...</div> 416 <div class="search-loading">Buscando...</div>
387 </div> 417 </div>
...@@ -438,19 +468,19 @@ ...@@ -438,19 +468,19 @@
438 </button> 468 </button>
439 </div> 469 </div>
440 <div class="mobile-filter-section"> 470 <div class="mobile-filter-section">
441 - <div class="mobile-filter-title">Buscar en:</div> 471 + <div class="mobile-filter-title">Buscar</div>
442 <div class="mobile-filter-bar"> 472 <div class="mobile-filter-bar">
443 <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.entidad} on:click={() => toggleFilter('entidad')}> 473 <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.entidad} on:click={() => toggleFilter('entidad')}>
444 <span class="filter-chip-dot" style="background:#4A8B6E"></span> 474 <span class="filter-chip-dot" style="background:#4A8B6E"></span>
445 - Entidades 475 + Instituciones
446 </button> 476 </button>
447 <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.objeto_gasto} on:click={() => toggleFilter('objeto_gasto')}> 477 <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.objeto_gasto} on:click={() => toggleFilter('objeto_gasto')}>
448 <span class="filter-chip-dot" style="background:#C9A751"></span> 478 <span class="filter-chip-dot" style="background:#C9A751"></span>
449 - Objetos 479 + Gastos
450 </button> 480 </button>
451 <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.rubro} on:click={() => toggleFilter('rubro')}> 481 <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.rubro} on:click={() => toggleFilter('rubro')}>
452 <span class="filter-chip-dot" style="background:#8B6E9B"></span> 482 <span class="filter-chip-dot" style="background:#8B6E9B"></span>
453 - Rubros 483 + Ingresos
454 </button> 484 </button>
455 </div> 485 </div>
456 </div> 486 </div>
...@@ -460,7 +490,11 @@ ...@@ -460,7 +490,11 @@
460 <div class="pills"> 490 <div class="pills">
461 <span class="pills-label">Ej:</span> 491 <span class="pills-label">Ej:</span>
462 {#each examples as ex} 492 {#each examples as ex}
463 - <button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button> 493 + {#if ex.href}
494 + <a class="pill" style="color:{ex.color};background:{ex.bg}" href={ex.href}>{ex.label}</a>
495 + {:else}
496 + <button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button>
497 + {/if}
464 {/each} 498 {/each}
465 </div> 499 </div>
466 </div> 500 </div>
...@@ -778,17 +812,31 @@ ...@@ -778,17 +812,31 @@
778 <div class="vis-grid-simple"> 812 <div class="vis-grid-simple">
779 {#each VISUALIZACIONES as v, i} 813 {#each VISUALIZACIONES as v, i}
780 <div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}> 814 <div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}>
781 - <div class="vis-card" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0"> 815 + {#if v.href}
782 - <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div> 816 + <a href={v.href} class="vis-card vis-card-link" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null}>
783 - <div> 817 + <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
784 - <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span> 818 + <div>
785 - <h4 class="vis-title">{v.title}</h4> 819 + <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
786 - <p class="vis-desc">{v.desc}</p> 820 + <h4 class="vis-title">{v.title}</h4>
787 - </div> 821 + <p class="vis-desc">{v.desc}</p>
788 - <div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}"> 822 + </div>
789 - Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span> 823 + <div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}">
824 + Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span>
825 + </div>
826 + </a>
827 + {:else}
828 + <div class="vis-card vis-card-disabled" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0">
829 + <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
830 + <div>
831 + <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
832 + <h4 class="vis-title">{v.title}</h4>
833 + <p class="vis-desc">{v.desc}</p>
834 + </div>
835 + <div class="vis-cta" style="color:#6B6860">
836 + Próximamente
837 + </div>
790 </div> 838 </div>
791 - </div> 839 + {/if}
792 </div> 840 </div>
793 {/each} 841 {/each}
794 </div> 842 </div>
...@@ -913,24 +961,31 @@ ...@@ -913,24 +961,31 @@
913 .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} 961 .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0}
914 .search-icon{flex-shrink:0;transition:stroke 0.3s} 962 .search-icon{flex-shrink:0;transition:stroke 0.3s}
915 .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} 963 .search-input-wrap{flex:1;position:relative;display:flex;align-items:center}
916 - .search-input-wrap input{width:100%;background:transparent;border:none;outline:none;color:#F5F0E8;font-size:16px;font-family:var(--sans);padding:12px 12px 12px 10px;caret-color:transparent} 964 + .search-input-wrap input{width:100%;background:transparent;border:none;outline:none;color:#F5F0E8;font-size:16px;font-family:var(--sans);padding:12px 12px 12px 10px;caret-color:#C9A751}
917 .search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none} 965 .search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none}
918 - .blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1.1s step-end infinite;border-radius:1px;flex-shrink:0} 966 + .blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1s step-end infinite;border-radius:1px;flex-shrink:0}
967 + .cursor-diario{background:#4A8B6E}
968 + .cursor-archivo{background:#C9A751}
919 .placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD} 969 .placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD}
920 .placeholder-mobile{display:none} 970 .placeholder-mobile{display:none}
921 .gear-btn{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s;flex-shrink:0;margin-left:8px} 971 .gear-btn{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s;flex-shrink:0;margin-left:8px}
972 + .gear-btn svg{transition:transform 0.3s ease}
973 + .gear-btn:hover svg{transform:rotate(45deg)}
922 .gear-btn:hover,.gear-active{background:rgba(255,255,255,0.12)} 974 .gear-btn:hover,.gear-active{background:rgba(255,255,255,0.12)}
923 .gear-wrap{position:relative} 975 .gear-wrap{position:relative}
924 .gear-desktop{display:block} 976 .gear-desktop{display:block}
925 - .gear-menu{position:absolute;top:calc(100% + 8px);right:0;background:rgba(28,28,26,0.98);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.12);border-radius:12px;padding:12px;min-width:180px;z-index:9999;box-shadow:0 8px 32px rgba(0,0,0,0.4)} 977 + .gear-menu{position:absolute;top:calc(100% + 6px);right:0;background:rgba(28,28,26,0.92);backdrop-filter:blur(32px) saturate(1.2);-webkit-backdrop-filter:blur(32px) saturate(1.2);border-radius:16px;min-width:320px;z-index:9999;box-shadow:0 12px 48px rgba(0,0,0,0.5);padding:4px;animation:gearFadeIn 0.15s ease-out}
926 - .gear-menu-title{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.1em;color:#6B6860;text-transform:uppercase;margin-bottom:8px;padding-left:4px} 978 + @keyframes gearFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
927 - .gear-filter-btn{width:100%;display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;background:transparent;border:none;cursor:pointer;transition:all 0.2s;font-family:var(--sans);font-size:14px;color:#B8B5AD;text-align:left} 979 + .gear-menu-title{font-family:var(--sans);font-size:11px;font-weight:400;color:#6B6860;padding:10px 14px 6px;letter-spacing:0.02em}
928 - .gear-filter-btn:hover{background:rgba(255,255,255,0.06)} 980 + .gear-row{display:flex;align-items:center;gap:12px;width:100%;background:transparent;border:none;padding:12px 14px;border-radius:12px;cursor:pointer;transition:background 0.15s;text-align:left}
929 - .gear-filter-btn.filter-active{color:#F5F0E8} 981 + .gear-row:hover{background:rgba(255,255,255,0.04)}
930 - .filter-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;opacity:0.4;transition:opacity 0.2s} 982 + .gear-check{width:18px;height:18px;border-radius:6px;border:1.5px solid rgba(255,255,255,0.15);background:transparent;display:flex;align-items:center;justify-content:center;transition:all 0.2s;flex-shrink:0;color:transparent}
931 - .gear-filter-btn.filter-active .filter-dot{opacity:1} 983 + .gear-check-on{border-color:transparent;background:#C9A751;color:#1C1C1A}
932 - .filter-check{margin-left:auto;font-size:12px;color:#4A8B6E;opacity:0;transition:opacity 0.2s} 984 + .gear-row-content{flex:1;display:flex;align-items:baseline;gap:8px}
933 - .gear-filter-btn.filter-active .filter-check{opacity:1} 985 + .gear-row-question{font-family:var(--sans);font-size:12px;color:#5A5850;font-weight:400}
986 + .gear-row-label{font-family:var(--sans);font-size:14px;color:#8B8880;transition:color 0.15s}
987 + .gear-row-label-on{color:#F5F0E8}
988 + .gear-row-count{font-family:var(--mono);font-size:10px;color:#4A4840}
934 989
935 /* Mobile settings button (hidden on desktop) */ 990 /* Mobile settings button (hidden on desktop) */
936 .mobile-settings-btn{display:none;width:40px;height:40px;border-radius:10px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;align-items:center;justify-content:center;position:relative;transition:all 0.3s;flex-shrink:0} 991 .mobile-settings-btn{display:none;width:40px;height:40px;border-radius:10px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;align-items:center;justify-content:center;position:relative;transition:all 0.3s;flex-shrink:0}
...@@ -976,7 +1031,7 @@ ...@@ -976,7 +1031,7 @@
976 .result-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;font-weight:600} 1031 .result-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;font-weight:600}
977 .pills{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;align-items:center;justify-content:center} 1032 .pills{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;align-items:center;justify-content:center}
978 .pills-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#B8B5AD;margin-right:2px} 1033 .pills-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#B8B5AD;margin-right:2px}
979 - .pill{font-family:var(--mono);font-size:12px;font-weight:300;letter-spacing:0.03em;border:none;border-radius:6px;padding:5px 12px;cursor:pointer;transition:all 0.25s;line-height:1.4;position:relative;overflow:hidden} 1034 + .pill{font-family:var(--mono);font-size:12px;font-weight:300;letter-spacing:0.03em;border:none;border-radius:6px;padding:5px 12px;cursor:pointer;transition:all 0.25s;line-height:1.4;position:relative;overflow:hidden;text-decoration:none}
980 .pill::before{content:'';position:absolute;inset:0;background:rgba(255,255,255,0.12);opacity:0;transition:opacity 0.25s} 1035 .pill::before{content:'';position:absolute;inset:0;background:rgba(255,255,255,0.12);opacity:0;transition:opacity 0.25s}
981 .pill:hover::before{opacity:1} 1036 .pill:hover::before{opacity:1}
982 .pill:hover{transform:translateY(-1px)} 1037 .pill:hover{transform:translateY(-1px)}
...@@ -1131,6 +1186,9 @@ ...@@ -1131,6 +1186,9 @@
1131 .vis-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860} 1186 .vis-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860}
1132 .vis-arrow{transition:transform 0.3s;display:inline-block} 1187 .vis-arrow{transition:transform 0.3s;display:inline-block}
1133 .vis-arrow-moved{transform:translateX(3px)} 1188 .vis-arrow-moved{transform:translateX(3px)}
1189 + .vis-card-link{text-decoration:none;color:inherit}
1190 + .vis-card-disabled{opacity:0.6;cursor:default}
1191 + .vis-card-disabled:hover{transform:none;box-shadow:none}
1134 1192
1135 /* ── MANIFIESTO ── */ 1193 /* ── MANIFIESTO ── */
1136 .manifiesto{background:#1C1C1A;color:#F5F0E8;padding:96px 48px;position:relative;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} 1194 .manifiesto{background:#1C1C1A;color:#F5F0E8;padding:96px 48px;position:relative;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always}
......
This diff could not be displayed because it is too large.
1 +<script>
2 + import { onMount } from 'svelte';
3 + import { marked } from 'marked';
4 +
5 + let content = $state('');
6 + let loading = $state(true);
7 + let headings = $state([]);
8 + let activeId = $state('');
9 + let sidebarOpen = $state(false);
10 + let expandedSections = $state(new Set());
11 +
12 + // Build hierarchical structure from flat headings
13 + function buildHierarchy(flatHeadings) {
14 + const root = { children: [], level: 0 };
15 + const stack = [root];
16 +
17 + for (const heading of flatHeadings) {
18 + const node = { ...heading, children: [] };
19 +
20 + // Pop stack until we find a parent with lower level
21 + while (stack.length > 1 && stack[stack.length - 1].level >= heading.level) {
22 + stack.pop();
23 + }
24 +
25 + stack[stack.length - 1].children.push(node);
26 + stack.push(node);
27 + }
28 +
29 + return root.children;
30 + }
31 +
32 + // Extract headings from markdown and add IDs
33 + function processMarkdown(markdown) {
34 + const headingsList = [];
35 + let headingIndex = 0;
36 +
37 + // Custom renderer for marked v17+
38 + const renderer = {
39 + heading({ tokens, depth }) {
40 + const text = tokens.map(t => t.text || t.raw || '').join('');
41 +
42 + const slug = text
43 + .toLowerCase()
44 + .normalize('NFD')
45 + .replace(/[\u0300-\u036f]/g, '')
46 + .replace(/[^\w\s-]/g, '')
47 + .replace(/\s+/g, '-')
48 + .replace(/-+/g, '-')
49 + .trim();
50 +
51 + const id = `${slug}-${headingIndex++}`;
52 +
53 + // Include up to level 4
54 + if (depth <= 4) {
55 + headingsList.push({
56 + id,
57 + text: text,
58 + level: depth
59 + });
60 + }
61 +
62 + // Add anchor link to each heading
63 + return `<h${depth} id="${id}" class="heading-anchor"><a href="#${id}" class="anchor-link">${text}</a></h${depth}>`;
64 + }
65 + };
66 +
67 + marked.use({ renderer });
68 + const html = marked.parse(markdown);
69 +
70 + return { html, headings: headingsList };
71 + }
72 +
73 + onMount(async () => {
74 + try {
75 + const response = await fetch('/docs.md');
76 + const markdown = await response.text();
77 + const result = processMarkdown(markdown);
78 + content = result.html;
79 + headings = buildHierarchy(result.headings);
80 +
81 + // Expand level 1 by default
82 + const initialExpanded = new Set();
83 + result.headings.filter(h => h.level === 1).forEach(h => initialExpanded.add(h.id));
84 + expandedSections = initialExpanded;
85 +
86 + // Setup intersection observer for scroll tracking
87 + setTimeout(() => {
88 + setupScrollTracking(result.headings);
89 +
90 + // Handle URL hash on page load
91 + if (window.location.hash) {
92 + const id = window.location.hash.slice(1);
93 + const element = document.getElementById(id);
94 + if (element) {
95 + // Expand parents of this heading
96 + const parentIds = findParentIds(id, headings);
97 + if (parentIds && parentIds.length > 0) {
98 + const newExpanded = new Set(expandedSections);
99 + parentIds.forEach(pid => newExpanded.add(pid));
100 + expandedSections = newExpanded;
101 + }
102 + // Scroll to element
103 + setTimeout(() => {
104 + element.scrollIntoView({ behavior: 'smooth', block: 'start' });
105 + activeId = id;
106 + }, 50);
107 + }
108 + }
109 + }, 100);
110 + } catch (error) {
111 + console.error('Error loading documentation:', error);
112 + content = '<p>Error al cargar la documentación.</p>';
113 + } finally {
114 + loading = false;
115 + }
116 + });
117 +
118 + // Find all parent IDs for a given heading ID
119 + function findParentIds(targetId, nodes, parents = []) {
120 + for (const node of nodes) {
121 + if (node.id === targetId) {
122 + return parents;
123 + }
124 + if (node.children && node.children.length > 0) {
125 + const found = findParentIds(targetId, node.children, [...parents, node.id]);
126 + if (found) return found;
127 + }
128 + }
129 + return null;
130 + }
131 +
132 + function setupScrollTracking(flatHeadings) {
133 + const observer = new IntersectionObserver(
134 + (entries) => {
135 + entries.forEach((entry) => {
136 + if (entry.isIntersecting) {
137 + const newActiveId = entry.target.id;
138 + activeId = newActiveId;
139 +
140 + // Auto-expand parents of the active heading
141 + const parentIds = findParentIds(newActiveId, headings);
142 + if (parentIds && parentIds.length > 0) {
143 + const newExpanded = new Set(expandedSections);
144 + parentIds.forEach(id => newExpanded.add(id));
145 + expandedSections = newExpanded;
146 + }
147 + }
148 + });
149 + },
150 + {
151 + rootMargin: '-100px 0px -70% 0px',
152 + threshold: 0
153 + }
154 + );
155 +
156 + flatHeadings.forEach((heading) => {
157 + const element = document.getElementById(heading.id);
158 + if (element) {
159 + observer.observe(element);
160 + }
161 + });
162 +
163 + return () => observer.disconnect();
164 + }
165 +
166 + function scrollToHeading(id) {
167 + const element = document.getElementById(id);
168 + if (element) {
169 + // Update URL hash without triggering scroll
170 + history.pushState(null, '', `#${id}`);
171 + element.scrollIntoView({ behavior: 'smooth', block: 'start' });
172 + sidebarOpen = false;
173 + }
174 + }
175 +
176 + function toggleSection(id) {
177 + const newExpanded = new Set(expandedSections);
178 + if (newExpanded.has(id)) {
179 + newExpanded.delete(id);
180 + } else {
181 + newExpanded.add(id);
182 + }
183 + expandedSections = newExpanded;
184 + }
185 +
186 + function toggleSidebar() {
187 + sidebarOpen = !sidebarOpen;
188 + }
189 +
190 + // Check if a heading or any of its children is active
191 + function isActiveOrHasActiveChild(node, currentActiveId) {
192 + if (node.id === currentActiveId) return true;
193 + if (node.children) {
194 + return node.children.some(child => isActiveOrHasActiveChild(child, currentActiveId));
195 + }
196 + return false;
197 + }
198 +</script>
199 +
200 +<svelte:head>
201 + <title>Documentación | Presupuesto Público Bolivia</title>
202 +</svelte:head>
203 +
204 +<div class="docs-page">
205 + <header class="docs-header">
206 + <div class="docs-header-content">
207 + <div class="docs-header-top">
208 + <a href="/" class="docs-back">
209 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
210 + <path d="M19 12H5M12 19l-7-7 7-7"/>
211 + </svg>
212 + <span>Inicio</span>
213 + </a>
214 +
215 + <!-- Mobile menu button -->
216 + <button class="docs-menu-btn" onclick={toggleSidebar}>
217 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
218 + <path d="M3 12h18M3 6h18M3 18h18"/>
219 + </svg>
220 + <span>Índice</span>
221 + </button>
222 + </div>
223 + <h1 class="docs-title">Documentación</h1>
224 + <p class="docs-subtitle">Estructura de las bases de datos del presupuesto público</p>
225 + </div>
226 + </header>
227 +
228 + <div class="docs-layout-wrapper">
229 + <!-- Fixed Sidebar / Table of Contents -->
230 + <aside class="docs-sidebar" class:open={sidebarOpen}>
231 + <div class="docs-sidebar-content">
232 + <div class="docs-sidebar-header">
233 + <span class="docs-sidebar-title">Índice</span>
234 + <button class="docs-sidebar-close" onclick={() => sidebarOpen = false}>
235 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
236 + <path d="M18 6L6 18M6 6l12 12"/>
237 + </svg>
238 + </button>
239 + </div>
240 +
241 + <nav class="docs-toc">
242 + {#snippet renderHeading(node, depth = 0)}
243 + <div class="toc-node">
244 + <div class="toc-item-row">
245 + {#if node.children && node.children.length > 0}
246 + <button
247 + class="toc-toggle"
248 + class:expanded={expandedSections.has(node.id)}
249 + onclick={() => toggleSection(node.id)}
250 + >
251 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
252 + <path d="M9 18l6-6-6-6"/>
253 + </svg>
254 + </button>
255 + {:else}
256 + <span class="toc-toggle-spacer"></span>
257 + {/if}
258 + <button
259 + class="docs-toc-item level-{node.level}"
260 + class:active={activeId === node.id}
261 + onclick={() => scrollToHeading(node.id)}
262 + >
263 + {node.text}
264 + </button>
265 + </div>
266 + {#if node.children && node.children.length > 0 && expandedSections.has(node.id)}
267 + <div class="toc-children">
268 + {#each node.children as child}
269 + {@render renderHeading(child, depth + 1)}
270 + {/each}
271 + </div>
272 + {/if}
273 + </div>
274 + {/snippet}
275 +
276 + {#each headings as heading}
277 + {@render renderHeading(heading)}
278 + {/each}
279 + </nav>
280 + </div>
281 + </aside>
282 +
283 + <!-- Main content -->
284 + <main class="docs-main">
285 + {#if loading}
286 + <div class="docs-loading">
287 + <svg class="docs-loader" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
288 + <rect x="1" y="1" rx="1" width="10" height="10">
289 + <animate id="sq1" attributeName="fill-opacity" values="1;0.4;0.4;0.4;1" dur="1.6s" repeatCount="indefinite" begin="0s"/>
290 + </rect>
291 + <rect x="13" y="1" rx="1" width="10" height="10">
292 + <animate id="sq2" attributeName="fill-opacity" values="0.4;1;0.4;0.4;0.4" dur="1.6s" repeatCount="indefinite" begin="0s"/>
293 + </rect>
294 + <rect x="1" y="13" rx="1" width="10" height="10">
295 + <animate id="sq3" attributeName="fill-opacity" values="0.4;0.4;0.4;1;0.4" dur="1.6s" repeatCount="indefinite" begin="0s"/>
296 + </rect>
297 + <rect x="13" y="13" rx="1" width="10" height="10">
298 + <animate id="sq4" attributeName="fill-opacity" values="0.4;0.4;1;0.4;0.4" dur="1.6s" repeatCount="indefinite" begin="0s"/>
299 + </rect>
300 + </svg>
301 + <p>Cargando documentación...</p>
302 + </div>
303 + {:else}
304 + <article class="docs-content">
305 + {@html content}
306 + </article>
307 + {/if}
308 + </main>
309 + </div>
310 +
311 + <!-- Overlay for mobile -->
312 + {#if sidebarOpen}
313 + <button class="docs-overlay" onclick={() => sidebarOpen = false}></button>
314 + {/if}
315 +</div>
316 +
317 +<style>
318 + .docs-page {
319 + min-height: 100vh;
320 + background-color: var(--theme-body);
321 + font-family: var(--font-sans);
322 + }
323 +
324 + .docs-header {
325 + border-bottom: 1px solid var(--theme-borde);
326 + background-color: var(--theme-body);
327 + position: fixed;
328 + top: 0;
329 + left: 0;
330 + right: 0;
331 + z-index: 20;
332 + }
333 +
334 + .docs-header-content {
335 + max-width: 1000px;
336 + margin: 0 auto;
337 + padding: 1rem 1.5rem;
338 + }
339 +
340 + .docs-header-top {
341 + display: flex;
342 + justify-content: space-between;
343 + align-items: center;
344 + margin-bottom: 0.5rem;
345 + }
346 +
347 + .docs-back {
348 + display: inline-flex;
349 + align-items: center;
350 + gap: 0.5rem;
351 + color: var(--theme-texto);
352 + text-decoration: none;
353 + font-size: 0.875rem;
354 + transition: color 0.15s ease;
355 + }
356 +
357 + .docs-back:hover {
358 + color: var(--theme-accent);
359 + }
360 +
361 + .docs-menu-btn {
362 + display: flex;
363 + align-items: center;
364 + gap: 0.5rem;
365 + padding: 0.5rem 0.75rem;
366 + background: var(--theme-fill);
367 + border: 1px solid var(--theme-borde);
368 + border-radius: 8px;
369 + color: var(--theme-texto);
370 + font-size: 0.875rem;
371 + cursor: pointer;
372 + transition: all 0.15s ease;
373 + }
374 +
375 + .docs-menu-btn:hover {
376 + color: var(--theme-titulo);
377 + border-color: var(--theme-accent);
378 + }
379 +
380 + @media (min-width: 1100px) {
381 + .docs-menu-btn {
382 + display: none;
383 + }
384 + }
385 +
386 + .docs-title {
387 + font-family: var(--font-sans);
388 + font-size: 1.5rem;
389 + font-weight: 700;
390 + color: var(--theme-titulo);
391 + margin: 0 0 0.25rem;
392 + }
393 +
394 + .docs-subtitle {
395 + color: var(--theme-texto);
396 + font-size: 0.875rem;
397 + margin: 0;
398 + }
399 +
400 + /* Centered layout container */
401 + .docs-layout-wrapper {
402 + max-width: 700px;
403 + margin: 0 auto;
404 + padding-top: 140px;
405 + padding-left: 1.5rem;
406 + padding-right: 1.5rem;
407 + }
408 +
409 + /* Sidebar */
410 + .docs-sidebar {
411 + display: none;
412 + }
413 +
414 + /* Desktop: sidebar (200) + gap (50) + content (600) = 850px total centered */
415 + @media (min-width: 1100px) {
416 + .docs-layout-wrapper {
417 + max-width: 600px;
418 + margin-left: calc(50% - 425px + 250px);
419 + margin-right: auto;
420 + padding-left: 2rem;
421 + padding-right: 2rem;
422 + }
423 +
424 + .docs-sidebar {
425 + display: block;
426 + position: fixed;
427 + top: 120px;
428 + left: calc(50% - 425px);
429 + width: 200px;
430 + max-height: calc(100vh - 140px);
431 + overflow-y: auto;
432 + overflow-x: hidden;
433 + padding: 0;
434 + z-index: 10;
435 + }
436 +
437 + .docs-sidebar-content {
438 + padding-right: 1rem;
439 + padding-top: 2rem;
440 + }
441 + }
442 +
443 + /* Larger screens: more space */
444 + @media (min-width: 1300px) {
445 + .docs-layout-wrapper {
446 + max-width: 650px;
447 + margin-left: calc(50% - 475px + 280px);
448 + }
449 +
450 + .docs-sidebar {
451 + left: calc(50% - 475px);
452 + width: 230px;
453 + }
454 + }
455 +
456 + /* Mobile sidebar */
457 + @media (max-width: 1099px) {
458 + .docs-sidebar {
459 + display: block;
460 + position: fixed;
461 + top: 0;
462 + left: 0;
463 + width: 320px;
464 + height: 100vh;
465 + background: var(--theme-body);
466 + z-index: 100;
467 + transform: translateX(-100%);
468 + transition: transform 0.25s ease;
469 + box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
470 + }
471 +
472 + .docs-sidebar.open {
473 + transform: translateX(0);
474 + }
475 +
476 + .docs-sidebar-content {
477 + height: 100%;
478 + overflow-y: auto;
479 + padding: 1rem;
480 + }
481 + }
482 +
483 + .docs-sidebar-header {
484 + display: flex;
485 + justify-content: space-between;
486 + align-items: center;
487 + margin-bottom: 1rem;
488 + padding-bottom: 0.75rem;
489 + border-bottom: 1px solid var(--theme-borde);
490 + }
491 +
492 + @media (min-width: 1100px) {
493 + .docs-sidebar-header {
494 + border-bottom: none;
495 + padding-bottom: 0;
496 + margin-bottom: 1.25rem;
497 + }
498 +
499 + .docs-sidebar-close {
500 + display: none;
501 + }
502 + }
503 +
504 + .docs-sidebar-title {
505 + font-size: 0.8125rem;
506 + font-weight: 600;
507 + text-transform: uppercase;
508 + letter-spacing: 0.08em;
509 + color: var(--theme-texto);
510 + }
511 +
512 + .docs-sidebar-close {
513 + padding: 0.25rem;
514 + color: var(--theme-texto);
515 + background: none;
516 + border: none;
517 + cursor: pointer;
518 + border-radius: 4px;
519 + transition: color 0.15s ease;
520 + }
521 +
522 + .docs-sidebar-close:hover {
523 + color: var(--theme-titulo);
524 + }
525 +
526 + /* Table of Contents */
527 + .docs-toc {
528 + display: flex;
529 + flex-direction: column;
530 + gap: 1px;
531 + }
532 +
533 + .toc-node {
534 + display: flex;
535 + flex-direction: column;
536 + }
537 +
538 + .toc-item-row {
539 + display: flex;
540 + align-items: flex-start;
541 + gap: 2px;
542 + }
543 +
544 + .toc-toggle {
545 + flex-shrink: 0;
546 + width: 20px;
547 + height: 24px;
548 + display: flex;
549 + align-items: center;
550 + justify-content: center;
551 + background: none;
552 + border: none;
553 + color: var(--theme-texto);
554 + cursor: pointer;
555 + opacity: 0.5;
556 + transition: all 0.15s ease;
557 + padding: 0;
558 + }
559 +
560 + .toc-toggle:hover {
561 + opacity: 1;
562 + color: var(--theme-accent);
563 + }
564 +
565 + .toc-toggle svg {
566 + transition: transform 0.15s ease;
567 + }
568 +
569 + .toc-toggle.expanded svg {
570 + transform: rotate(90deg);
571 + }
572 +
573 + .toc-toggle-spacer {
574 + width: 20px;
575 + flex-shrink: 0;
576 + }
577 +
578 + .toc-children {
579 + padding-left: 10px;
580 + border-left: 1px solid color-mix(in srgb, var(--theme-borde) 50%, transparent);
581 + margin-left: 9px;
582 + }
583 +
584 + .docs-toc-item {
585 + flex: 1;
586 + display: block;
587 + text-align: left;
588 + padding: 0.35rem 0.5rem;
589 + font-size: 0.8125rem;
590 + color: var(--theme-texto);
591 + background: none;
592 + border: none;
593 + border-radius: 4px;
594 + cursor: pointer;
595 + transition: all 0.15s ease;
596 + line-height: 1.3;
597 + }
598 +
599 + .docs-toc-item:hover {
600 + color: var(--theme-titulo);
601 + background: var(--theme-fill);
602 + }
603 +
604 + .docs-toc-item.active {
605 + color: var(--theme-accent);
606 + background: color-mix(in srgb, var(--theme-accent) 12%, transparent);
607 + font-weight: 500;
608 + }
609 +
610 + .docs-toc-item.level-1 {
611 + font-weight: 600;
612 + font-size: 0.875rem;
613 + color: var(--theme-titulo);
614 + }
615 +
616 + .docs-toc-item.level-2 {
617 + font-weight: 500;
618 + }
619 +
620 + .docs-toc-item.level-3 {
621 + font-size: 0.8125rem;
622 + }
623 +
624 + .docs-toc-item.level-4 {
625 + font-size: 0.75rem;
626 + font-family: monospace;
627 + color: color-mix(in srgb, var(--theme-texto) 80%, transparent);
628 + }
629 +
630 + .docs-toc-item.level-4.active {
631 + color: var(--theme-accent);
632 + }
633 +
634 + /* Overlay */
635 + .docs-overlay {
636 + display: none;
637 + }
638 +
639 + @media (max-width: 1099px) {
640 + .docs-overlay {
641 + display: block;
642 + position: fixed;
643 + inset: 0;
644 + background: rgba(0, 0, 0, 0.4);
645 + z-index: 50;
646 + border: none;
647 + cursor: pointer;
648 + }
649 + }
650 +
651 + /* Main content */
652 + .docs-main {
653 + flex: 1;
654 + min-width: 0;
655 + padding: 0 0 4rem 0;
656 + }
657 +
658 + @media (max-width: 1099px) {
659 + .docs-layout-wrapper {
660 + padding-top: 130px;
661 + padding-left: 1rem;
662 + padding-right: 1rem;
663 + }
664 +
665 + .docs-main {
666 + padding: 0 0 4rem 0;
667 + }
668 + }
669 +
670 + .docs-loading {
671 + display: flex;
672 + flex-direction: column;
673 + align-items: center;
674 + justify-content: center;
675 + padding: 4rem 2rem;
676 + gap: 1rem;
677 + color: var(--theme-texto);
678 + }
679 +
680 + .docs-loader {
681 + width: 32px;
682 + height: 32px;
683 + fill: var(--theme-texto);
684 + opacity: 0.6;
685 + }
686 +
687 + /* Markdown content styles */
688 + .docs-content {
689 + color: var(--theme-titulo);
690 + line-height: 1.7;
691 + }
692 +
693 + .docs-content :global(h1) {
694 + font-family: var(--font-sans);
695 + font-size: 1.75rem;
696 + font-weight: 700;
697 + color: var(--theme-titulo);
698 + margin: 3rem 0 1.5rem;
699 + scroll-margin-top: 130px;
700 + }
701 +
702 + .docs-content :global(h1:first-child) {
703 + margin-top: 0;
704 + }
705 +
706 + .docs-content :global(h2) {
707 + font-family: var(--font-sans);
708 + font-size: 1.5rem;
709 + font-weight: 700;
710 + color: var(--theme-titulo);
711 + margin: 2.5rem 0 1rem;
712 + scroll-margin-top: 130px;
713 + }
714 +
715 + .docs-content :global(h3) {
716 + font-size: 1.25rem;
717 + font-weight: 600;
718 + color: var(--theme-titulo);
719 + margin: 2rem 0 0.75rem;
720 + scroll-margin-top: 130px;
721 + }
722 +
723 + .docs-content :global(h4) {
724 + font-size: 1rem;
725 + font-weight: 600;
726 + color: var(--theme-accent);
727 + margin: 1.5rem 0 0.5rem;
728 + font-family: monospace;
729 + scroll-margin-top: 130px;
730 + }
731 +
732 + .docs-content :global(p) {
733 + margin: 0 0 1rem;
734 + color: var(--theme-texto);
735 + }
736 +
737 + .docs-content :global(ul),
738 + .docs-content :global(ol) {
739 + margin: 0 0 1rem;
740 + padding-left: 1.5rem;
741 + color: var(--theme-texto);
742 + }
743 +
744 + .docs-content :global(li) {
745 + margin-bottom: 0.5rem;
746 + }
747 +
748 + .docs-content :global(strong) {
749 + font-weight: 600;
750 + color: var(--theme-titulo);
751 + }
752 +
753 + .docs-content :global(code) {
754 + font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
755 + font-size: 0.875em;
756 + background-color: var(--theme-fill);
757 + color: var(--theme-accent);
758 + padding: 0.2em 0.4em;
759 + border-radius: 4px;
760 + border: 1px solid var(--theme-borde);
761 + }
762 +
763 + .docs-content :global(pre) {
764 + background-color: var(--theme-fill);
765 + border: 1px solid var(--theme-borde);
766 + border-radius: 8px;
767 + padding: 1rem;
768 + margin: 1rem 0 1.5rem;
769 + overflow-x: auto;
770 + }
771 +
772 + .docs-content :global(pre code) {
773 + background: none;
774 + border: none;
775 + padding: 0;
776 + font-size: 0.875rem;
777 + color: var(--theme-titulo);
778 + }
779 +
780 + .docs-content :global(hr) {
781 + border: none;
782 + height: 1px;
783 + background: color-mix(in srgb, var(--theme-borde) 25%, transparent);
784 + margin: 2rem 0;
785 + opacity: 0.5;
786 + }
787 +
788 + .docs-content :global(blockquote) {
789 + border-left: 3px solid var(--theme-accent);
790 + margin: 1rem 0;
791 + padding: 0.5rem 1rem;
792 + background-color: var(--theme-fill);
793 + border-radius: 0 8px 8px 0;
794 + }
795 +
796 + .docs-content :global(blockquote p) {
797 + margin: 0;
798 + }
799 +
800 + /* Table of contents style for nested lists */
801 + .docs-content :global(ul ul) {
802 + margin-top: 0.5rem;
803 + margin-bottom: 0.5rem;
804 + }
805 +
806 + /* Links */
807 + .docs-content :global(a) {
808 + color: var(--theme-accent);
809 + text-decoration: underline;
810 + text-decoration-color: transparent;
811 + transition: text-decoration-color 0.15s ease;
812 + }
813 +
814 + .docs-content :global(a:hover) {
815 + text-decoration-color: var(--theme-accent);
816 + }
817 +
818 + /* Heading anchor links */
819 + .docs-content :global(.heading-anchor) {
820 + position: relative;
821 + }
822 +
823 + .docs-content :global(.anchor-link) {
824 + color: inherit;
825 + text-decoration: none;
826 + }
827 +
828 + .docs-content :global(.anchor-link:hover) {
829 + text-decoration: none;
830 + }
831 +
832 + .docs-content :global(.heading-anchor:hover .anchor-link::before) {
833 + content: '#';
834 + position: absolute;
835 + left: -1.25em;
836 + color: var(--theme-accent);
837 + opacity: 0.6;
838 + font-weight: 400;
839 + }
840 +
841 + .docs-content :global(h4.heading-anchor:hover .anchor-link::before) {
842 + left: -1em;
843 + font-size: 0.875em;
844 + }
845 +
846 + /* Responsive */
847 + @media (max-width: 640px) {
848 + .docs-header-content {
849 + padding: 0.75rem 1rem;
850 + }
851 +
852 + .docs-title {
853 + font-size: 1.25rem;
854 + }
855 +
856 + .docs-content :global(h1) {
857 + font-size: 1.5rem;
858 + scroll-margin-top: 120px;
859 + }
860 +
861 + .docs-content :global(h2) {
862 + font-size: 1.25rem;
863 + scroll-margin-top: 120px;
864 + }
865 +
866 + .docs-content :global(h3) {
867 + font-size: 1.125rem;
868 + scroll-margin-top: 120px;
869 + }
870 +
871 + .docs-content :global(h4) {
872 + scroll-margin-top: 120px;
873 + }
874 +
875 + .docs-content :global(pre) {
876 + font-size: 0.8rem;
877 + padding: 0.75rem;
878 + }
879 + }
880 +</style>
1 <script> 1 <script>
2 import { onMount } from 'svelte'; 2 import { onMount } from 'svelte';
3 - import { goto } from '$app/navigation'; 3 + import * as d3 from 'd3';
4 - import { 4 +
5 - query, 5 + // Estado
6 - results, 6 + let streamData = $state([]);
7 - isLoading, 7 + let loading = $state(true);
8 - indexLoaded, 8 + let container = $state(null);
9 - error, 9 + let width = $state(0);
10 - selectedIndex, 10 + let height = $state(500);
11 - showResults, 11 + let tooltip = $state({ show: false, x: 0, y: 0, data: null });
12 - initIndex, 12 +
13 - performSearch, 13 + // Drilldown
14 - clearSearch, 14 + let currentLevel = $state('tipo'); // tipo -> clase -> cuenta -> subcuenta
15 - navigateResults 15 + let breadcrumb = $state([{ level: 'tipo', codigo: null, descripcion: 'Todos los ingresos' }]);
16 - } from '$lib/stores/searchStore'; 16 + let currentParent = $state(null);
17 - 17 +
18 - let mounted = false; 18 + // Colores para las categorías
19 - let mode = 'archivo'; 19 + const colorScale = d3.scaleOrdinal()
20 - let searchFocused = false; 20 + .range([
21 - let searchVal = ''; 21 + '#4A8B6E', '#C9A751', '#8B6E9B', '#6B9FD4', '#D4846B',
22 - let hDiario = false; 22 + '#5B8FA8', '#A8845B', '#7B9B6B', '#9B7B8B', '#6B8B9B',
23 - let hArchivo = false; 23 + '#B89B6B', '#6B7B9B', '#9B8B6B', '#7B8B7B', '#8B7B6B',
24 - let hVis = null; 24 + '#6B9B8B', '#9B6B7B', '#7B6B9B', '#8B9B6B', '#6B8B7B'
25 - let hHist = null; 25 + ]);
26 - let hWidget = null; 26 +
27 - let hGear = false; 27 + // Cargar datos
28 - let mobileSettingsOpen = false; 28 + async function loadData() {
29 - let searchInput; 29 + loading = true;
30 - let debounceTimer; 30 + try {
31 + const response = await fetch('/stream1.csv');
32 + const text = await response.text();
33 + const parsed = d3.csvParse(text, d => ({
34 + entidad: +d.entidad,
35 + año: +d.año,
36 + nivel: d.nivel,
37 + codigo: +d.codigo,
38 + descripcion: d.descripcion,
39 + codigo_padre: d.codigo_padre ? +d.codigo_padre : null,
40 + desc_padre: d.desc_padre || null,
41 + devengado: +d.devengado
42 + }));
43 +
44 + // Filtrar solo entidad 0 (Todo el Estado)
45 + streamData = parsed.filter(d => d.entidad === 0);
46 + console.log('Stream data loaded:', streamData.length, 'rows');
47 + } catch (err) {
48 + console.error('Error loading data:', err);
49 + }
50 + loading = false;
51 + }
31 52
32 - onMount(() => { 53 + // Obtener datos filtrados para el nivel actual
33 - setTimeout(() => { mounted = true; }, 80); 54 + function getFilteredData() {
34 - initIndex(); 55 + let filtered = streamData.filter(d => d.nivel === currentLevel);
35 - });
36 56
37 - function toggleMobileSettings() { 57 + if (currentParent !== null) {
38 - mobileSettingsOpen = !mobileSettingsOpen; 58 + filtered = filtered.filter(d => d.codigo_padre === currentParent);
39 - } 59 + }
40 60
41 - $: isD = mode === 'diario'; 61 + return filtered;
42 - $: accent = isD ? '#4A8B6E' : '#C9A751';
43 - $: placeholder = isD
44 - ? 'Busca una entidad, un ministerio, un municipio...'
45 - : 'Busca en 7 clasificadores: partidas, rubros, fuentes...';
46 - $: examples = isD
47 - ? [
48 - { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
49 - { label: 'Min. Educación', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' },
50 - { label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
51 - ]
52 - : [
53 - { label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962' },
54 - { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
55 - { label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' },
56 - { label: 'Consultorías', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
57 - { label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0' },
58 - ];
59 -
60 - function switchMode(m) {
61 - mode = m;
62 - searchVal = '';
63 - clearSearch();
64 - if (searchInput) searchInput.value = '';
65 } 62 }
66 63
67 - function handleBlur() { 64 + // Preparar datos para el streamgraph
68 - setTimeout(() => { searchFocused = false; }, 200); 65 + function prepareStreamData() {
69 - } 66 + const filtered = getFilteredData();
67 + if (!filtered.length) return { years: [], series: [], keys: [] };
68 +
69 + // Agrupar por año
70 + const byYear = d3.group(filtered, d => d.año);
71 + const years = Array.from(byYear.keys()).sort((a, b) => a - b);
72 +
73 + // Obtener todas las categorías únicas
74 + const categories = [...new Set(filtered.map(d => d.codigo))];
75 +
76 + // Crear estructura para stack
77 + const stackData = years.map(year => {
78 + const yearData = byYear.get(year) || [];
79 + const row = { año: year };
80 + categories.forEach(cat => {
81 + const item = yearData.find(d => d.codigo === cat);
82 + row[cat] = item ? item.devengado : 0;
83 + });
84 + return row;
85 + });
70 86
71 - function handleInput(e) { 87 + // Crear el stack
72 - const value = e.target.value; 88 + const stack = d3.stack()
73 - searchVal = value; 89 + .keys(categories)
74 - clearTimeout(debounceTimer); 90 + .offset(d3.stackOffsetWiggle)
75 - debounceTimer = setTimeout(() => { 91 + .order(d3.stackOrderInsideOut);
76 - performSearch(value);
77 - }, 250);
78 - }
79 92
80 - function handleKeydown(e) { 93 + const series = stack(stackData);
81 - if (!$showResults) return; 94 +
82 - switch (e.key) { 95 + // Agregar descripciones a las series
83 - case 'ArrowDown': 96 + series.forEach(s => {
84 - e.preventDefault(); 97 + const item = filtered.find(d => d.codigo === +s.key);
85 - navigateResults('down', $results.length); 98 + s.descripcion = item ? item.descripcion : s.key;
86 - break; 99 + });
87 - case 'ArrowUp': 100 +
88 - e.preventDefault(); 101 + return { years, series, keys: categories, stackData };
89 - navigateResults('up', $results.length);
90 - break;
91 - case 'Enter':
92 - e.preventDefault();
93 - if ($selectedIndex >= 0 && $results[$selectedIndex]) {
94 - handleSelect($results[$selectedIndex]);
95 - }
96 - break;
97 - case 'Escape':
98 - clearSearch();
99 - searchVal = '';
100 - if (searchInput) searchInput.value = '';
101 - searchInput?.blur();
102 - break;
103 - }
104 } 102 }
105 103
106 - function handleSelect(item) { 104 + // Formatear moneda
107 - if (item.tipo === 'entidad') { 105 + function formatMoney(value) {
108 - goto(`/entidad/${item.codigo}`); 106 + if (value >= 1e9) return `Bs ${(value / 1e9).toFixed(1)}B`;
109 - } else if (item.tipo === 'objeto_gasto') { 107 + if (value >= 1e6) return `Bs ${(value / 1e6).toFixed(1)}M`;
110 - goto(`/objeto/${item.codigo}`); 108 + if (value >= 1e3) return `Bs ${(value / 1e3).toFixed(0)}K`;
111 - } else if (item.tipo === 'rubro') { 109 + return `Bs ${value.toFixed(0)}`;
112 - goto(`/rubro/${item.codigo}`);
113 - }
114 - clearSearch();
115 - searchVal = '';
116 - if (searchInput) searchInput.value = '';
117 } 110 }
118 111
119 - function getTypeLabel(tipo) { 112 + // Manejar clic para drilldown
120 - switch (tipo) { 113 + function handleClick(codigo, descripcion) {
121 - case 'entidad': return 'ENTIDAD'; 114 + const levels = ['tipo', 'clase', 'cuenta', 'subcuenta'];
122 - case 'objeto_gasto': return 'OBJETO'; 115 + const currentIndex = levels.indexOf(currentLevel);
123 - case 'rubro': return 'RUBRO'; 116 +
124 - default: return tipo?.toUpperCase() || ''; 117 + if (currentIndex < levels.length - 1) {
118 + const nextLevel = levels[currentIndex + 1];
119 +
120 + // Verificar si hay datos en el siguiente nivel
121 + const hasChildren = streamData.some(
122 + d => d.nivel === nextLevel && d.codigo_padre === codigo
123 + );
124 +
125 + if (hasChildren) {
126 + currentParent = codigo;
127 + currentLevel = nextLevel;
128 + breadcrumb = [...breadcrumb, { level: nextLevel, codigo, descripcion }];
129 + }
125 } 130 }
126 } 131 }
127 132
128 - function getTypeColor(tipo) { 133 + // Navegar con breadcrumb
129 - switch (tipo) { 134 + function navigateTo(index) {
130 - case 'entidad': return '#4A8B6E'; 135 + if (index === 0) {
131 - case 'objeto_gasto': return '#C9A751'; 136 + currentLevel = 'tipo';
132 - case 'rubro': return '#8B6E9B'; 137 + currentParent = null;
133 - default: return '#6B6860'; 138 + breadcrumb = [{ level: 'tipo', codigo: null, descripcion: 'Todos los ingresos' }];
139 + } else {
140 + const target = breadcrumb[index];
141 + currentLevel = target.level;
142 + currentParent = index > 0 ? breadcrumb[index - 1].codigo : null;
143 +
144 + // Si navegamos a un nivel intermedio, necesitamos el padre correcto
145 + if (index > 0) {
146 + currentParent = breadcrumb[index].codigo;
147 + // Buscar el nivel siguiente
148 + const levels = ['tipo', 'clase', 'cuenta', 'subcuenta'];
149 + const nextLevelIndex = levels.indexOf(target.level) + 1;
150 + if (nextLevelIndex < levels.length) {
151 + currentLevel = levels[nextLevelIndex];
152 + }
153 + }
154 +
155 + breadcrumb = breadcrumb.slice(0, index + 1);
134 } 156 }
135 } 157 }
136 158
137 - const WIDGETS_INGRESOS = [ 159 + // Efecto para observar tamaño
138 - { id: 'rubros', label: 'tipos de ingreso', d: 'Impuestos, regalías, ventas, créditos', t: 'Recursos por rubros', n: '324', href: '/clasificadores/rubros' }, 160 + $effect(() => {
139 - { id: 'fuentes', label: 'fuentes de financiamiento', d: 'TGN, crédito externo, donaciones', t: 'Fuentes', n: '22', href: '/clasificadores/fuente' }, 161 + if (!container) return;
140 - { id: 'organismos', label: 'organismos financiadores', d: 'BID, Banco Mundial, cooperación', t: 'Organismos', n: '228', href: '/clasificadores/organismos' }, 162 +
141 - ]; 163 + const observer = new ResizeObserver(entries => {
142 - 164 + for (const entry of entries) {
143 - const WIDGETS_GASTOS_QUE = [ 165 + width = entry.contentRect.width;
144 - { id: 'objeto', label: 'objetos de gasto', d: 'Sueldos, consultorías, materiales', t: 'Objeto del gasto', n: '324', href: '/clasificadores/objeto-gasto' },
145 - { id: 'sector', label: 'sectores económicos', d: 'Industria, construcción, servicios', t: 'Sectores', n: '488', href: '/clasificadores/sectores' },
146 - ];
147 -
148 - const WIDGETS_GASTOS_BOTTOM = [
149 - { id: 'finalidad', label: 'finalidades y funciones', d: 'Educación, salud, defensa', t: 'Finalidad', n: '132', href: '/clasificadores/funcional' },
150 - { id: 'donde', label: 'ubicaciones geográficas', d: 'Departamentos, municipios', t: 'Geográfico', n: '352', href: '/clasificadores/geografico' },
151 - ];
152 -
153 - const HISTORIAS = [
154 - { color: '#C9A751', title: '¿Cuánto gasta Bolivia en consultorías cada año?', desc: 'Un análisis de 20 años revela patrones que todo ciudadano debería conocer. La evolución del gasto en servicios profesionales muestra tendencias inesperadas.', tag: 'INVESTIGACIÓN', tall: true },
155 - { color: '#C44B3F', title: 'Salud vs. Defensa: 20 años', desc: 'Cómo se redistribuyó el presupuesto entre sectores.', tag: 'COMPARATIVA', tall: false },
156 - { color: '#4A8B6E', title: 'El boom del gasto en infraestructura', desc: 'De 2006 a 2014 Bolivia multiplicó por 8 su inversión pública. Un recorrido por las grandes obras y su impacto fiscal.', tag: 'CRÓNICA', tall: true },
157 - { color: '#6B8CA6', title: '¿Quién financia a Bolivia?', desc: 'Un mapa de los organismos internacionales que prestan al país.', tag: 'ANÁLISIS', tall: false },
158 - ];
159 -
160 - const VISUALIZACIONES = [
161 - { color: '#4A8B6E', title: 'Evolución del gasto en salud', desc: 'Cómo cambiaron las prioridades en salud a lo largo de dos décadas.', type: 'Serie temporal' },
162 - { color: '#C9A751', title: 'Treemap del presupuesto 2025', desc: 'Explora cada peso del presupuesto actual.', type: 'Treemap' },
163 - { color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa' },
164 - ];
165 -
166 - /* Reveal action */
167 - let revealStates = {};
168 - function reveal(node, { id, delay = 0 }) {
169 - revealStates[id] = false;
170 - revealStates = revealStates;
171 - const obs = new IntersectionObserver(([e]) => {
172 - if (e.isIntersecting) {
173 - setTimeout(() => { revealStates[id] = true; revealStates = revealStates; }, delay);
174 - obs.disconnect();
175 } 166 }
176 - }, { threshold: 0.08 }); 167 + });
177 - obs.observe(node);
178 - return { destroy() { obs.disconnect(); } };
179 - }
180 168
181 - /* Section navigation */ 169 + observer.observe(container);
182 - const SECTIONS_NAV = [ 170 + return () => observer.disconnect();
183 - { id: 'hero', label: 'Inicio' }, 171 + });
184 - { id: 'datos', label: 'Datos' },
185 - { id: 'clasificadores', label: 'Clasificadores' },
186 - { id: 'historias', label: 'Historias' },
187 - { id: 'visualizaciones', label: 'Visualizaciones' },
188 - { id: 'manifiesto', label: 'Manifiesto' },
189 - { id: 'descargas', label: 'Descargas' },
190 - ];
191 - let currentSection = 'hero';
192 172
193 onMount(() => { 173 onMount(() => {
194 - const sections = document.querySelectorAll('[data-section]'); 174 + loadData();
195 - const observer = new IntersectionObserver((entries) => {
196 - entries.forEach(entry => {
197 - if (entry.isIntersecting) {
198 - currentSection = entry.target.dataset.section;
199 - }
200 - });
201 - }, { threshold: 0.5 });
202 - sections.forEach(section => observer.observe(section));
203 - return () => observer.disconnect();
204 }); 175 });
205 176
206 - function scrollToSection(id) { 177 + // Datos preparados reactivos
207 - const el = document.querySelector(`[data-section="${id}"]`); 178 + let prepared = $derived(prepareStreamData());
208 - if (el) el.scrollIntoView({ behavior: 'smooth' });
209 - }
210 -</script>
211 179
212 -<div class="page" class:mounted> 180 + // Escalas reactivas
181 + let xScale = $derived(
182 + d3.scaleLinear()
183 + .domain(d3.extent(prepared.years))
184 + .range([60, width - 20])
185 + );
213 186
214 - <!-- Section Navigation Dots --> 187 + let yExtent = $derived(() => {
215 - <nav class="section-dots"> 188 + if (!prepared.series.length) return [0, 0];
216 - {#each SECTIONS_NAV as s} 189 + const allValues = prepared.series.flatMap(s => s.flatMap(d => [d[0], d[1]]));
190 + return d3.extent(allValues);
191 + });
192 +
193 + let yScale = $derived(
194 + d3.scaleLinear()
195 + .domain(yExtent())
196 + .range([height - 40, 40])
197 + );
198 +
199 + // Generador de área
200 + let areaGenerator = $derived(
201 + d3.area()
202 + .x(d => xScale(d.data.año))
203 + .y0(d => yScale(d[0]))
204 + .y1(d => yScale(d[1]))
205 + .curve(d3.curveBasis)
206 + );
207 +</script>
208 +
209 +<svelte:head>
210 + <title>Streamgraph - Ingresos del Estado</title>
211 +</svelte:head>
212 +
213 +<div class="page">
214 + <header>
215 + <h1>Ingresos del Estado Boliviano</h1>
216 + <p class="subtitle">Streamgraph interactivo con drilldown · 2005-2025</p>
217 + </header>
218 +
219 + <!-- Breadcrumb -->
220 + <nav class="breadcrumb">
221 + {#each breadcrumb as crumb, i}
222 + {#if i > 0}
223 + <span class="separator">→</span>
224 + {/if}
217 <button 225 <button
218 - class="section-dot" 226 + class="crumb"
219 - class:section-dot-active={currentSection === s.id} 227 + class:active={i === breadcrumb.length - 1}
220 - on:click={() => scrollToSection(s.id)} 228 + onclick={() => navigateTo(i)}
221 - aria-label={s.label} 229 + disabled={i === breadcrumb.length - 1}
222 > 230 >
223 - <span class="section-dot-label">{s.label}</span> 231 + {crumb.descripcion}
224 </button> 232 </button>
225 {/each} 233 {/each}
226 </nav> 234 </nav>
227 235
228 - <!-- ═══ LANDING SCREEN (NAV + HERO) ═══ --> 236 + <!-- Nivel actual -->
229 - <section class="landing-screen" data-section="hero"> 237 + <div class="level-indicator">
230 - <nav> 238 + Nivel: <strong>{currentLevel}</strong>
231 - <div class="nav-logo"><img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /></div> 239 + {#if currentLevel !== 'subcuenta'}
232 - </nav> 240 + <span class="hint">· Clic en una banda para ver detalle</span>
233 - 241 + {/if}
234 - <!-- ═══ HERO ═══ --> 242 + </div>
235 - <section class="hero"> 243 +
236 - <div class="dot-pattern"></div> 244 + <!-- Contenedor del gráfico -->
237 - <div class="hero-inner"> 245 + <div class="chart-container" bind:this={container}>
238 - <!-- Bloque 1: Identidad --> 246 + {#if loading}
239 - <div class="hero-identity"> 247 + <div class="loading">Cargando datos...</div>
240 - <h1 class="hero-title anim" style="--d:200ms">Presupuesto <span class="gold">Abierto</span></h1> 248 + {:else if width > 0 && prepared.series.length > 0}
241 - <p class="hero-sub anim" style="--d:340ms">Información diaria e histórica de todo el Estado Boliviano.</p> 249 + <svg {width} {height}>
242 - <div class="hero-stats anim" style="--d:420ms"> 250 + <!-- Eje X -->
243 - <span class="stat-num">21</span> años<span class="stat-dot">·</span><span class="stat-num">8.3M</span> registros<span class="stat-dot">·</span><span class="stat-num">647</span> instituciones<span class="stat-dot">·</span><span class="stat-num">7</span> clasificadores<span class="stat-dot">·</span><span class="stat-num">10</span> visualizaciones<span class="stat-dot">·</span><span class="stat-num">4</span> historias 251 + <g class="axis-x" transform="translate(0, {height - 35})">
244 - </div> 252 + {#each prepared.years.filter((_, i) => i % 2 === 0) as year}
245 - </div> 253 + <text x={xScale(year)} y="20" text-anchor="middle" class="axis-label">
246 - 254 + {year}
247 - <!-- Bloque 2: Búsqueda --> 255 + </text>
248 - <div class="hero-search anim" style="--d:520ms"> 256 + <line
249 - <div class="search-wrap"> 257 + x1={xScale(year)}
250 - <!-- Search bar unificado --> 258 + y1="-{height - 80}"
251 - <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && ($results.length > 0 || ($query.length >= 2 && $results.length === 0) || $isLoading)} style="--accent:{accent}"> 259 + x2={xScale(year)}
252 - <!-- Toggle integrado (desktop) --> 260 + y2="0"
253 - <div class="toggle-bar toggle-desktop"> 261 + class="grid-line"
254 - <button class="toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => switchMode('archivo')}> 262 + />
255 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={mode === 'archivo' ? '#C9A751' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> 263 + {/each}
256 - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> 264 + </g>
257 - </svg> 265 +
258 - 2005 a 2025 266 + <!-- Streams -->
259 - </button> 267 + {#each prepared.series as serie, i}
260 - <button class="toggle-btn" class:toggle-active={mode === 'diario'} on:click={() => switchMode('diario')}> 268 + {@const color = colorScale(serie.key)}
261 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={mode === 'diario' ? '#4A8B6E' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> 269 + <path
262 - <polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/> 270 + d={areaGenerator(serie)}
263 - </svg> 271 + fill={color}
264 - Diario 272 + fill-opacity="0.85"
265 - </button> 273 + stroke={color}
266 - </div> 274 + stroke-width="0.5"
267 - <div class="search-divider"></div> 275 + class="stream-path"
268 - 276 + class:clickable={currentLevel !== 'subcuenta'}
269 - <!-- Mobile: mode indicator + settings button --> 277 + onmouseenter={(e) => {
270 - <button class="mobile-settings-btn" class:mobile-settings-open={mobileSettingsOpen} on:click={toggleMobileSettings}> 278 + tooltip = {
271 - <span class="mobile-mode-indicator" style="background:{accent}"></span> 279 + show: true,
272 - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={mobileSettingsOpen ? accent : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> 280 + x: e.clientX,
273 - <circle cx="12" cy="12" r="3"/> 281 + y: e.clientY,
274 - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> 282 + data: {
275 - </svg> 283 + codigo: serie.key,
276 - </button> 284 + descripcion: serie.descripcion,
277 - 285 + color,
278 - <svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={searchFocused ? accent : '#B8B5AD'} stroke-width="2" stroke-linecap="round"> 286 + total: d3.sum(serie, d => d[1] - d[0])
279 - <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> 287 + }
280 - </svg> 288 + };
281 - <div class="search-input-wrap"> 289 + }}
282 - <input 290 + onmousemove={(e) => {
283 - type="text" 291 + tooltip.x = e.clientX;
284 - bind:this={searchInput} 292 + tooltip.y = e.clientY;
285 - bind:value={searchVal} 293 + }}
286 - on:input={handleInput} 294 + onmouseleave={() => tooltip.show = false}
287 - on:keydown={handleKeydown} 295 + onclick={() => handleClick(+serie.key, serie.descripcion)}
288 - on:focus={() => searchFocused = true} 296 + />
289 - on:blur={handleBlur} 297 + {/each}
290 - disabled={!$indexLoaded}
291 - />
292 - {#if !searchVal && !searchFocused}
293 - <div class="search-placeholder">
294 - <span class="blink-cursor" style="background:{accent}"></span>
295 - <span class="placeholder-text placeholder-desktop">{$indexLoaded ? placeholder : 'Cargando...'}</span>
296 - <span class="placeholder-text placeholder-mobile">{$indexLoaded ? (isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...') : 'Cargando...'}</span>
297 - </div>
298 - {/if}
299 - {#if searchVal && !$isLoading}
300 - <button class="search-clear" on:click={() => { clearSearch(); searchVal = ''; if (searchInput) searchInput.value = ''; }}>
301 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
302 - </button>
303 - {/if}
304 - </div>
305 - {#if !isD}
306 - <div class="gear-btn gear-desktop" class:gear-hover={hGear} on:mouseenter={() => hGear = true} on:mouseleave={() => hGear = false} role="button" tabindex="0">
307 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B8B5AD" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
308 - <circle cx="12" cy="12" r="3"/>
309 - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
310 - </svg>
311 - </div>
312 - {/if}
313 - </div>
314 -
315 - <!-- Search Results Dropdown -->
316 - {#if $isLoading}
317 - <div class="search-results">
318 - <div class="search-loading">Buscando...</div>
319 - </div>
320 - {:else if searchFocused && $results.length > 0}
321 - <div class="search-results">
322 - {#each $results as item, i}
323 - <button
324 - type="button"
325 - class="search-result-item"
326 - class:result-selected={$selectedIndex === i}
327 - on:click={() => handleSelect(item)}
328 - >
329 - <div class="result-main">
330 - <span class="result-name">{item.nombre}</span>
331 - {#if item.sigla}
332 - <span class="result-sigla">({item.sigla})</span>
333 - {/if}
334 - </div>
335 - <div class="result-meta">
336 - {#if item.codigo}
337 - <span class="result-code">{item.codigo}</span>
338 - {/if}
339 - <span class="result-type" style="color:{getTypeColor(item.tipo)}">{getTypeLabel(item.tipo)}</span>
340 - </div>
341 - </button>
342 - {/each}
343 - </div>
344 - {:else if searchFocused && $query.length >= 2 && $results.length === 0}
345 - <div class="search-results">
346 - <div class="search-no-results">Sin resultados para "{$query}"</div>
347 - </div>
348 - {/if}
349 -
350 - {#if $error}
351 - <div class="search-error">{$error}</div>
352 - {/if}
353 -
354 - <!-- Mobile settings panel -->
355 - <div class="mobile-settings-panel" class:mobile-settings-panel-open={mobileSettingsOpen}>
356 - <div class="mobile-toggle-bar">
357 - <button class="mobile-toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => { switchMode('archivo'); mobileSettingsOpen = false; }}>
358 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={mode === 'archivo' ? '#C9A751' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
359 - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
360 - </svg>
361 - <span>Histórico</span>
362 - <span class="mobile-toggle-sub">2005 — 2025</span>
363 - </button>
364 - <button class="mobile-toggle-btn" class:toggle-active={mode === 'diario'} on:click={() => { switchMode('diario'); mobileSettingsOpen = false; }}>
365 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={mode === 'diario' ? '#4A8B6E' : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
366 - <polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/>
367 - </svg>
368 - <span>Diario</span>
369 - <span class="mobile-toggle-sub">En vivo</span>
370 - </button>
371 - </div>
372 - </div>
373 -
374 - <!-- Pills -->
375 - <div class="pills">
376 - <span class="pills-label">Ej:</span>
377 - {#each examples as ex}
378 - <button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button>
379 - {/each}
380 - </div>
381 - </div>
382 - </div>
383 - </div>
384 -
385 - <!-- Scroll hint -->
386 - <div class="scroll-hint anim" style="--d:700ms" class:scroll-hint-hidden={searchFocused && $results.length > 0}>
387 - <svg width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="#B8B5AD" stroke-width="1.2" stroke-linecap="round">
388 - <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/>
389 </svg> 298 </svg>
390 - <span>Explora el portal</span>
391 - </div>
392 - </section>
393 - </section>
394 -
395 - <!-- ═══ PORTAL DIRECTORY ═══ -->
396 - <section class="directory" data-section="datos">
397 - <div class="directory-inner">
398 - <!-- Left: Title -->
399 - <div class="directory-left">
400 - <div use:reveal={{ id: 'dir-head' }} class="reveal" class:revealed={revealStates['dir-head']}>
401 - <p class="section-tag">LOS DATOS</p>
402 - <h2 class="section-title">Dos formas<br/>de ver el<br/>presupuesto.</h2>
403 - </div>
404 - </div>
405 -
406 - <!-- Right: Cards -->
407 - <div class="directory-right">
408 - <!-- Diario -->
409 - <div use:reveal={{ id: 'door-d', delay: 60 }} class="reveal" class:revealed={revealStates['door-d']}>
410 - <div class="door door-warm" class:door-hovered={hDiario} on:mouseenter={() => hDiario = true} on:mouseleave={() => hDiario = false} role="button" tabindex="0">
411 - <div class="door-content">
412 - <div class="door-header">
413 - <div class="door-icon" style="opacity:{hDiario ? 1 : 0.4}">
414 - <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#4A8B6E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/></svg>
415 - </div>
416 - <div class="door-badge">
417 - <div class="pulse-dot-sm"></div>
418 - <span class="door-badge-text" style="color:#4A8B6E">EN VIVO</span>
419 - </div>
420 - </div>
421 - <h3 class="door-title door-title-dark">Diariamente</h3>
422 - <div class="door-pills">
423 - <span class="door-pill door-pill-green">¿Cuánto se ejecutó hoy?</span>
424 - <span class="door-pill door-pill-green">¿Quién gastó más?</span>
425 - <span class="door-pill door-pill-green">¿Qué cambió desde ayer?</span>
426 - <span class="door-pill door-pill-green">Panorama del presupuesto en curso</span>
427 - <span class="door-pill door-pill-green">Bitácora de movimientos</span>
428 - <span class="door-pill door-pill-green">Descarga de datos diaria</span>
429 - <span class="door-pill door-pill-green">Vista de todo el Estado</span>
430 - <span class="door-pill door-pill-green">Vista por Institución</span>
431 - </div>
432 - </div>
433 - <div class="door-footer">
434 - <span class="door-meta">ACTUALIZADO HACE 2 HORAS</span>
435 - <span class="door-cta" style="color:{hDiario ? '#4A8B6E' : '#6B6860'}">Explorar →</span>
436 - </div>
437 - </div>
438 - </div>
439 -
440 - <!-- Archivo -->
441 - <div use:reveal={{ id: 'door-a', delay: 160 }} class="reveal" class:revealed={revealStates['door-a']}>
442 - <div class="door door-warm" class:door-hovered={hArchivo} on:mouseenter={() => hArchivo = true} on:mouseleave={() => hArchivo = false} role="button" tabindex="0">
443 - <div class="door-content">
444 - <div class="door-header">
445 - <div class="door-icon" style="opacity:{hArchivo ? 1 : 0.4}">
446 - <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#A68A3E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="6" width="14" height="14" rx="2"/><path d="M7 6V4.5A1.5 1.5 0 0 1 8.5 3h7A1.5 1.5 0 0 1 17 4.5V6"/><line x1="8" y1="11" x2="14" y2="11"/><line x1="8" y1="14.5" x2="12" y2="14.5"/></svg>
447 - </div>
448 - <span class="door-badge-text" style="color:#A68A3E">2005 — 2025</span>
449 - </div>
450 - <h3 class="door-title door-title-dark">Históricamente</h3>
451 - <div class="door-pills">
452 - <span class="door-pill door-pill-light">Tendencias</span>
453 - <span class="door-pill door-pill-light">Tipos de ingreso</span>
454 - <span class="door-pill door-pill-light">Tipos de gasto</span>
455 - <span class="door-pill door-pill-light">Por institución</span>
456 - <span class="door-pill door-pill-light">Por sector</span>
457 - <span class="door-pill door-pill-light">Por autonomías</span>
458 - <span class="door-pill door-pill-light">Comparar entidades</span>
459 - </div>
460 - </div>
461 - <div class="door-footer">
462 - <span class="door-meta">8M REGISTROS · 7 CLASIFICADORES</span>
463 - <span class="door-cta" style="color:{hArchivo ? '#C9A751' : '#6B6860'}">Explorar →</span>
464 - </div>
465 - </div>
466 - </div>
467 - </div>
468 - </div>
469 - </section>
470 -
471 - <!-- ═══ CLASIFICADORES ═══ -->
472 - <section id="clasificadores" class="clasificadores" data-section="clasificadores">
473 - <div class="clas-container">
474 - <!-- Header -->
475 - <div use:reveal={{ id: 'clas-head' }} class="reveal clas-header-section" class:revealed={revealStates['clas-head']}>
476 - <div class="clas-header-row">
477 - <div>
478 - <p class="section-tag">CLASIFICADORES</p>
479 - <h2 class="section-title">Elige cómo explorar el presupuesto</h2>
480 - <p class="clas-subtitle">Navega por cada una de las 2,626 piezas de los 8 clasificadores.</p>
481 - </div>
482 - <a href="/clasificadores" class="clas-ver-todos">Ver todos →</a>
483 - </div>
484 - </div>
485 299
486 - <!-- ¿Quién? --> 300 + <!-- Leyenda -->
487 - <div use:reveal={{ id: 'clas-quien', delay: 100 }} class="reveal" class:revealed={revealStates['clas-quien']}> 301 + <div class="legend">
488 - <div class="clas-group-card"> 302 + {#each prepared.series as serie}
489 - <div class="clas-group-header"> 303 + {@const color = colorScale(serie.key)}
490 - <div class="clas-group-icon" style="--c:#1C1C1A"> 304 + <button
491 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 305 + class="legend-item"
492 - </div> 306 + onclick={() => handleClick(+serie.key, serie.descripcion)}
493 - <div> 307 + >
494 - <div class="clas-group-label">¿QUIÉN?</div> 308 + <span class="legend-color" style="background: {color}"></span>
495 - <div class="clas-group-sub">La entidad que gasta o recibe</div> 309 + <span class="legend-label">{serie.descripcion}</span>
496 - </div> 310 + </button>
497 - </div>
498 - <a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} on:mouseenter={() => hWidget = 'quien'} on:mouseleave={() => hWidget = null}>
499 - <div class="widget-left">
500 - <div class="widget-headline">
501 - <span class="widget-num-big">756</span>
502 - <span class="widget-label">instituciones públicas</span>
503 - </div>
504 - <div class="widget-desc">Ministerios, gobernaciones, municipios, empresas públicas, universidades</div>
505 - </div>
506 - <div class="widget-right">
507 - <span class="widget-tech">INSTITUCIONAL</span>
508 - <span class="widget-arrow" class:arrow-show={hWidget === 'quien'}>→</span>
509 - </div>
510 - </a>
511 - </div>
512 - </div>
513 -
514 - <!-- Grid: Ingresos + Gastos -->
515 - <div class="clas-main-grid">
516 - <!-- INGRESOS -->
517 - <div use:reveal={{ id: 'clas-ing', delay: 200 }} class="reveal clas-col" class:revealed={revealStates['clas-ing']}>
518 - <div class="clas-group-card">
519 - <div class="clas-group-header">
520 - <div class="clas-group-icon" style="--c:#C9A751">
521 - <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
522 - </div>
523 - <div>
524 - <div class="clas-group-label" style="color:#C9A751">INGRESOS</div>
525 - </div>
526 - </div>
527 - <div class="widgets-col">
528 - {#each WIDGETS_INGRESOS as w}
529 - <a href={w.href} class="widget-card widget-grow" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}>
530 - <div class="widget-headline">
531 - <span class="widget-num-big widget-num-gold">{w.n}</span>
532 - <span class="widget-label">{w.label}</span>
533 - </div>
534 - <p class="widget-desc">{w.d}</p>
535 - <div class="widget-footer">
536 - <span class="widget-tech">{w.t}</span>
537 - <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span>
538 - </div>
539 - </a>
540 - {/each}
541 - </div>
542 - </div>
543 - </div>
544 -
545 - <!-- GASTOS -->
546 - <div use:reveal={{ id: 'clas-gas', delay: 300 }} class="reveal clas-col" class:revealed={revealStates['clas-gas']}>
547 - <div class="clas-group-card">
548 - <div class="clas-group-header">
549 - <div class="clas-group-icon" style="--c:#4A8B6E">
550 - <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
551 - </div>
552 - <div>
553 - <div class="clas-group-label" style="color:#4A8B6E">GASTOS</div>
554 - </div>
555 - </div>
556 -
557 - <!-- Gastos content wrapper -->
558 - <div class="gastos-content">
559 - <!-- ¿En qué? sub-group -->
560 - <div class="clas-subgroup">
561 - <div class="clas-subgroup-header">
562 - <div class="clas-subgroup-icon" style="--c:#4A8B6E">
563 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
564 - </div>
565 - <div>
566 - <div class="clas-subgroup-label">¿EN QUÉ?</div>
567 - </div>
568 - </div>
569 - <div class="widgets-row">
570 - {#each WIDGETS_GASTOS_QUE as w}
571 - <a href={w.href} class="widget-card widget-sm" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}>
572 - <div class="widget-headline">
573 - <span class="widget-num-big widget-num-green">{w.n}</span>
574 - <span class="widget-label">{w.label}</span>
575 - </div>
576 - <p class="widget-desc widget-desc-sm">{w.d}</p>
577 - <div class="widget-footer">
578 - <span class="widget-tech">{w.t}</span>
579 - <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span>
580 - </div>
581 - </a>
582 - {/each}
583 - </div>
584 - </div>
585 -
586 - <!-- Bottom: Para qué + Dónde -->
587 - <div class="widgets-row">
588 - <!-- ¿Para qué? -->
589 - <div class="clas-subgroup">
590 - <div class="clas-subgroup-header">
591 - <div class="clas-subgroup-icon" style="--c:#4A8B6E">
592 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>
593 - </div>
594 - <div>
595 - <div class="clas-subgroup-label">¿PARA QUÉ?</div>
596 - </div>
597 - </div>
598 - <a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} on:mouseenter={() => hWidget = 'finalidad'} on:mouseleave={() => hWidget = null}>
599 - <div class="widget-headline">
600 - <span class="widget-num-big widget-num-green">132</span>
601 - <span class="widget-label">finalidades y funciones</span>
602 - </div>
603 - <p class="widget-desc">Educación, salud, defensa</p>
604 - <div class="widget-footer">
605 - <span class="widget-tech">Finalidad</span>
606 - <span class="widget-arrow" class:arrow-show={hWidget === 'finalidad'}>→</span>
607 - </div>
608 - </a>
609 - </div>
610 -
611 - <!-- ¿Dónde? -->
612 - <div class="clas-subgroup">
613 - <div class="clas-subgroup-header">
614 - <div class="clas-subgroup-icon" style="--c:#4A8B6E">
615 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>
616 - </div>
617 - <div>
618 - <div class="clas-subgroup-label">¿DÓNDE?</div>
619 - </div>
620 - </div>
621 - <a href="/clasificadores/geografico" class="widget-card widget-full" class:widget-hover={hWidget === 'donde'} on:mouseenter={() => hWidget = 'donde'} on:mouseleave={() => hWidget = null}>
622 - <div class="widget-headline">
623 - <span class="widget-num-big widget-num-green">352</span>
624 - <span class="widget-label">ubicaciones geográficas</span>
625 - </div>
626 - <p class="widget-desc">Departamentos, municipios</p>
627 - <div class="widget-footer">
628 - <span class="widget-tech">Geográfico</span>
629 - <span class="widget-arrow" class:arrow-show={hWidget === 'donde'}>→</span>
630 - </div>
631 - </a>
632 - </div>
633 - </div>
634 - </div>
635 - </div>
636 - </div>
637 - </div>
638 - </div>
639 - </section>
640 -
641 - <!-- ═══ HISTORIAS ═══ -->
642 - <section id="historias" class="historias" data-section="historias">
643 - <div class="container">
644 - <div use:reveal={{ id: 'hist-head' }} class="reveal" class:revealed={revealStates['hist-head']}>
645 - <div class="hist-header">
646 - <div>
647 - <div class="hist-tag-row">
648 - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6B6860" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
649 - <p class="section-tag">HISTORIAS</p>
650 - </div>
651 - <h2 class="section-title">Los datos, contados.</h2>
652 - <p class="hist-subtitle">Investigaciones y análisis narrativos sobre el presupuesto público.</p>
653 - </div>
654 - <a href="/historias" class="gold-link">Ver todas →</a>
655 - </div>
656 - </div>
657 - <div class="hist-grid">
658 - {#each HISTORIAS as h, i}
659 - <div use:reveal={{ id: `hist-${i}`, delay: 60 + i * 80 }} class="reveal hist-cell" class:revealed={revealStates[`hist-${i}`]} class:hist-tall={h.tall}>
660 - <div class="hist-card" class:hist-card-hovered={hHist === i} on:mouseenter={() => hHist = i} on:mouseleave={() => hHist = null} role="button" tabindex="0" style="--card-color:{h.color}">
661 - <div class="hist-accent" style="background:{h.color};transform:scaleX({hHist === i ? 1 : 0})"></div>
662 - <div class="hist-content">
663 - <span class="hist-tag" style="color:{h.color}">{h.tag}</span>
664 - <h4 class="hist-title">{h.title}</h4>
665 - <p class="hist-desc">{h.desc}</p>
666 - </div>
667 - <div class="hist-cta" style="color:{hHist === i ? h.color : '#6B6860'}">
668 - Leer historia <span class="hist-arrow" class:hist-arrow-moved={hHist === i}>→</span>
669 - </div>
670 - </div>
671 - </div>
672 {/each} 311 {/each}
673 </div> 312 </div>
674 - </div> 313 + {:else}
675 - </section> 314 + <div class="no-data">No hay datos para este nivel</div>
676 - 315 + {/if}
677 - <!-- ═══ VISUALIZACIONES ═══ --> 316 + </div>
678 - <section id="visualizaciones" class="visualizaciones" data-section="visualizaciones"> 317 +
679 - <div class="container"> 318 + <!-- Tooltip -->
680 - <div use:reveal={{ id: 'vis-head' }} class="reveal" class:revealed={revealStates['vis-head']}> 319 + {#if tooltip.show && tooltip.data}
681 - <div class="vis-header"> 320 + <div
682 - <div> 321 + class="tooltip"
683 - <div class="vis-tag-row"> 322 + style="left: {tooltip.x + 15}px; top: {tooltip.y - 10}px;"
684 - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6B6860" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> 323 + >
685 - <p class="section-tag">VISUALIZACIONES</p> 324 + <div class="tooltip-header" style="border-color: {tooltip.data.color}">
686 - </div> 325 + <span class="tooltip-code">{tooltip.data.codigo}</span>
687 - <h2 class="section-title">Explora los datos.</h2> 326 + <span class="tooltip-desc">{tooltip.data.descripcion}</span>
688 - <p class="vis-subtitle">Herramientas interactivas para navegar el presupuesto.</p>
689 - </div>
690 - <a href="/visualizaciones" class="gold-link">Ver todas →</a>
691 - </div>
692 </div> 327 </div>
693 - <div class="vis-grid-simple"> 328 + <div class="tooltip-value">
694 - {#each VISUALIZACIONES as v, i} 329 + Total acumulado: {formatMoney(tooltip.data.total)}
695 - <div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}>
696 - <div class="vis-card" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0">
697 - <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
698 - <div>
699 - <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
700 - <h4 class="vis-title">{v.title}</h4>
701 - <p class="vis-desc">{v.desc}</p>
702 - </div>
703 - <div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}">
704 - Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span>
705 - </div>
706 - </div>
707 - </div>
708 - {/each}
709 </div> 330 </div>
331 + {#if currentLevel !== 'subcuenta'}
332 + <div class="tooltip-hint">Clic para ver detalle</div>
333 + {/if}
710 </div> 334 </div>
711 - </section> 335 + {/if}
712 -
713 - <!-- ═══ MANIFIESTO ═══ -->
714 - <section class="manifiesto" data-section="manifiesto">
715 - <div class="dot-pattern"></div>
716 - <div class="manifiesto-inner">
717 - <div use:reveal={{ id: 'man' }} class="reveal" class:revealed={revealStates['man']}>
718 - <p class="section-tag-light">POR QUÉ EXISTE ESTO</p>
719 - <h2 class="manifiesto-title">El presupuesto público no es de los gobiernos.<br/><span class="gold">Es de todos.</span></h2>
720 - </div>
721 - </div>
722 - </section>
723 -
724 - <!-- ═══ DESCARGAS ═══ -->
725 - <section id="descargas" class="descargas" data-section="descargas">
726 - <div class="descargas-inner">
727 - <div use:reveal={{ id: 'desc' }} class="reveal" class:revealed={revealStates['desc']}>
728 - <h2 class="descargas-title">Descarga los datos.</h2>
729 - <div class="descargas-formats">
730 - <span class="format-pill">.csv</span>
731 - <span class="format-pill">.parquet</span>
732 - <span class="format-pill">.json</span>
733 - </div>
734 - <a href="/descargas" class="descargas-link">
735 - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v12"/><polyline points="8 12 12 16 16 12"/><path d="M4 18h16"/></svg>
736 - Ir a página de descargas
737 - </a>
738 - </div>
739 - </div>
740 - </section>
741 -
742 - <!-- ═══ FOOTER ═══ -->
743 - <footer>
744 - <div class="footer-inner">
745 - <div class="footer-org">MINISTERIO DE ECONOMÍA Y FINANZAS PÚBLICAS</div>
746 - <div class="footer-right">DATOS ABIERTOS · 2005 — 2026</div>
747 - </div>
748 - </footer>
749 </div> 336 </div>
750 337
751 <style> 338 <style>
752 - @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap'); 339 + .page {
753 - :global(*){margin:0;padding:0;box-sizing:border-box} 340 + min-height: 100vh;
754 - :global(html){width:100%;height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;scroll-behavior:smooth} 341 + background: #1a1a1a;
755 - :global(body){width:100%;min-height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A} 342 + color: #f5f5f5;
756 - :global(::selection){background:#C9A751;color:#1C1C1A} 343 + padding: 2rem;
757 - :global(::-webkit-scrollbar){width:5px} 344 + font-family: system-ui, -apple-system, sans-serif;
758 - :global(::-webkit-scrollbar-track){background:#1C1C1A} 345 + }
759 - :global(::-webkit-scrollbar-thumb){background:#444;border-radius:3px} 346 +
760 - :global(a){color:inherit;text-decoration:none} 347 + header {
761 - 348 + text-align: center;
762 - @keyframes pulse{0%,100%{opacity:0.35}50%{opacity:1}} 349 + margin-bottom: 2rem;
763 - @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} 350 + }
764 - @keyframes scrollHint{0%,100%{transform:translateY(0);opacity:0.6}50%{transform:translateY(5px);opacity:1}} 351 +
765 - @keyframes heartbeat{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:48}} 352 + h1 {
766 - 353 + font-size: 2rem;
767 - .page{--serif:'Qanelas',system-ui,sans-serif;--sans:'Qanelas',system-ui,sans-serif;--mono:'JetBrains Mono',monospace;font-family:var(--serif);color:#1C1C1A;width:100%;min-width:100%;overflow-x:hidden;scroll-snap-type:y mandatory;overflow-y:scroll;height:100vh} 354 + font-weight: 700;
768 - 355 + margin: 0;
769 - /* Section Navigation Dots */ 356 + color: #f5f5f5;
770 - .section-dots{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:100;display:flex;flex-direction:column;gap:12px;background:rgba(255,255,255,0.1);backdrop-filter:blur(8px);padding:12px 8px;border-radius:20px} 357 + }
771 - .section-dot{width:10px;height:10px;border-radius:50%;border:1.5px solid rgba(100,100,100,0.4);background:rgba(150,150,150,0.2);cursor:pointer;transition:all 0.3s;padding:0;position:relative} 358 +
772 - .section-dot:hover{border-color:rgba(100,100,100,0.6);background:rgba(150,150,150,0.4);transform:scale(1.2)} 359 + .subtitle {
773 - .section-dot-active{background:#C9A751;border-color:#C9A751} 360 + color: #888;
774 - .section-dot-active:hover{transform:scale(1.2)} 361 + margin-top: 0.5rem;
775 - .section-dot-label{position:absolute;right:20px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;letter-spacing:0.05em;color:#6B6860;white-space:nowrap;opacity:0;transition:opacity 0.2s;pointer-events:none} 362 + }
776 - .section-dot:hover .section-dot-label{opacity:1} 363 +
777 - .gold{color:#C9A751} 364 + .breadcrumb {
778 - .container{max-width:1060px;margin:0 auto} 365 + display: flex;
779 - 366 + align-items: center;
780 - /* Reveal */ 367 + gap: 0.5rem;
781 - .reveal{opacity:0;transform:translateY(28px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)} 368 + margin-bottom: 1rem;
782 - .revealed{opacity:1!important;transform:translateY(0)!important} 369 + flex-wrap: wrap;
783 - .anim{opacity:0;transform:translateY(18px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1);transition-delay:var(--d,0ms)} 370 + }
784 - .mounted .anim{opacity:1;transform:translateY(0)} 371 +
785 - 372 + .separator {
786 - /* ── NAV ── */ 373 + color: #555;
787 - nav{background:#1C1C1A;padding:12px 48px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0} 374 + }
788 - .nav-left{display:flex;align-items:center} 375 +
789 - .nav-right{display:flex;align-items:center;gap:24px} 376 + .crumb {
790 - .nav-link{font-family:var(--sans);font-size:14px;color:#B8B5AD;letter-spacing:0.01em;font-weight:400;transition:color 0.2s} 377 + background: #2a2a2a;
791 - .nav-link:hover{color:#F5F0E8} 378 + border: 1px solid #333;
792 - .nav-link-icon{display:flex;align-items:center;gap:5px} 379 + color: #aaa;
793 - .heartbeat-line{animation:heartbeat 3s ease-in-out infinite} 380 + padding: 0.4rem 0.8rem;
794 - .nav-date{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:9px;letter-spacing:0.06em;color:#B8B5AD;font-weight:400} 381 + border-radius: 6px;
795 - .date-label{color:#9B9890} 382 + cursor: pointer;
796 - .date-sep{margin:0 3px;opacity:0.3} 383 + font-size: 0.85rem;
797 - .pulse-dot{width:5px;height:5px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite} 384 + transition: all 0.2s;
798 - .pulse-dot-sm{width:6px;height:6px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite;box-shadow:0 0 8px rgba(74,139,110,0.37)} 385 + }
799 - .nav-sep{width:1px;height:16px;background:#2A2A27} 386 +
800 - .nav-logo{height:88px;display:flex;align-items:center;flex-shrink:0} 387 + .crumb:hover:not(:disabled) {
801 - .nav-logo img{height:100%;width:auto;object-fit:contain} 388 + background: #333;
802 - 389 + color: #fff;
803 - /* ── HERO ── */ 390 + }
804 - .landing-screen{height:100vh;background:#1C1C1A;display:flex;flex-direction:column;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} 391 +
805 - .hero{background:#1C1C1A;color:#F5F0E8;flex:1;display:flex;flex-direction:column;justify-content:center;padding:40px 48px 32px;position:relative;overflow:hidden} 392 + .crumb.active {
806 - .dot-pattern{position:absolute;inset:0;opacity:0.02;background-image:radial-gradient(circle at 1px 1px,#B8B5AD 0.5px,transparent 0);background-size:40px 40px} 393 + background: #C9A751;
807 - .dot-pattern-sm{position:absolute;inset:0;opacity:0.015;background-image:radial-gradient(circle at 1px 1px,#fff 0.5px,transparent 0);background-size:32px 32px} 394 + color: #1a1a1a;
808 - .hero-inner{max-width:1060px;margin:0 auto;width:100%;position:relative;margin-top:-80px;text-align:center} 395 + font-weight: 600;
809 - .hero-identity{margin-bottom:20px} 396 + cursor: default;
810 - .hero-search{display:flex;justify-content:center;position:relative;z-index:50} 397 + }
811 - .hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD} 398 +
812 - .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;max-width:720px;margin:14px auto 0} 399 + .crumb:disabled {
813 - .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#9B9890;max-width:100%;margin-top:14px;line-height:1.6} 400 + cursor: default;
814 - .hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1} 401 + }
815 - .stat-num{color:#F5F0E8} 402 +
816 - .stat-dot{margin:0 10px;opacity:0.3} 403 + .level-indicator {
817 - 404 + color: #666;
818 - /* Search */ 405 + font-size: 0.85rem;
819 - .search-wrap{max-width:860px;width:100%;margin:0 auto;position:relative;z-index:9999} 406 + margin-bottom: 1rem;
820 - .search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.04);border-radius:16px;padding:8px 14px 8px 8px;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(201,167,81,0.12),0 8px 32px rgba(0,0,0,0.25);position:relative} 407 + }
821 - .search-focused{background:rgba(255,255,255,0.07);box-shadow:0 0 0 1.5px rgba(201,167,81,0.35),0 8px 40px rgba(0,0,0,0.3)} 408 +
822 - .search-has-results{border-radius:16px 16px 0 0} 409 + .level-indicator strong {
823 - .toggle-bar{display:flex;border-radius:10px;padding:2px;background:rgba(0,0,0,0.2);flex-shrink:0} 410 + color: #C9A751;
824 - .toggle-btn{background:transparent;border:none;border-radius:8px;padding:12px 16px;cursor:pointer;display:flex;align-items:center;gap:7px;transition:all 0.3s;font-family:var(--sans);font-size:14px;font-weight:400;color:#9B9890} 411 + text-transform: uppercase;
825 - .toggle-btn svg{display:block;flex-shrink:0} 412 + }
826 - .toggle-btn:hover{color:#B8B5AD} 413 +
827 - .toggle-active{background:rgba(255,255,255,0.1);font-weight:600;color:#F5F0E8} 414 + .hint {
828 - .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} 415 + color: #555;
829 - .search-icon{flex-shrink:0;transition:stroke 0.3s} 416 + }
830 - .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} 417 +
831 - .search-input-wrap input{width:100%;background:transparent;border:none;outline:none;color:#F5F0E8;font-size:16px;font-family:var(--sans);padding:12px 12px 12px 10px;caret-color:transparent} 418 + .chart-container {
832 - .search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none} 419 + background: #222;
833 - .blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1.1s step-end infinite;border-radius:1px;flex-shrink:0} 420 + border-radius: 12px;
834 - .placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD} 421 + padding: 1rem;
835 - .placeholder-mobile{display:none} 422 + min-height: 500px;
836 - .gear-btn{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s;flex-shrink:0;margin-left:8px} 423 + position: relative;
837 - .gear-hover{background:rgba(255,255,255,0.12)} 424 + }
838 - 425 +
839 - /* Mobile settings button (hidden on desktop) */ 426 + .loading, .no-data {
840 - .mobile-settings-btn{display:none;width:40px;height:40px;border-radius:10px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;align-items:center;justify-content:center;position:relative;transition:all 0.3s;flex-shrink:0} 427 + display: flex;
841 - .mobile-settings-btn:hover,.mobile-settings-open{background:rgba(255,255,255,0.12)} 428 + align-items: center;
842 - .mobile-mode-indicator{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%} 429 + justify-content: center;
843 - 430 + height: 400px;
844 - /* Mobile settings panel (hidden on desktop) */ 431 + color: #666;
845 - .mobile-settings-panel{display:none;overflow:hidden;max-height:0;opacity:0;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)} 432 + }
846 - .mobile-settings-panel-open{max-height:200px;opacity:1;margin-top:12px} 433 +
847 - .mobile-toggle-bar{display:flex;gap:10px} 434 + svg {
848 - .mobile-toggle-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;padding:16px 12px;border-radius:12px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s} 435 + display: block;
849 - .mobile-toggle-btn.toggle-active{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.15)} 436 + }
850 - .mobile-toggle-btn span{font-family:var(--sans);font-size:14px;font-weight:600;color:#F5F0E8} 437 +
851 - .mobile-toggle-btn svg{margin-bottom:4px} 438 + .axis-label {
852 - .mobile-toggle-sub{font-size:11px!important;font-weight:400!important;color:#9B9890!important;font-family:var(--mono)!important;letter-spacing:0.05em} 439 + fill: #666;
853 - 440 + font-size: 12px;
854 - /* Search clear button */ 441 + }
855 - .search-clear{background:none;border:none;cursor:pointer;color:#6B6860;padding:4px;display:flex;align-items:center;justify-content:center;transition:color 0.2s;flex-shrink:0} 442 +
856 - .search-clear:hover{color:#F5F0E8} 443 + .grid-line {
857 - 444 + stroke: #333;
858 - /* Search Results Dropdown */ 445 + stroke-width: 0.5;
859 - .search-results{position:absolute;top:100%;left:0;right:0;background:rgba(28,28,26,0.98);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);border-top:none;border-radius:0 0 16px 16px;max-height:320px;overflow-y:auto;z-index:9999} 446 + stroke-dasharray: 2, 4;
860 - .search-loading,.search-no-results{padding:16px 20px;font-family:var(--sans);font-size:14px;color:#9B9890;text-align:center} 447 + }
861 - .search-error{margin-top:8px;font-family:var(--sans);font-size:13px;color:#C44B3F;text-align:center} 448 +
862 - 449 + .stream-path {
863 - .search-result-item{width:100%;text-align:left;background:none;border:none;border-top:1px solid rgba(255,255,255,0.06);padding:14px 20px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;transition:background 0.2s} 450 + transition: fill-opacity 0.2s, transform 0.2s;
864 - .search-result-item:first-child{border-top:none} 451 + }
865 - .search-result-item:hover,.result-selected{background:rgba(255,255,255,0.06)} 452 +
866 - 453 + .stream-path:hover {
867 - .result-main{display:flex;align-items:center;gap:8px;flex:1;min-width:0} 454 + fill-opacity: 1;
868 - .result-name{font-family:var(--sans);font-size:14px;color:#F5F0E8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 455 + }
869 - .result-sigla{font-family:var(--sans);font-size:12px;color:#9B9890;flex-shrink:0} 456 +
870 - 457 + .stream-path.clickable {
871 - .result-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:12px} 458 + cursor: pointer;
872 - .result-code{font-family:var(--mono);font-size:10px;color:#9B9890;background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px} 459 + }
873 - .result-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;font-weight:600} 460 +
874 - .pills{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;align-items:center;justify-content:center} 461 + .stream-path.clickable:hover {
875 - .pills-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#B8B5AD;margin-right:2px} 462 + filter: brightness(1.1);
876 - .pill{font-family:var(--mono);font-size:12px;font-weight:300;letter-spacing:0.03em;border:none;border-radius:6px;padding:5px 12px;cursor:pointer;transition:all 0.25s;line-height:1.4;position:relative;overflow:hidden} 463 + }
877 - .pill::before{content:'';position:absolute;inset:0;background:rgba(255,255,255,0.12);opacity:0;transition:opacity 0.25s} 464 +
878 - .pill:hover::before{opacity:1} 465 + .legend {
879 - .pill:hover{transform:translateY(-1px)} 466 + display: flex;
880 - 467 + flex-wrap: wrap;
881 - .scroll-hint{position:absolute;bottom:28px;left:0;right:0;margin:0 auto;width:fit-content;display:flex;flex-direction:column;align-items:center;gap:10px;animation:scrollHint 2.5s ease-in-out infinite;z-index:1;transition:opacity 0.3s} 468 + gap: 0.5rem;
882 - .scroll-hint-hidden{opacity:0;pointer-events:none} 469 + margin-top: 1rem;
883 - .scroll-hint span{font-family:var(--mono);font-size:10px;letter-spacing:0.2em;color:#B8B5AD;opacity:0.7} 470 + padding-top: 1rem;
884 - 471 + border-top: 1px solid #333;
885 - /* ── DIRECTORY ── */ 472 + }
886 - .directory{background:#f5f5f7;min-height:100vh;display:flex;align-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:48px} 473 +
887 - .directory-inner{display:grid;grid-template-columns:280px 1fr;gap:48px;width:100%;max-width:1200px;margin:0 auto;align-items:center} 474 + .legend-item {
888 - .directory-left{} 475 + display: flex;
889 - .directory-right{display:flex;flex-direction:column;gap:20px} 476 + align-items: center;
890 - .section-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#6B6860;margin-bottom:10px} 477 + gap: 0.4rem;
891 - .section-tag-light{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#B8B5AD} 478 + background: #2a2a2a;
892 - .section-title{font-size:clamp(40px,4.5vw,56px);font-weight:900;letter-spacing:-0.025em;line-height:1.05;color:#1C1C1A} 479 + border: 1px solid #333;
893 - .section-title-light{font-size:clamp(32px,4vw,46px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8} 480 + padding: 0.3rem 0.6rem;
894 - 481 + border-radius: 4px;
895 - /* Door cards */ 482 + cursor: pointer;
896 - .door{border-radius:24px;padding:44px 40px 40px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;display:flex;flex-direction:column;justify-content:space-between;min-height:260px} 483 + transition: all 0.2s;
897 - .door-hovered{transform:translateY(-4px)} 484 + }
898 - .door-dark{background:#1C1C1A;color:#F5F0E8;box-shadow:none} 485 +
899 - .door-dark.door-hovered{box-shadow:0 24px 64px rgba(0,0,0,0.2)} 486 + .legend-item:hover {
900 - .door-warm{background:#ffffff;color:#1C1C1A;box-shadow:none} 487 + background: #333;
901 - .door-warm.door-hovered{box-shadow:0 24px 64px rgba(201,167,81,0.12)} 488 + }
902 - .door-accent{position:absolute;top:0;left:0;right:0;height:3px;transform-origin:left;transition:transform 0.6s cubic-bezier(0.16,1,0.3,1)} 489 +
903 - .door-content{position:relative} 490 + .legend-color {
904 - .door-header{display:flex;align-items:center;gap:10px;margin-bottom:16px} 491 + width: 12px;
905 - .door-icon{transition:opacity 0.3s} 492 + height: 12px;
906 - .door-badge{display:flex;align-items:center;gap:6px} 493 + border-radius: 2px;
907 - .door-badge-text{font-family:var(--mono);font-size:11px;letter-spacing:0.16em;font-weight:500} 494 + flex-shrink: 0;
908 - .door-title{font-size:clamp(28px,3vw,38px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8} 495 + }
909 - .door-title-dark{color:#1C1C1A} 496 +
910 - .door-desc{font-family:var(--sans);font-size:17px;color:#6B6860;line-height:1.55;margin-top:10px;max-width:480px} 497 + .legend-label {
911 - .door-desc-light{color:#9B9890} 498 + font-size: 0.75rem;
912 - .door-pills{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px} 499 + color: #aaa;
913 - .door-pill{font-family:var(--sans);font-size:16px;padding:10px 16px;border-radius:8px;line-height:1.3} 500 + max-width: 150px;
914 - .door-pill-dark{background:rgba(74,139,110,0.12);color:#8ac4a8;font-weight:500} 501 + overflow: hidden;
915 - .door-pill-light{background:rgba(166,138,62,0.1);color:#a68a3e;font-weight:500} 502 + text-overflow: ellipsis;
916 - .door-pill-green{background:rgba(74,139,110,0.1);color:#3d7a5e;font-weight:500} 503 + white-space: nowrap;
917 - .door-footer{position:relative;display:flex;justify-content:space-between;align-items:center;margin-top:24px} 504 + }
918 - .door-meta{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#4A4843} 505 +
919 - .door-meta-light{color:#B8B5AD} 506 + .tooltip {
920 - .door-cta{font-family:var(--sans);font-size:16px;font-weight:400;transition:color 0.3s} 507 + position: fixed;
921 - 508 + background: rgba(30, 30, 30, 0.95);
922 - /* ── CLASIFICADORES ── */ 509 + backdrop-filter: blur(8px);
923 - .clasificadores{background:#e8e8ea;color:#1C1C1A;padding:48px;height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} 510 + border: 1px solid #444;
924 - .clas-container{max-width:1100px;margin:0 auto;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column} 511 + border-radius: 8px;
925 - .clas-header-section{margin-bottom:24px;flex-shrink:0} 512 + padding: 0.75rem 1rem;
926 - .clas-header-row{display:flex;justify-content:space-between;align-items:flex-end} 513 + pointer-events: none;
927 - .clas-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px} 514 + z-index: 1000;
928 - .clas-ver-todos{font-family:var(--sans);font-size:15px;font-weight:500;color:#1C1C1A;transition:color 0.2s} 515 + max-width: 300px;
929 - .clas-ver-todos:hover{color:#4A8B6E} 516 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
930 - 517 + }
931 - /* Group cards */ 518 +
932 - .clas-group-card{background:#f2f2f4;border:1px solid rgba(0,0,0,0.04);border-radius:20px;padding:16px;margin-bottom:12px;height:100%;display:flex;flex-direction:column;transition:all 0.3s} 519 + .tooltip-header {
933 - /* Parent reacts to child hover */ 520 + border-left: 3px solid;
934 - .clas-group-card:has(.widget-card:hover){background:#efefef;border-color:rgba(0,0,0,0.06)} 521 + padding-left: 0.5rem;
935 - .clas-group-card:has(.widget-card:hover) > .clas-group-header .clas-group-label{transform:scale(1.05);transform-origin:left} 522 + margin-bottom: 0.5rem;
936 - /* Parent hover highlights children */ 523 + }
937 - .clas-group-card:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)} 524 +
938 - .clas-group-card:hover .clas-subgroup{background:#f5f5f7} 525 + .tooltip-code {
939 - .gastos-content{display:flex;flex-direction:column;flex:1;gap:10px} 526 + font-family: monospace;
940 - .gastos-content .clas-subgroup{flex:1;margin-bottom:0} 527 + font-size: 0.75rem;
941 - .gastos-content > .widgets-row{flex:1} 528 + color: #888;
942 - .clas-group-header{display:flex;align-items:center;gap:10px;margin-bottom:10px} 529 + display: block;
943 - .clas-group-icon{width:32px;height:32px;border-radius:10px;background:color-mix(in srgb, var(--c) 10%, transparent);border:1px solid color-mix(in srgb, var(--c) 15%, transparent);display:flex;align-items:center;justify-content:center;color:var(--c);flex-shrink:0} 530 + }
944 - .clas-group-label{font-family:var(--mono);font-size:12px;letter-spacing:0.15em;font-weight:600;color:#1C1C1A;transition:transform 0.3s cubic-bezier(0.16,1,0.3,1)} 531 +
945 - .clas-group-sub{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px} 532 + .tooltip-desc {
946 - 533 + font-size: 0.9rem;
947 - /* Subgroup */ 534 + color: #f5f5f5;
948 - .clas-subgroup{background:#f7f7f9;border:1px solid rgba(0,0,0,0.03);border-radius:14px;padding:12px;margin-bottom:10px;display:flex;flex-direction:column;transition:all 0.3s} 535 + font-weight: 500;
949 - /* Subgroup parent reacts to child hover */ 536 + }
950 - .clas-subgroup:has(.widget-card:hover){background:#f3f3f5;border-color:rgba(0,0,0,0.05)} 537 +
951 - .clas-subgroup:has(.widget-card:hover) > .clas-subgroup-header .clas-subgroup-label{transform:scale(1.05);transform-origin:left} 538 + .tooltip-value {
952 - /* Subgroup hover highlights children */ 539 + font-size: 0.85rem;
953 - .clas-subgroup:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)} 540 + color: #C9A751;
954 - .clas-subgroup .widgets-row{flex:1} 541 + font-weight: 600;
955 - .clas-subgroup-header{display:flex;align-items:center;gap:10px;margin-bottom:10px}
956 - .clas-subgroup-icon{width:32px;height:32px;border-radius:10px;background:color-mix(in srgb, var(--c) 10%, transparent);border:1px solid color-mix(in srgb, var(--c) 15%, transparent);display:flex;align-items:center;justify-content:center;color:var(--c);flex-shrink:0}
957 - .clas-subgroup-label{font-family:var(--mono);font-size:12px;letter-spacing:0.15em;font-weight:600;color:#1C1C1A;transition:transform 0.3s cubic-bezier(0.16,1,0.3,1)}
958 - .clas-subgroup-tag{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px}
959 -
960 - /* Main grid */
961 - .clas-main-grid{display:grid;grid-template-columns:1fr 2fr;gap:12px;align-items:stretch;flex:1;min-height:0}
962 - .clas-col{display:flex;flex-direction:column}
963 - .clas-col .clas-group-card{flex:1}
964 -
965 - /* Widget cards */
966 - .widget-card{display:block;background:#fafafa;border:1px solid rgba(0,0,0,0.04);border-radius:14px;padding:14px;cursor:pointer;text-decoration:none;color:#1C1C1A;transition:all 0.35s cubic-bezier(0.16,1,0.3,1);box-shadow:0 1px 3px rgba(0,0,0,0.02)}
967 - .widget-hover{background:#ffffff;border-color:rgba(0,0,0,0.06);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.05)}
968 - .widget-h{display:flex;justify-content:space-between;align-items:center}
969 - .widget-left{flex:1}
970 - .widget-right{display:flex;align-items:center;gap:12px;flex-shrink:0}
971 - .widget-grow{flex:1}
972 - .widget-sm{padding:12px}
973 - .widget-full{flex:1}
974 - .widgets-col{display:flex;flex-direction:column;gap:8px;flex:1}
975 - .widgets-col .widget-card{flex:1}
976 - .widgets-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;flex:1}
977 - .widgets-row .widget-card{display:flex;flex-direction:column;justify-content:space-between}
978 - .widget-headline{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}
979 - .widget-num-big{font-family:var(--mono);font-size:24px;font-weight:700;line-height:1}
980 - .widget-num-gold{color:#a68a3e}
981 - .widget-num-green{color:#4A8B6E}
982 - .widget-label{font-family:var(--sans);font-size:14px;font-weight:700;color:#1C1C1A;line-height:1.2}
983 - .widget-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.4;font-weight:400}
984 - .widget-desc-sm{font-size:13px}
985 - .widget-footer{display:flex;justify-content:space-between;align-items:center;margin-top:8px}
986 - .widget-tech{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;color:#9B9890;text-transform:uppercase}
987 - .widget-arrow{font-family:var(--sans);font-size:14px;color:transparent;transform:translateX(-6px);transition:all 0.35s cubic-bezier(0.16,1,0.3,1)}
988 - .arrow-show{color:#1C1C1A;transform:translateX(0)}
989 -
990 - /* ── HISTORIAS ── */
991 - .historias{background:#1C1C1A;padding:80px 48px;color:#F5F0E8;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always}
992 - .historias .container{width:100%;max-width:1060px;margin:0 auto}
993 - .hist-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px}
994 - .hist-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
995 - .hist-tag-row svg{stroke:#9B9890}
996 - .hist-tag-row .section-tag{color:#9B9890}
997 - .historias .section-title{color:#F5F0E8}
998 - .hist-subtitle{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:10px}
999 - .hist-grid{display:grid;grid-template-columns:repeat(3, 1fr);grid-auto-rows:minmax(120px, auto);gap:16px}
1000 - .hist-cell{grid-row:span 1}
1001 - .hist-tall{grid-row:span 2}
1002 - .hist-card{background:color-mix(in srgb, var(--card-color) 15%, #1C1C1A);border:1px solid color-mix(in srgb, var(--card-color) 25%, transparent);border-radius:18px;padding:28px 24px 24px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;height:100%;display:flex;flex-direction:column;justify-content:space-between}
1003 - .hist-card-hovered{background:color-mix(in srgb, var(--card-color) 22%, #1C1C1A);border-color:color-mix(in srgb, var(--card-color) 35%, transparent);transform:translateY(-3px)}
1004 - .hist-accent{position:absolute;top:0;left:0;right:0;height:2px;transform-origin:left;transition:transform 0.5s cubic-bezier(0.16,1,0.3,1)}
1005 - .hist-content{flex:1}
1006 - .hist-tag{font-family:var(--mono);font-size:10px;letter-spacing:0.14em;font-weight:500}
1007 - .hist-title{font-family:var(--serif);font-size:18px;font-weight:800;margin-top:12px;line-height:1.25;letter-spacing:-0.01em;color:#F5F0E8}
1008 - .hist-tall .hist-title{font-size:22px}
1009 - .hist-desc{font-family:var(--sans);font-size:14px;color:#9B9890;line-height:1.5;margin-top:10px}
1010 - .hist-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860}
1011 - .hist-arrow{transition:transform 0.3s;display:inline-block}
1012 - .hist-arrow-moved{transform:translateX(3px)}
1013 -
1014 - /* ── VISUALIZACIONES ── */
1015 - .visualizaciones{background:#f5f5f7;padding:80px 48px;color:#1C1C1A;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always}
1016 - .visualizaciones .container{width:100%;max-width:1060px;margin:0 auto}
1017 - .vis-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px}
1018 - .vis-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
1019 - .vis-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px}
1020 - .gold-link{font-family:var(--sans);font-size:15px;color:#C9A751;font-weight:600}
1021 - .vis-grid-simple{display:grid;grid-template-columns:repeat(3, 1fr);gap:16px}
1022 - .vis-card{background:#fafafc;border-radius:18px;padding:28px 24px 24px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;height:100%;display:flex;flex-direction:column;justify-content:space-between}
1023 - .vis-card-hovered{transform:translateY(-2px);box-shadow:0 16px 40px rgba(0,0,0,0.05)}
1024 - .vis-accent{position:absolute;top:0;left:0;right:0;height:2px;transform-origin:left;transition:transform 0.5s cubic-bezier(0.16,1,0.3,1)}
1025 - .vis-type{font-family:var(--mono);font-size:10px;letter-spacing:0.1em;font-weight:500;padding:5px 10px;border-radius:6px;display:inline-block}
1026 - .vis-title{font-family:var(--serif);font-size:18px;font-weight:800;margin-top:12px;line-height:1.25;letter-spacing:-0.01em;color:#1C1C1A}
1027 - .vis-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.5;margin-top:10px}
1028 - .vis-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860}
1029 - .vis-arrow{transition:transform 0.3s;display:inline-block}
1030 - .vis-arrow-moved{transform:translateX(3px)}
1031 -
1032 - /* ── MANIFIESTO ── */
1033 - .manifiesto{background:#1C1C1A;color:#F5F0E8;padding:96px 48px;position:relative;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always}
1034 - .manifiesto-inner{max-width:640px;width:100%;margin:0 auto;position:relative;text-align:center}
1035 - .manifiesto-title{font-size:clamp(36px,4.5vw,56px);font-weight:900;line-height:1.08;letter-spacing:-0.025em;margin-top:18px}
1036 - .manifiesto-text{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:20px;line-height:1.7}
1037 -
1038 - /* ── DESCARGAS ── */
1039 - .descargas{background:#1C1C1A;padding:80px 48px;color:#F5F0E8;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always}
1040 - .descargas-inner{max-width:640px;width:100%;margin:0 auto;text-align:center}
1041 - .descargas-title{font-size:clamp(32px,4vw,48px);font-weight:900;line-height:1.1;letter-spacing:-0.02em;color:#F5F0E8}
1042 - .descargas-formats{display:flex;justify-content:center;gap:12px;margin-top:24px}
1043 - .format-pill{font-family:var(--mono);font-size:14px;letter-spacing:0.05em;color:#9B9890;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);padding:8px 16px;border-radius:8px}
1044 - .descargas-link{display:inline-flex;align-items:center;gap:8px;margin-top:32px;font-family:var(--sans);font-size:15px;color:#C9A751;transition:all 0.3s}
1045 - .descargas-link:hover{color:#d4b45a;gap:10px}
1046 -
1047 - /* ── FOOTER ── */
1048 - footer{background:#1C1C1A;padding:40px 48px;border-top:1px solid #2A2A27}
1049 - .footer-inner{max-width:1060px;margin:0 auto;display:flex;justify-content:space-between;align-items:center}
1050 - .footer-org{font-family:var(--mono);font-size:12px;letter-spacing:0.12em;color:#6B6860}
1051 - .footer-unit{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:3px}
1052 - .footer-right{font-family:var(--mono);font-size:12px;letter-spacing:0.1em;color:#6B6860}
1053 -
1054 - /* ══════════════════════════════════════════════════════════════════════════
1055 - RESPONSIVE - TABLET (768px)
1056 - ══════════════════════════════════════════════════════════════════════════ */
1057 - @media(max-width:768px){
1058 - .page{scroll-snap-type:y proximity}
1059 - .landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal}
1060 -
1061 - /* Section dots - bottom horizontal bar */
1062 - .section-dots{position:fixed;right:auto;left:50%;top:auto;bottom:20px;transform:translateX(-50%);flex-direction:row;gap:10px;padding:12px 20px;border-radius:24px;background:rgba(28,28,26,0.7);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);z-index:1000}
1063 - .section-dot{width:8px;height:8px}
1064 - .section-dot-label{display:none}
1065 -
1066 - /* Nav */
1067 - nav{padding:12px 24px}
1068 - .nav-logo{height:60px}
1069 -
1070 - /* Hero */
1071 - .hero{padding:32px 24px}
1072 - .hero-inner{margin-top:0}
1073 - .hero-title{font-size:clamp(36px,8vw,48px)}
1074 - .hero-stats{font-size:11px;line-height:1.6}
1075 - .stat-dot{margin:0 6px}
1076 -
1077 - /* Search bar - mobile compact */
1078 - .search-bar{flex-wrap:nowrap;padding:8px 12px;gap:8px}
1079 - .toggle-desktop{display:none}
1080 - .search-divider{display:none}
1081 - .gear-desktop{display:none}
1082 - .mobile-settings-btn{display:flex}
1083 - .mobile-settings-panel{display:block}
1084 - .search-icon{margin-left:0}
1085 - .search-input-wrap{order:1}
1086 - .search-input-wrap input{font-size:15px;padding:10px 8px}
1087 - .placeholder-desktop{display:none}
1088 - .placeholder-mobile{display:inline;font-size:14px}
1089 -
1090 - /* Search results mobile */
1091 - .search-results{max-height:260px;border-radius:0 0 12px 12px}
1092 - .search-result-item{padding:12px 16px}
1093 - .result-name{font-size:13px}
1094 - .result-meta{flex-direction:column;align-items:flex-end;gap:4px}
1095 - .result-code{font-size:9px}
1096 - .result-type{font-size:8px}
1097 - .pills{gap:6px}
1098 - .pill{font-size:11px;padding:4px 10px}
1099 - .scroll-hint{bottom:20px}
1100 -
1101 - /* Directory */
1102 - .directory{padding:48px 24px}
1103 - .directory-inner{grid-template-columns:1fr;gap:32px}
1104 - .directory-left{text-align:center}
1105 - .section-title{font-size:clamp(32px,7vw,42px)}
1106 - .door{padding:28px 24px 24px;min-height:auto}
1107 - .door-title{font-size:clamp(24px,5vw,32px)}
1108 - .door-pills{gap:6px}
1109 - .door-pill{font-size:14px;padding:8px 12px}
1110 - .door-meta{font-size:10px}
1111 -
1112 - /* Clasificadores */
1113 - .clasificadores{padding:32px 20px;height:auto;min-height:100vh;overflow-y:auto}
1114 - .clas-container{max-height:none}
1115 - .clas-header-row{flex-direction:column;align-items:flex-start;gap:12px}
1116 - .clas-ver-todos{align-self:flex-start}
1117 - .clas-main-grid{grid-template-columns:1fr;gap:12px}
1118 - .widgets-row{grid-template-columns:1fr}
1119 - .widget-h{flex-direction:column;align-items:flex-start;gap:12px}
1120 - .widget-right{width:100%;justify-content:space-between}
1121 - .widget-num-big{font-size:22px}
1122 - .clas-group-icon,.clas-subgroup-icon{width:28px;height:28px}
1123 -
1124 - /* Historias */
1125 - .historias{padding:60px 24px}
1126 - .hist-header{flex-direction:column;align-items:flex-start;gap:16px}
1127 - .hist-grid{grid-template-columns:1fr 1fr;grid-auto-rows:auto;gap:12px}
1128 - .hist-tall{grid-row:span 1}
1129 - .hist-card{padding:20px 18px 18px}
1130 - .hist-title{font-size:16px}
1131 - .hist-tall .hist-title{font-size:18px}
1132 - .hist-desc{font-size:13px}
1133 -
1134 - /* Visualizaciones */
1135 - .visualizaciones{padding:60px 24px}
1136 - .vis-header{flex-direction:column;align-items:flex-start;gap:16px}
1137 - .vis-grid-simple{grid-template-columns:1fr 1fr;gap:12px}
1138 - .vis-card{padding:20px 18px 18px}
1139 - .vis-title{font-size:16px}
1140 -
1141 - /* Manifiesto */
1142 - .manifiesto{padding:60px 24px}
1143 - .manifiesto-title{font-size:clamp(28px,6vw,40px)}
1144 -
1145 - /* Descargas */
1146 - .descargas{padding:60px 24px}
1147 - .descargas-title{font-size:clamp(28px,6vw,40px)}
1148 - .descargas-formats{gap:8px}
1149 - .format-pill{font-size:12px;padding:6px 12px}
1150 -
1151 - /* Footer */
1152 - footer{padding:32px 24px}
1153 - .footer-inner{flex-direction:column;gap:12px;text-align:center}
1154 - .footer-org,.footer-right{font-size:10px}
1155 } 542 }
1156 543
1157 - /* ══════════════════════════════════════════════════════════════════════════ 544 + .tooltip-hint {
1158 - RESPONSIVE - MOBILE (480px) 545 + font-size: 0.75rem;
1159 - ══════════════════════════════════════════════════════════════════════════ */ 546 + color: #666;
1160 - @media(max-width:480px){ 547 + margin-top: 0.3rem;
1161 - /* Section dots - bottom bar smaller */
1162 - .section-dots{bottom:16px;gap:8px;padding:10px 16px;border-radius:20px}
1163 - .section-dot{width:7px;height:7px}
1164 -
1165 - /* Nav */
1166 - nav{padding:10px 16px}
1167 - .nav-logo{height:48px}
1168 -
1169 - /* Hero */
1170 - .hero{padding:24px 16px}
1171 - .hero-title{font-size:clamp(32px,9vw,40px)}
1172 - .hero-sub{font-size:15px}
1173 - .hero-stats{font-size:10px;display:flex;flex-wrap:wrap;justify-content:center;gap:4px 0}
1174 - .stat-dot{margin:0 4px}
1175 -
1176 - /* Search */
1177 - .search-wrap{padding:0}
1178 - .mobile-settings-btn{width:36px;height:36px}
1179 - .mobile-toggle-btn{padding:14px 10px}
1180 - .mobile-toggle-btn span{font-size:13px}
1181 - .search-results{max-height:220px}
1182 - .search-result-item{padding:10px 14px}
1183 - .result-name{font-size:12px}
1184 - .result-sigla{font-size:10px}
1185 - .pills-label{display:none}
1186 - .pill{font-size:10px;padding:4px 8px}
1187 - .scroll-hint{display:none}
1188 -
1189 - /* Directory */
1190 - .directory{padding:40px 16px}
1191 - .directory-inner{gap:24px}
1192 - .section-tag{font-size:10px}
1193 - .section-title{font-size:clamp(28px,8vw,36px)}
1194 - .door{padding:24px 20px 20px;border-radius:18px}
1195 - .door-icon svg{width:22px;height:22px}
1196 - .door-title{font-size:clamp(22px,6vw,28px)}
1197 - .door-pill{font-size:13px;padding:7px 10px}
1198 - .door-footer{flex-direction:column;align-items:flex-start;gap:8px}
1199 - .door-meta{font-size:9px}
1200 - .door-cta{font-size:14px}
1201 -
1202 - /* Clasificadores */
1203 - .clasificadores{padding:24px 16px}
1204 - .clas-header-section{margin-bottom:16px}
1205 - .section-title{font-size:clamp(24px,6vw,32px)}
1206 - .clas-subtitle{font-size:14px}
1207 - .clas-group-card{padding:12px;border-radius:14px;margin-bottom:10px}
1208 - .clas-subgroup{padding:10px;border-radius:10px}
1209 - .clas-group-header,.clas-subgroup-header{margin-bottom:8px}
1210 - .clas-group-label,.clas-subgroup-label{font-size:10px}
1211 - .clas-group-sub{font-size:12px}
1212 - .widget-card{padding:12px;border-radius:10px}
1213 - .widget-num-big{font-size:20px}
1214 - .widget-label{font-size:13px}
1215 - .widget-desc{font-size:12px}
1216 - .widget-tech{font-size:8px}
1217 -
1218 - /* Historias */
1219 - .historias{padding:48px 16px}
1220 - .hist-subtitle{font-size:14px}
1221 - .hist-grid{grid-template-columns:1fr;gap:10px}
1222 - .hist-card{padding:18px 16px 16px;border-radius:14px}
1223 - .hist-tag{font-size:9px}
1224 - .hist-title{font-size:16px}
1225 - .hist-desc{font-size:12px;margin-top:8px}
1226 - .hist-cta{font-size:13px;margin-top:14px}
1227 -
1228 - /* Visualizaciones */
1229 - .visualizaciones{padding:48px 16px}
1230 - .vis-subtitle{font-size:14px}
1231 - .vis-grid-simple{grid-template-columns:1fr;gap:10px}
1232 - .vis-card{padding:18px 16px 16px;border-radius:14px}
1233 - .vis-type{font-size:9px;padding:4px 8px}
1234 - .vis-title{font-size:16px}
1235 - .vis-desc{font-size:12px}
1236 - .vis-cta{font-size:13px;margin-top:14px}
1237 -
1238 - /* Manifiesto */
1239 - .manifiesto{padding:48px 16px}
1240 - .section-tag-light{font-size:10px}
1241 - .manifiesto-title{font-size:clamp(24px,7vw,32px)}
1242 -
1243 - /* Descargas */
1244 - .descargas{padding:48px 16px}
1245 - .descargas-title{font-size:clamp(24px,7vw,32px)}
1246 - .descargas-formats{flex-wrap:wrap;gap:6px}
1247 - .format-pill{font-size:11px;padding:5px 10px}
1248 - .descargas-link{font-size:14px;margin-top:24px}
1249 -
1250 - /* Footer */
1251 - footer{padding:24px 16px}
1252 - .footer-org{font-size:9px;letter-spacing:0.08em}
1253 - .footer-right{font-size:9px}
1254 } 548 }
1255 -</style>
...\ No newline at end of file ...\ No newline at end of file
549 +</style>
......
This diff could not be displayed because it is too large.