Rafael Lopez

documentacion

......@@ -27,6 +27,8 @@ claude.md
# Docs internos
static/ESTILOS_*.md
static/docs.md
BASES_DE_DATOS.md
.Rproj.user
# Data
......
......@@ -10,7 +10,8 @@
"dependencies": {
"@supabase/supabase-js": "^2.98.0",
"d3": "^7.9.0",
"fuse.js": "^7.1.0"
"fuse.js": "^7.1.0",
"marked": "^17.0.4"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
......@@ -2335,6 +2336,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/marked": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
"integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
......
......@@ -21,6 +21,7 @@
"dependencies": {
"@supabase/supabase-js": "^2.98.0",
"d3": "^7.9.0",
"fuse.js": "^7.1.0"
"fuse.js": "^7.1.0",
"marked": "^17.0.4"
}
}
......
......@@ -58,23 +58,32 @@
gearMenuOpen = !gearMenuOpen;
}
function closeGearMenu() {
setTimeout(() => { gearMenuOpen = false; }, 150);
}
onMount(() => {
setTimeout(() => { mounted = true; }, 80);
initIndex();
// Pre-calentar Supabase en background para evitar cold start
warmupSupabase();
// Cerrar gear menu al hacer click fuera
const handleClickOutside = (e) => {
if (gearMenuOpen && !e.target.closest('.gear-wrap')) {
gearMenuOpen = false;
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
});
// Auto-focus en el buscador cuando el índice esté listo (solo una vez)
// Auto-focus en el buscador cuando el índice esté listo (solo desktop)
$: if ($indexLoaded && searchInput && !autoFocused) {
autoFocused = true;
setTimeout(() => {
searchInput?.focus();
}, 50);
// No auto-focus en móvil para evitar que aparezca el teclado
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
if (!isMobile) {
setTimeout(() => {
searchInput?.focus();
}, 50);
}
}
function toggleMobileSettings() {
......@@ -85,7 +94,7 @@
$: accent = isD ? '#4A8B6E' : '#C9A751';
$: placeholder = isD
? 'Busca una entidad, un ministerio, un municipio...'
: 'Busca en 7 clasificadores: partidas, rubros, fuentes...';
: 'Busca instituciones, gastos, ingresos, sectores...';
$: examples = isD
? [
{ label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
......@@ -93,11 +102,11 @@
{ label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
]
: [
{ label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962' },
{ label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
{ label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962', href: '/objeto/11700' },
{ label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0', href: '/rubro/14000' },
{ label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' },
{ label: 'Consultorías', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
{ label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0' },
{ label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
{ label: 'Publicidad', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
];
function switchMode(m) {
......@@ -205,9 +214,9 @@
];
const VISUALIZACIONES = [
{ 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' },
{ color: '#C9A751', title: 'Treemap del presupuesto 2025', desc: 'Explora cada peso del presupuesto actual.', type: 'Treemap' },
{ color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa' },
{ 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 },
{ color: '#C9A751', title: '¿En qué se gasta?', desc: 'Explora el presupuesto por objeto de gasto.', type: 'Treemap', href: '/clasificadores/objeto-gasto?modo=mapa' },
{ color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa', href: null },
];
/* Reveal action */
......@@ -295,7 +304,7 @@
<div class="hero-search anim" style="--d:520ms">
<div class="search-wrap">
<!-- Search bar unificado -->
<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}">
<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}">
<!-- Toggle integrado (desktop) -->
<div class="toggle-bar toggle-desktop">
<button class="toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => switchMode('archivo')}>
......@@ -338,7 +347,7 @@
/>
{#if !searchVal}
<div class="search-placeholder">
<span class="blink-cursor" style="background:{accent}"></span>
<span class="blink-cursor" class:cursor-diario={isD} class:cursor-archivo={!isD}></span>
<span class="placeholder-text placeholder-desktop">{placeholder}</span>
<span class="placeholder-text placeholder-mobile">{isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...'}</span>
</div>
......@@ -350,8 +359,8 @@
{/if}
</div>
<!-- Gear button with filter dropdown (desktop) -->
<div class="gear-wrap gear-desktop">
<button class="gear-btn" class:gear-active={gearMenuOpen} on:click={toggleGearMenu} on:blur={closeGearMenu}>
<div class="gear-wrap gear-desktop" on:click|stopPropagation>
<button class="gear-btn" class:gear-active={gearMenuOpen} on:click={toggleGearMenu}>
<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">
<circle cx="12" cy="12" r="3"/>
<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 @@
</button>
{#if gearMenuOpen}
<div class="gear-menu">
<div class="gear-menu-title">Buscar en:</div>
<button class="gear-filter-btn" class:filter-active={searchFilters.entidad} on:mousedown={() => toggleFilter('entidad')}>
<span class="filter-dot" style="background:#4A8B6E"></span>
<span>Entidades</span>
<span class="filter-check">{searchFilters.entidad ? '✓' : ''}</span>
<div class="gear-menu-title">Ajusta tu búsqueda</div>
<button class="gear-row" on:click={() => toggleFilter('entidad')}>
<div class="gear-check" class:gear-check-on={searchFilters.entidad}>
{#if searchFilters.entidad}
<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>
{/if}
</div>
<div class="gear-row-content">
<span class="gear-row-question">¿Quién?</span>
<span class="gear-row-label" class:gear-row-label-on={searchFilters.entidad}>Instituciones</span>
</div>
<span class="gear-row-count">756</span>
</button>
<button class="gear-filter-btn" class:filter-active={searchFilters.objeto_gasto} on:mousedown={() => toggleFilter('objeto_gasto')}>
<span class="filter-dot" style="background:#C9A751"></span>
<span>Objetos de gasto</span>
<span class="filter-check">{searchFilters.objeto_gasto ? '✓' : ''}</span>
<button class="gear-row" on:click={() => toggleFilter('objeto_gasto')}>
<div class="gear-check" class:gear-check-on={searchFilters.objeto_gasto}>
{#if searchFilters.objeto_gasto}
<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>
{/if}
</div>
<div class="gear-row-content">
<span class="gear-row-question">¿En qué?</span>
<span class="gear-row-label" class:gear-row-label-on={searchFilters.objeto_gasto}>Objetos de gasto</span>
</div>
<span class="gear-row-count">324</span>
</button>
<button class="gear-filter-btn" class:filter-active={searchFilters.rubro} on:mousedown={() => toggleFilter('rubro')}>
<span class="filter-dot" style="background:#8B6E9B"></span>
<span>Rubros</span>
<span class="filter-check">{searchFilters.rubro ? '✓' : ''}</span>
<button class="gear-row" on:click={() => toggleFilter('rubro')}>
<div class="gear-check" class:gear-check-on={searchFilters.rubro}>
{#if searchFilters.rubro}
<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>
{/if}
</div>
<div class="gear-row-content">
<span class="gear-row-question">¿De dónde?</span>
<span class="gear-row-label" class:gear-row-label-on={searchFilters.rubro}>Rubros de ingreso</span>
</div>
<span class="gear-row-count">324</span>
</button>
</div>
{/if}
......@@ -381,7 +411,7 @@
</div>
<!-- Search Results Dropdown -->
{#if $isLoading}
{#if $isLoading && $query.length >= 2}
<div class="search-results">
<div class="search-loading">Buscando...</div>
</div>
......@@ -438,19 +468,19 @@
</button>
</div>
<div class="mobile-filter-section">
<div class="mobile-filter-title">Buscar en:</div>
<div class="mobile-filter-title">Buscar</div>
<div class="mobile-filter-bar">
<button class="mobile-filter-chip" class:filter-chip-active={searchFilters.entidad} on:click={() => toggleFilter('entidad')}>
<span class="filter-chip-dot" style="background:#4A8B6E"></span>
Entidades
Instituciones
</button>
<button class="mobile-filter-chip" class:filter-chip-active={searchFilters.objeto_gasto} on:click={() => toggleFilter('objeto_gasto')}>
<span class="filter-chip-dot" style="background:#C9A751"></span>
Objetos
Gastos
</button>
<button class="mobile-filter-chip" class:filter-chip-active={searchFilters.rubro} on:click={() => toggleFilter('rubro')}>
<span class="filter-chip-dot" style="background:#8B6E9B"></span>
Rubros
Ingresos
</button>
</div>
</div>
......@@ -460,7 +490,11 @@
<div class="pills">
<span class="pills-label">Ej:</span>
{#each examples as ex}
<button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button>
{#if ex.href}
<a class="pill" style="color:{ex.color};background:{ex.bg}" href={ex.href}>{ex.label}</a>
{:else}
<button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button>
{/if}
{/each}
</div>
</div>
......@@ -778,17 +812,31 @@
<div class="vis-grid-simple">
{#each VISUALIZACIONES as v, i}
<div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}>
<div class="vis-card" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0">
<div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
<div>
<span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
<h4 class="vis-title">{v.title}</h4>
<p class="vis-desc">{v.desc}</p>
</div>
<div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}">
Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span>
{#if v.href}
<a href={v.href} class="vis-card vis-card-link" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null}>
<div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
<div>
<span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
<h4 class="vis-title">{v.title}</h4>
<p class="vis-desc">{v.desc}</p>
</div>
<div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}">
Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span>
</div>
</a>
{:else}
<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">
<div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
<div>
<span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
<h4 class="vis-title">{v.title}</h4>
<p class="vis-desc">{v.desc}</p>
</div>
<div class="vis-cta" style="color:#6B6860">
Próximamente
</div>
</div>
</div>
{/if}
</div>
{/each}
</div>
......@@ -913,24 +961,31 @@
.search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0}
.search-icon{flex-shrink:0;transition:stroke 0.3s}
.search-input-wrap{flex:1;position:relative;display:flex;align-items:center}
.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}
.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}
.search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none}
.blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1.1s step-end infinite;border-radius:1px;flex-shrink:0}
.blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1s step-end infinite;border-radius:1px;flex-shrink:0}
.cursor-diario{background:#4A8B6E}
.cursor-archivo{background:#C9A751}
.placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD}
.placeholder-mobile{display:none}
.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}
.gear-btn svg{transition:transform 0.3s ease}
.gear-btn:hover svg{transform:rotate(45deg)}
.gear-btn:hover,.gear-active{background:rgba(255,255,255,0.12)}
.gear-wrap{position:relative}
.gear-desktop{display:block}
.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)}
.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}
.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}
.gear-filter-btn:hover{background:rgba(255,255,255,0.06)}
.gear-filter-btn.filter-active{color:#F5F0E8}
.filter-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;opacity:0.4;transition:opacity 0.2s}
.gear-filter-btn.filter-active .filter-dot{opacity:1}
.filter-check{margin-left:auto;font-size:12px;color:#4A8B6E;opacity:0;transition:opacity 0.2s}
.gear-filter-btn.filter-active .filter-check{opacity:1}
.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}
@keyframes gearFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
.gear-menu-title{font-family:var(--sans);font-size:11px;font-weight:400;color:#6B6860;padding:10px 14px 6px;letter-spacing:0.02em}
.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}
.gear-row:hover{background:rgba(255,255,255,0.04)}
.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}
.gear-check-on{border-color:transparent;background:#C9A751;color:#1C1C1A}
.gear-row-content{flex:1;display:flex;align-items:baseline;gap:8px}
.gear-row-question{font-family:var(--sans);font-size:12px;color:#5A5850;font-weight:400}
.gear-row-label{font-family:var(--sans);font-size:14px;color:#8B8880;transition:color 0.15s}
.gear-row-label-on{color:#F5F0E8}
.gear-row-count{font-family:var(--mono);font-size:10px;color:#4A4840}
/* Mobile settings button (hidden on desktop) */
.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 @@
.result-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;font-weight:600}
.pills{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;align-items:center;justify-content:center}
.pills-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#B8B5AD;margin-right:2px}
.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}
.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}
.pill::before{content:'';position:absolute;inset:0;background:rgba(255,255,255,0.12);opacity:0;transition:opacity 0.25s}
.pill:hover::before{opacity:1}
.pill:hover{transform:translateY(-1px)}
......@@ -1131,6 +1186,9 @@
.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}
.vis-arrow{transition:transform 0.3s;display:inline-block}
.vis-arrow-moved{transform:translateX(3px)}
.vis-card-link{text-decoration:none;color:inherit}
.vis-card-disabled{opacity:0.6;cursor:default}
.vis-card-disabled:hover{transform:none;box-shadow:none}
/* ── MANIFIESTO ── */
.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.
<script>
import { onMount } from 'svelte';
import { marked } from 'marked';
let content = $state('');
let loading = $state(true);
let headings = $state([]);
let activeId = $state('');
let sidebarOpen = $state(false);
let expandedSections = $state(new Set());
// Build hierarchical structure from flat headings
function buildHierarchy(flatHeadings) {
const root = { children: [], level: 0 };
const stack = [root];
for (const heading of flatHeadings) {
const node = { ...heading, children: [] };
// Pop stack until we find a parent with lower level
while (stack.length > 1 && stack[stack.length - 1].level >= heading.level) {
stack.pop();
}
stack[stack.length - 1].children.push(node);
stack.push(node);
}
return root.children;
}
// Extract headings from markdown and add IDs
function processMarkdown(markdown) {
const headingsList = [];
let headingIndex = 0;
// Custom renderer for marked v17+
const renderer = {
heading({ tokens, depth }) {
const text = tokens.map(t => t.text || t.raw || '').join('');
const slug = text
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
const id = `${slug}-${headingIndex++}`;
// Include up to level 4
if (depth <= 4) {
headingsList.push({
id,
text: text,
level: depth
});
}
// Add anchor link to each heading
return `<h${depth} id="${id}" class="heading-anchor"><a href="#${id}" class="anchor-link">${text}</a></h${depth}>`;
}
};
marked.use({ renderer });
const html = marked.parse(markdown);
return { html, headings: headingsList };
}
onMount(async () => {
try {
const response = await fetch('/docs.md');
const markdown = await response.text();
const result = processMarkdown(markdown);
content = result.html;
headings = buildHierarchy(result.headings);
// Expand level 1 by default
const initialExpanded = new Set();
result.headings.filter(h => h.level === 1).forEach(h => initialExpanded.add(h.id));
expandedSections = initialExpanded;
// Setup intersection observer for scroll tracking
setTimeout(() => {
setupScrollTracking(result.headings);
// Handle URL hash on page load
if (window.location.hash) {
const id = window.location.hash.slice(1);
const element = document.getElementById(id);
if (element) {
// Expand parents of this heading
const parentIds = findParentIds(id, headings);
if (parentIds && parentIds.length > 0) {
const newExpanded = new Set(expandedSections);
parentIds.forEach(pid => newExpanded.add(pid));
expandedSections = newExpanded;
}
// Scroll to element
setTimeout(() => {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
activeId = id;
}, 50);
}
}
}, 100);
} catch (error) {
console.error('Error loading documentation:', error);
content = '<p>Error al cargar la documentación.</p>';
} finally {
loading = false;
}
});
// Find all parent IDs for a given heading ID
function findParentIds(targetId, nodes, parents = []) {
for (const node of nodes) {
if (node.id === targetId) {
return parents;
}
if (node.children && node.children.length > 0) {
const found = findParentIds(targetId, node.children, [...parents, node.id]);
if (found) return found;
}
}
return null;
}
function setupScrollTracking(flatHeadings) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const newActiveId = entry.target.id;
activeId = newActiveId;
// Auto-expand parents of the active heading
const parentIds = findParentIds(newActiveId, headings);
if (parentIds && parentIds.length > 0) {
const newExpanded = new Set(expandedSections);
parentIds.forEach(id => newExpanded.add(id));
expandedSections = newExpanded;
}
}
});
},
{
rootMargin: '-100px 0px -70% 0px',
threshold: 0
}
);
flatHeadings.forEach((heading) => {
const element = document.getElementById(heading.id);
if (element) {
observer.observe(element);
}
});
return () => observer.disconnect();
}
function scrollToHeading(id) {
const element = document.getElementById(id);
if (element) {
// Update URL hash without triggering scroll
history.pushState(null, '', `#${id}`);
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
sidebarOpen = false;
}
}
function toggleSection(id) {
const newExpanded = new Set(expandedSections);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
expandedSections = newExpanded;
}
function toggleSidebar() {
sidebarOpen = !sidebarOpen;
}
// Check if a heading or any of its children is active
function isActiveOrHasActiveChild(node, currentActiveId) {
if (node.id === currentActiveId) return true;
if (node.children) {
return node.children.some(child => isActiveOrHasActiveChild(child, currentActiveId));
}
return false;
}
</script>
<svelte:head>
<title>Documentación | Presupuesto Público Bolivia</title>
</svelte:head>
<div class="docs-page">
<header class="docs-header">
<div class="docs-header-content">
<div class="docs-header-top">
<a href="/" class="docs-back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
<span>Inicio</span>
</a>
<!-- Mobile menu button -->
<button class="docs-menu-btn" onclick={toggleSidebar}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12h18M3 6h18M3 18h18"/>
</svg>
<span>Índice</span>
</button>
</div>
<h1 class="docs-title">Documentación</h1>
<p class="docs-subtitle">Estructura de las bases de datos del presupuesto público</p>
</div>
</header>
<div class="docs-layout-wrapper">
<!-- Fixed Sidebar / Table of Contents -->
<aside class="docs-sidebar" class:open={sidebarOpen}>
<div class="docs-sidebar-content">
<div class="docs-sidebar-header">
<span class="docs-sidebar-title">Índice</span>
<button class="docs-sidebar-close" onclick={() => sidebarOpen = false}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
<nav class="docs-toc">
{#snippet renderHeading(node, depth = 0)}
<div class="toc-node">
<div class="toc-item-row">
{#if node.children && node.children.length > 0}
<button
class="toc-toggle"
class:expanded={expandedSections.has(node.id)}
onclick={() => toggleSection(node.id)}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>
{:else}
<span class="toc-toggle-spacer"></span>
{/if}
<button
class="docs-toc-item level-{node.level}"
class:active={activeId === node.id}
onclick={() => scrollToHeading(node.id)}
>
{node.text}
</button>
</div>
{#if node.children && node.children.length > 0 && expandedSections.has(node.id)}
<div class="toc-children">
{#each node.children as child}
{@render renderHeading(child, depth + 1)}
{/each}
</div>
{/if}
</div>
{/snippet}
{#each headings as heading}
{@render renderHeading(heading)}
{/each}
</nav>
</div>
</aside>
<!-- Main content -->
<main class="docs-main">
{#if loading}
<div class="docs-loading">
<svg class="docs-loader" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" rx="1" width="10" height="10">
<animate id="sq1" attributeName="fill-opacity" values="1;0.4;0.4;0.4;1" dur="1.6s" repeatCount="indefinite" begin="0s"/>
</rect>
<rect x="13" y="1" rx="1" width="10" height="10">
<animate id="sq2" attributeName="fill-opacity" values="0.4;1;0.4;0.4;0.4" dur="1.6s" repeatCount="indefinite" begin="0s"/>
</rect>
<rect x="1" y="13" rx="1" width="10" height="10">
<animate id="sq3" attributeName="fill-opacity" values="0.4;0.4;0.4;1;0.4" dur="1.6s" repeatCount="indefinite" begin="0s"/>
</rect>
<rect x="13" y="13" rx="1" width="10" height="10">
<animate id="sq4" attributeName="fill-opacity" values="0.4;0.4;1;0.4;0.4" dur="1.6s" repeatCount="indefinite" begin="0s"/>
</rect>
</svg>
<p>Cargando documentación...</p>
</div>
{:else}
<article class="docs-content">
{@html content}
</article>
{/if}
</main>
</div>
<!-- Overlay for mobile -->
{#if sidebarOpen}
<button class="docs-overlay" onclick={() => sidebarOpen = false}></button>
{/if}
</div>
<style>
.docs-page {
min-height: 100vh;
background-color: var(--theme-body);
font-family: var(--font-sans);
}
.docs-header {
border-bottom: 1px solid var(--theme-borde);
background-color: var(--theme-body);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 20;
}
.docs-header-content {
max-width: 1000px;
margin: 0 auto;
padding: 1rem 1.5rem;
}
.docs-header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.docs-back {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--theme-texto);
text-decoration: none;
font-size: 0.875rem;
transition: color 0.15s ease;
}
.docs-back:hover {
color: var(--theme-accent);
}
.docs-menu-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--theme-fill);
border: 1px solid var(--theme-borde);
border-radius: 8px;
color: var(--theme-texto);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s ease;
}
.docs-menu-btn:hover {
color: var(--theme-titulo);
border-color: var(--theme-accent);
}
@media (min-width: 1100px) {
.docs-menu-btn {
display: none;
}
}
.docs-title {
font-family: var(--font-sans);
font-size: 1.5rem;
font-weight: 700;
color: var(--theme-titulo);
margin: 0 0 0.25rem;
}
.docs-subtitle {
color: var(--theme-texto);
font-size: 0.875rem;
margin: 0;
}
/* Centered layout container */
.docs-layout-wrapper {
max-width: 700px;
margin: 0 auto;
padding-top: 140px;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
/* Sidebar */
.docs-sidebar {
display: none;
}
/* Desktop: sidebar (200) + gap (50) + content (600) = 850px total centered */
@media (min-width: 1100px) {
.docs-layout-wrapper {
max-width: 600px;
margin-left: calc(50% - 425px + 250px);
margin-right: auto;
padding-left: 2rem;
padding-right: 2rem;
}
.docs-sidebar {
display: block;
position: fixed;
top: 120px;
left: calc(50% - 425px);
width: 200px;
max-height: calc(100vh - 140px);
overflow-y: auto;
overflow-x: hidden;
padding: 0;
z-index: 10;
}
.docs-sidebar-content {
padding-right: 1rem;
padding-top: 2rem;
}
}
/* Larger screens: more space */
@media (min-width: 1300px) {
.docs-layout-wrapper {
max-width: 650px;
margin-left: calc(50% - 475px + 280px);
}
.docs-sidebar {
left: calc(50% - 475px);
width: 230px;
}
}
/* Mobile sidebar */
@media (max-width: 1099px) {
.docs-sidebar {
display: block;
position: fixed;
top: 0;
left: 0;
width: 320px;
height: 100vh;
background: var(--theme-body);
z-index: 100;
transform: translateX(-100%);
transition: transform 0.25s ease;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
}
.docs-sidebar.open {
transform: translateX(0);
}
.docs-sidebar-content {
height: 100%;
overflow-y: auto;
padding: 1rem;
}
}
.docs-sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--theme-borde);
}
@media (min-width: 1100px) {
.docs-sidebar-header {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1.25rem;
}
.docs-sidebar-close {
display: none;
}
}
.docs-sidebar-title {
font-size: 0.8125rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--theme-texto);
}
.docs-sidebar-close {
padding: 0.25rem;
color: var(--theme-texto);
background: none;
border: none;
cursor: pointer;
border-radius: 4px;
transition: color 0.15s ease;
}
.docs-sidebar-close:hover {
color: var(--theme-titulo);
}
/* Table of Contents */
.docs-toc {
display: flex;
flex-direction: column;
gap: 1px;
}
.toc-node {
display: flex;
flex-direction: column;
}
.toc-item-row {
display: flex;
align-items: flex-start;
gap: 2px;
}
.toc-toggle {
flex-shrink: 0;
width: 20px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--theme-texto);
cursor: pointer;
opacity: 0.5;
transition: all 0.15s ease;
padding: 0;
}
.toc-toggle:hover {
opacity: 1;
color: var(--theme-accent);
}
.toc-toggle svg {
transition: transform 0.15s ease;
}
.toc-toggle.expanded svg {
transform: rotate(90deg);
}
.toc-toggle-spacer {
width: 20px;
flex-shrink: 0;
}
.toc-children {
padding-left: 10px;
border-left: 1px solid color-mix(in srgb, var(--theme-borde) 50%, transparent);
margin-left: 9px;
}
.docs-toc-item {
flex: 1;
display: block;
text-align: left;
padding: 0.35rem 0.5rem;
font-size: 0.8125rem;
color: var(--theme-texto);
background: none;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
line-height: 1.3;
}
.docs-toc-item:hover {
color: var(--theme-titulo);
background: var(--theme-fill);
}
.docs-toc-item.active {
color: var(--theme-accent);
background: color-mix(in srgb, var(--theme-accent) 12%, transparent);
font-weight: 500;
}
.docs-toc-item.level-1 {
font-weight: 600;
font-size: 0.875rem;
color: var(--theme-titulo);
}
.docs-toc-item.level-2 {
font-weight: 500;
}
.docs-toc-item.level-3 {
font-size: 0.8125rem;
}
.docs-toc-item.level-4 {
font-size: 0.75rem;
font-family: monospace;
color: color-mix(in srgb, var(--theme-texto) 80%, transparent);
}
.docs-toc-item.level-4.active {
color: var(--theme-accent);
}
/* Overlay */
.docs-overlay {
display: none;
}
@media (max-width: 1099px) {
.docs-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 50;
border: none;
cursor: pointer;
}
}
/* Main content */
.docs-main {
flex: 1;
min-width: 0;
padding: 0 0 4rem 0;
}
@media (max-width: 1099px) {
.docs-layout-wrapper {
padding-top: 130px;
padding-left: 1rem;
padding-right: 1rem;
}
.docs-main {
padding: 0 0 4rem 0;
}
}
.docs-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
gap: 1rem;
color: var(--theme-texto);
}
.docs-loader {
width: 32px;
height: 32px;
fill: var(--theme-texto);
opacity: 0.6;
}
/* Markdown content styles */
.docs-content {
color: var(--theme-titulo);
line-height: 1.7;
}
.docs-content :global(h1) {
font-family: var(--font-sans);
font-size: 1.75rem;
font-weight: 700;
color: var(--theme-titulo);
margin: 3rem 0 1.5rem;
scroll-margin-top: 130px;
}
.docs-content :global(h1:first-child) {
margin-top: 0;
}
.docs-content :global(h2) {
font-family: var(--font-sans);
font-size: 1.5rem;
font-weight: 700;
color: var(--theme-titulo);
margin: 2.5rem 0 1rem;
scroll-margin-top: 130px;
}
.docs-content :global(h3) {
font-size: 1.25rem;
font-weight: 600;
color: var(--theme-titulo);
margin: 2rem 0 0.75rem;
scroll-margin-top: 130px;
}
.docs-content :global(h4) {
font-size: 1rem;
font-weight: 600;
color: var(--theme-accent);
margin: 1.5rem 0 0.5rem;
font-family: monospace;
scroll-margin-top: 130px;
}
.docs-content :global(p) {
margin: 0 0 1rem;
color: var(--theme-texto);
}
.docs-content :global(ul),
.docs-content :global(ol) {
margin: 0 0 1rem;
padding-left: 1.5rem;
color: var(--theme-texto);
}
.docs-content :global(li) {
margin-bottom: 0.5rem;
}
.docs-content :global(strong) {
font-weight: 600;
color: var(--theme-titulo);
}
.docs-content :global(code) {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 0.875em;
background-color: var(--theme-fill);
color: var(--theme-accent);
padding: 0.2em 0.4em;
border-radius: 4px;
border: 1px solid var(--theme-borde);
}
.docs-content :global(pre) {
background-color: var(--theme-fill);
border: 1px solid var(--theme-borde);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0 1.5rem;
overflow-x: auto;
}
.docs-content :global(pre code) {
background: none;
border: none;
padding: 0;
font-size: 0.875rem;
color: var(--theme-titulo);
}
.docs-content :global(hr) {
border: none;
height: 1px;
background: color-mix(in srgb, var(--theme-borde) 25%, transparent);
margin: 2rem 0;
opacity: 0.5;
}
.docs-content :global(blockquote) {
border-left: 3px solid var(--theme-accent);
margin: 1rem 0;
padding: 0.5rem 1rem;
background-color: var(--theme-fill);
border-radius: 0 8px 8px 0;
}
.docs-content :global(blockquote p) {
margin: 0;
}
/* Table of contents style for nested lists */
.docs-content :global(ul ul) {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
/* Links */
.docs-content :global(a) {
color: var(--theme-accent);
text-decoration: underline;
text-decoration-color: transparent;
transition: text-decoration-color 0.15s ease;
}
.docs-content :global(a:hover) {
text-decoration-color: var(--theme-accent);
}
/* Heading anchor links */
.docs-content :global(.heading-anchor) {
position: relative;
}
.docs-content :global(.anchor-link) {
color: inherit;
text-decoration: none;
}
.docs-content :global(.anchor-link:hover) {
text-decoration: none;
}
.docs-content :global(.heading-anchor:hover .anchor-link::before) {
content: '#';
position: absolute;
left: -1.25em;
color: var(--theme-accent);
opacity: 0.6;
font-weight: 400;
}
.docs-content :global(h4.heading-anchor:hover .anchor-link::before) {
left: -1em;
font-size: 0.875em;
}
/* Responsive */
@media (max-width: 640px) {
.docs-header-content {
padding: 0.75rem 1rem;
}
.docs-title {
font-size: 1.25rem;
}
.docs-content :global(h1) {
font-size: 1.5rem;
scroll-margin-top: 120px;
}
.docs-content :global(h2) {
font-size: 1.25rem;
scroll-margin-top: 120px;
}
.docs-content :global(h3) {
font-size: 1.125rem;
scroll-margin-top: 120px;
}
.docs-content :global(h4) {
scroll-margin-top: 120px;
}
.docs-content :global(pre) {
font-size: 0.8rem;
padding: 0.75rem;
}
}
</style>
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import {
query,
results,
isLoading,
indexLoaded,
error,
selectedIndex,
showResults,
initIndex,
performSearch,
clearSearch,
navigateResults
} from '$lib/stores/searchStore';
let mounted = false;
let mode = 'archivo';
let searchFocused = false;
let searchVal = '';
let hDiario = false;
let hArchivo = false;
let hVis = null;
let hHist = null;
let hWidget = null;
let hGear = false;
let mobileSettingsOpen = false;
let searchInput;
let debounceTimer;
import * as d3 from 'd3';
// Estado
let streamData = $state([]);
let loading = $state(true);
let container = $state(null);
let width = $state(0);
let height = $state(500);
let tooltip = $state({ show: false, x: 0, y: 0, data: null });
// Drilldown
let currentLevel = $state('tipo'); // tipo -> clase -> cuenta -> subcuenta
let breadcrumb = $state([{ level: 'tipo', codigo: null, descripcion: 'Todos los ingresos' }]);
let currentParent = $state(null);
// Colores para las categorías
const colorScale = d3.scaleOrdinal()
.range([
'#4A8B6E', '#C9A751', '#8B6E9B', '#6B9FD4', '#D4846B',
'#5B8FA8', '#A8845B', '#7B9B6B', '#9B7B8B', '#6B8B9B',
'#B89B6B', '#6B7B9B', '#9B8B6B', '#7B8B7B', '#8B7B6B',
'#6B9B8B', '#9B6B7B', '#7B6B9B', '#8B9B6B', '#6B8B7B'
]);
// Cargar datos
async function loadData() {
loading = true;
try {
const response = await fetch('/stream1.csv');
const text = await response.text();
const parsed = d3.csvParse(text, d => ({
entidad: +d.entidad,
año: +d.año,
nivel: d.nivel,
codigo: +d.codigo,
descripcion: d.descripcion,
codigo_padre: d.codigo_padre ? +d.codigo_padre : null,
desc_padre: d.desc_padre || null,
devengado: +d.devengado
}));
// Filtrar solo entidad 0 (Todo el Estado)
streamData = parsed.filter(d => d.entidad === 0);
console.log('Stream data loaded:', streamData.length, 'rows');
} catch (err) {
console.error('Error loading data:', err);
}
loading = false;
}
onMount(() => {
setTimeout(() => { mounted = true; }, 80);
initIndex();
});
// Obtener datos filtrados para el nivel actual
function getFilteredData() {
let filtered = streamData.filter(d => d.nivel === currentLevel);
function toggleMobileSettings() {
mobileSettingsOpen = !mobileSettingsOpen;
}
if (currentParent !== null) {
filtered = filtered.filter(d => d.codigo_padre === currentParent);
}
$: isD = mode === 'diario';
$: accent = isD ? '#4A8B6E' : '#C9A751';
$: placeholder = isD
? 'Busca una entidad, un ministerio, un municipio...'
: 'Busca en 7 clasificadores: partidas, rubros, fuentes...';
$: examples = isD
? [
{ label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
{ label: 'Min. Educación', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' },
{ label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
]
: [
{ label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962' },
{ label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' },
{ label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' },
{ label: 'Consultorías', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' },
{ label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0' },
];
function switchMode(m) {
mode = m;
searchVal = '';
clearSearch();
if (searchInput) searchInput.value = '';
return filtered;
}
function handleBlur() {
setTimeout(() => { searchFocused = false; }, 200);
}
// Preparar datos para el streamgraph
function prepareStreamData() {
const filtered = getFilteredData();
if (!filtered.length) return { years: [], series: [], keys: [] };
// Agrupar por año
const byYear = d3.group(filtered, d => d.año);
const years = Array.from(byYear.keys()).sort((a, b) => a - b);
// Obtener todas las categorías únicas
const categories = [...new Set(filtered.map(d => d.codigo))];
// Crear estructura para stack
const stackData = years.map(year => {
const yearData = byYear.get(year) || [];
const row = { año: year };
categories.forEach(cat => {
const item = yearData.find(d => d.codigo === cat);
row[cat] = item ? item.devengado : 0;
});
return row;
});
function handleInput(e) {
const value = e.target.value;
searchVal = value;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
performSearch(value);
}, 250);
}
// Crear el stack
const stack = d3.stack()
.keys(categories)
.offset(d3.stackOffsetWiggle)
.order(d3.stackOrderInsideOut);
function handleKeydown(e) {
if (!$showResults) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
navigateResults('down', $results.length);
break;
case 'ArrowUp':
e.preventDefault();
navigateResults('up', $results.length);
break;
case 'Enter':
e.preventDefault();
if ($selectedIndex >= 0 && $results[$selectedIndex]) {
handleSelect($results[$selectedIndex]);
}
break;
case 'Escape':
clearSearch();
searchVal = '';
if (searchInput) searchInput.value = '';
searchInput?.blur();
break;
}
const series = stack(stackData);
// Agregar descripciones a las series
series.forEach(s => {
const item = filtered.find(d => d.codigo === +s.key);
s.descripcion = item ? item.descripcion : s.key;
});
return { years, series, keys: categories, stackData };
}
function handleSelect(item) {
if (item.tipo === 'entidad') {
goto(`/entidad/${item.codigo}`);
} else if (item.tipo === 'objeto_gasto') {
goto(`/objeto/${item.codigo}`);
} else if (item.tipo === 'rubro') {
goto(`/rubro/${item.codigo}`);
}
clearSearch();
searchVal = '';
if (searchInput) searchInput.value = '';
// Formatear moneda
function formatMoney(value) {
if (value >= 1e9) return `Bs ${(value / 1e9).toFixed(1)}B`;
if (value >= 1e6) return `Bs ${(value / 1e6).toFixed(1)}M`;
if (value >= 1e3) return `Bs ${(value / 1e3).toFixed(0)}K`;
return `Bs ${value.toFixed(0)}`;
}
function getTypeLabel(tipo) {
switch (tipo) {
case 'entidad': return 'ENTIDAD';
case 'objeto_gasto': return 'OBJETO';
case 'rubro': return 'RUBRO';
default: return tipo?.toUpperCase() || '';
// Manejar clic para drilldown
function handleClick(codigo, descripcion) {
const levels = ['tipo', 'clase', 'cuenta', 'subcuenta'];
const currentIndex = levels.indexOf(currentLevel);
if (currentIndex < levels.length - 1) {
const nextLevel = levels[currentIndex + 1];
// Verificar si hay datos en el siguiente nivel
const hasChildren = streamData.some(
d => d.nivel === nextLevel && d.codigo_padre === codigo
);
if (hasChildren) {
currentParent = codigo;
currentLevel = nextLevel;
breadcrumb = [...breadcrumb, { level: nextLevel, codigo, descripcion }];
}
}
}
function getTypeColor(tipo) {
switch (tipo) {
case 'entidad': return '#4A8B6E';
case 'objeto_gasto': return '#C9A751';
case 'rubro': return '#8B6E9B';
default: return '#6B6860';
// Navegar con breadcrumb
function navigateTo(index) {
if (index === 0) {
currentLevel = 'tipo';
currentParent = null;
breadcrumb = [{ level: 'tipo', codigo: null, descripcion: 'Todos los ingresos' }];
} else {
const target = breadcrumb[index];
currentLevel = target.level;
currentParent = index > 0 ? breadcrumb[index - 1].codigo : null;
// Si navegamos a un nivel intermedio, necesitamos el padre correcto
if (index > 0) {
currentParent = breadcrumb[index].codigo;
// Buscar el nivel siguiente
const levels = ['tipo', 'clase', 'cuenta', 'subcuenta'];
const nextLevelIndex = levels.indexOf(target.level) + 1;
if (nextLevelIndex < levels.length) {
currentLevel = levels[nextLevelIndex];
}
}
breadcrumb = breadcrumb.slice(0, index + 1);
}
}
const WIDGETS_INGRESOS = [
{ id: 'rubros', label: 'tipos de ingreso', d: 'Impuestos, regalías, ventas, créditos', t: 'Recursos por rubros', n: '324', href: '/clasificadores/rubros' },
{ id: 'fuentes', label: 'fuentes de financiamiento', d: 'TGN, crédito externo, donaciones', t: 'Fuentes', n: '22', href: '/clasificadores/fuente' },
{ id: 'organismos', label: 'organismos financiadores', d: 'BID, Banco Mundial, cooperación', t: 'Organismos', n: '228', href: '/clasificadores/organismos' },
];
const WIDGETS_GASTOS_QUE = [
{ id: 'objeto', label: 'objetos de gasto', d: 'Sueldos, consultorías, materiales', t: 'Objeto del gasto', n: '324', href: '/clasificadores/objeto-gasto' },
{ id: 'sector', label: 'sectores económicos', d: 'Industria, construcción, servicios', t: 'Sectores', n: '488', href: '/clasificadores/sectores' },
];
const WIDGETS_GASTOS_BOTTOM = [
{ id: 'finalidad', label: 'finalidades y funciones', d: 'Educación, salud, defensa', t: 'Finalidad', n: '132', href: '/clasificadores/funcional' },
{ id: 'donde', label: 'ubicaciones geográficas', d: 'Departamentos, municipios', t: 'Geográfico', n: '352', href: '/clasificadores/geografico' },
];
const HISTORIAS = [
{ 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 },
{ color: '#C44B3F', title: 'Salud vs. Defensa: 20 años', desc: 'Cómo se redistribuyó el presupuesto entre sectores.', tag: 'COMPARATIVA', tall: false },
{ 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 },
{ 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 },
];
const VISUALIZACIONES = [
{ 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' },
{ color: '#C9A751', title: 'Treemap del presupuesto 2025', desc: 'Explora cada peso del presupuesto actual.', type: 'Treemap' },
{ color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa' },
];
/* Reveal action */
let revealStates = {};
function reveal(node, { id, delay = 0 }) {
revealStates[id] = false;
revealStates = revealStates;
const obs = new IntersectionObserver(([e]) => {
if (e.isIntersecting) {
setTimeout(() => { revealStates[id] = true; revealStates = revealStates; }, delay);
obs.disconnect();
// Efecto para observar tamaño
$effect(() => {
if (!container) return;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
width = entry.contentRect.width;
}
}, { threshold: 0.08 });
obs.observe(node);
return { destroy() { obs.disconnect(); } };
}
});
/* Section navigation */
const SECTIONS_NAV = [
{ id: 'hero', label: 'Inicio' },
{ id: 'datos', label: 'Datos' },
{ id: 'clasificadores', label: 'Clasificadores' },
{ id: 'historias', label: 'Historias' },
{ id: 'visualizaciones', label: 'Visualizaciones' },
{ id: 'manifiesto', label: 'Manifiesto' },
{ id: 'descargas', label: 'Descargas' },
];
let currentSection = 'hero';
observer.observe(container);
return () => observer.disconnect();
});
onMount(() => {
const sections = document.querySelectorAll('[data-section]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
currentSection = entry.target.dataset.section;
}
});
}, { threshold: 0.5 });
sections.forEach(section => observer.observe(section));
return () => observer.disconnect();
loadData();
});
function scrollToSection(id) {
const el = document.querySelector(`[data-section="${id}"]`);
if (el) el.scrollIntoView({ behavior: 'smooth' });
}
</script>
// Datos preparados reactivos
let prepared = $derived(prepareStreamData());
<div class="page" class:mounted>
// Escalas reactivas
let xScale = $derived(
d3.scaleLinear()
.domain(d3.extent(prepared.years))
.range([60, width - 20])
);
<!-- Section Navigation Dots -->
<nav class="section-dots">
{#each SECTIONS_NAV as s}
let yExtent = $derived(() => {
if (!prepared.series.length) return [0, 0];
const allValues = prepared.series.flatMap(s => s.flatMap(d => [d[0], d[1]]));
return d3.extent(allValues);
});
let yScale = $derived(
d3.scaleLinear()
.domain(yExtent())
.range([height - 40, 40])
);
// Generador de área
let areaGenerator = $derived(
d3.area()
.x(d => xScale(d.data.año))
.y0(d => yScale(d[0]))
.y1(d => yScale(d[1]))
.curve(d3.curveBasis)
);
</script>
<svelte:head>
<title>Streamgraph - Ingresos del Estado</title>
</svelte:head>
<div class="page">
<header>
<h1>Ingresos del Estado Boliviano</h1>
<p class="subtitle">Streamgraph interactivo con drilldown · 2005-2025</p>
</header>
<!-- Breadcrumb -->
<nav class="breadcrumb">
{#each breadcrumb as crumb, i}
{#if i > 0}
<span class="separator">→</span>
{/if}
<button
class="section-dot"
class:section-dot-active={currentSection === s.id}
on:click={() => scrollToSection(s.id)}
aria-label={s.label}
class="crumb"
class:active={i === breadcrumb.length - 1}
onclick={() => navigateTo(i)}
disabled={i === breadcrumb.length - 1}
>
<span class="section-dot-label">{s.label}</span>
{crumb.descripcion}
</button>
{/each}
</nav>
<!-- ═══ LANDING SCREEN (NAV + HERO) ═══ -->
<section class="landing-screen" data-section="hero">
<nav>
<div class="nav-logo"><img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /></div>
</nav>
<!-- ═══ HERO ═══ -->
<section class="hero">
<div class="dot-pattern"></div>
<div class="hero-inner">
<!-- Bloque 1: Identidad -->
<div class="hero-identity">
<h1 class="hero-title anim" style="--d:200ms">Presupuesto <span class="gold">Abierto</span></h1>
<p class="hero-sub anim" style="--d:340ms">Información diaria e histórica de todo el Estado Boliviano.</p>
<div class="hero-stats anim" style="--d:420ms">
<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
</div>
</div>
<!-- Bloque 2: Búsqueda -->
<div class="hero-search anim" style="--d:520ms">
<div class="search-wrap">
<!-- Search bar unificado -->
<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}">
<!-- Toggle integrado (desktop) -->
<div class="toggle-bar toggle-desktop">
<button class="toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => switchMode('archivo')}>
<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">
<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"/>
</svg>
2005 a 2025
</button>
<button class="toggle-btn" class:toggle-active={mode === 'diario'} on:click={() => switchMode('diario')}>
<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">
<polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/>
</svg>
Diario
</button>
</div>
<div class="search-divider"></div>
<!-- Mobile: mode indicator + settings button -->
<button class="mobile-settings-btn" class:mobile-settings-open={mobileSettingsOpen} on:click={toggleMobileSettings}>
<span class="mobile-mode-indicator" style="background:{accent}"></span>
<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">
<circle cx="12" cy="12" r="3"/>
<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"/>
</svg>
</button>
<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">
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
</svg>
<div class="search-input-wrap">
<input
type="text"
bind:this={searchInput}
bind:value={searchVal}
on:input={handleInput}
on:keydown={handleKeydown}
on:focus={() => searchFocused = true}
on:blur={handleBlur}
disabled={!$indexLoaded}
/>
{#if !searchVal && !searchFocused}
<div class="search-placeholder">
<span class="blink-cursor" style="background:{accent}"></span>
<span class="placeholder-text placeholder-desktop">{$indexLoaded ? placeholder : 'Cargando...'}</span>
<span class="placeholder-text placeholder-mobile">{$indexLoaded ? (isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...') : 'Cargando...'}</span>
</div>
{/if}
{#if searchVal && !$isLoading}
<button class="search-clear" on:click={() => { clearSearch(); searchVal = ''; if (searchInput) searchInput.value = ''; }}>
<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>
</button>
{/if}
</div>
{#if !isD}
<div class="gear-btn gear-desktop" class:gear-hover={hGear} on:mouseenter={() => hGear = true} on:mouseleave={() => hGear = false} role="button" tabindex="0">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B8B5AD" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<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"/>
</svg>
</div>
{/if}
</div>
<!-- Search Results Dropdown -->
{#if $isLoading}
<div class="search-results">
<div class="search-loading">Buscando...</div>
</div>
{:else if searchFocused && $results.length > 0}
<div class="search-results">
{#each $results as item, i}
<button
type="button"
class="search-result-item"
class:result-selected={$selectedIndex === i}
on:click={() => handleSelect(item)}
>
<div class="result-main">
<span class="result-name">{item.nombre}</span>
{#if item.sigla}
<span class="result-sigla">({item.sigla})</span>
{/if}
</div>
<div class="result-meta">
{#if item.codigo}
<span class="result-code">{item.codigo}</span>
{/if}
<span class="result-type" style="color:{getTypeColor(item.tipo)}">{getTypeLabel(item.tipo)}</span>
</div>
</button>
{/each}
</div>
{:else if searchFocused && $query.length >= 2 && $results.length === 0}
<div class="search-results">
<div class="search-no-results">Sin resultados para "{$query}"</div>
</div>
{/if}
{#if $error}
<div class="search-error">{$error}</div>
{/if}
<!-- Mobile settings panel -->
<div class="mobile-settings-panel" class:mobile-settings-panel-open={mobileSettingsOpen}>
<div class="mobile-toggle-bar">
<button class="mobile-toggle-btn" class:toggle-active={mode === 'archivo'} on:click={() => { switchMode('archivo'); mobileSettingsOpen = false; }}>
<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">
<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"/>
</svg>
<span>Histórico</span>
<span class="mobile-toggle-sub">2005 — 2025</span>
</button>
<button class="mobile-toggle-btn" class:toggle-active={mode === 'diario'} on:click={() => { switchMode('diario'); mobileSettingsOpen = false; }}>
<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">
<polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/>
</svg>
<span>Diario</span>
<span class="mobile-toggle-sub">En vivo</span>
</button>
</div>
</div>
<!-- Pills -->
<div class="pills">
<span class="pills-label">Ej:</span>
{#each examples as ex}
<button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button>
{/each}
</div>
</div>
</div>
</div>
<!-- Scroll hint -->
<div class="scroll-hint anim" style="--d:700ms" class:scroll-hint-hidden={searchFocused && $results.length > 0}>
<svg width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="#B8B5AD" stroke-width="1.2" stroke-linecap="round">
<rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/>
<!-- Nivel actual -->
<div class="level-indicator">
Nivel: <strong>{currentLevel}</strong>
{#if currentLevel !== 'subcuenta'}
<span class="hint">· Clic en una banda para ver detalle</span>
{/if}
</div>
<!-- Contenedor del gráfico -->
<div class="chart-container" bind:this={container}>
{#if loading}
<div class="loading">Cargando datos...</div>
{:else if width > 0 && prepared.series.length > 0}
<svg {width} {height}>
<!-- Eje X -->
<g class="axis-x" transform="translate(0, {height - 35})">
{#each prepared.years.filter((_, i) => i % 2 === 0) as year}
<text x={xScale(year)} y="20" text-anchor="middle" class="axis-label">
{year}
</text>
<line
x1={xScale(year)}
y1="-{height - 80}"
x2={xScale(year)}
y2="0"
class="grid-line"
/>
{/each}
</g>
<!-- Streams -->
{#each prepared.series as serie, i}
{@const color = colorScale(serie.key)}
<path
d={areaGenerator(serie)}
fill={color}
fill-opacity="0.85"
stroke={color}
stroke-width="0.5"
class="stream-path"
class:clickable={currentLevel !== 'subcuenta'}
onmouseenter={(e) => {
tooltip = {
show: true,
x: e.clientX,
y: e.clientY,
data: {
codigo: serie.key,
descripcion: serie.descripcion,
color,
total: d3.sum(serie, d => d[1] - d[0])
}
};
}}
onmousemove={(e) => {
tooltip.x = e.clientX;
tooltip.y = e.clientY;
}}
onmouseleave={() => tooltip.show = false}
onclick={() => handleClick(+serie.key, serie.descripcion)}
/>
{/each}
</svg>
<span>Explora el portal</span>
</div>
</section>
</section>
<!-- ═══ PORTAL DIRECTORY ═══ -->
<section class="directory" data-section="datos">
<div class="directory-inner">
<!-- Left: Title -->
<div class="directory-left">
<div use:reveal={{ id: 'dir-head' }} class="reveal" class:revealed={revealStates['dir-head']}>
<p class="section-tag">LOS DATOS</p>
<h2 class="section-title">Dos formas<br/>de ver el<br/>presupuesto.</h2>
</div>
</div>
<!-- Right: Cards -->
<div class="directory-right">
<!-- Diario -->
<div use:reveal={{ id: 'door-d', delay: 60 }} class="reveal" class:revealed={revealStates['door-d']}>
<div class="door door-warm" class:door-hovered={hDiario} on:mouseenter={() => hDiario = true} on:mouseleave={() => hDiario = false} role="button" tabindex="0">
<div class="door-content">
<div class="door-header">
<div class="door-icon" style="opacity:{hDiario ? 1 : 0.4}">
<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>
</div>
<div class="door-badge">
<div class="pulse-dot-sm"></div>
<span class="door-badge-text" style="color:#4A8B6E">EN VIVO</span>
</div>
</div>
<h3 class="door-title door-title-dark">Diariamente</h3>
<div class="door-pills">
<span class="door-pill door-pill-green">¿Cuánto se ejecutó hoy?</span>
<span class="door-pill door-pill-green">¿Quién gastó más?</span>
<span class="door-pill door-pill-green">¿Qué cambió desde ayer?</span>
<span class="door-pill door-pill-green">Panorama del presupuesto en curso</span>
<span class="door-pill door-pill-green">Bitácora de movimientos</span>
<span class="door-pill door-pill-green">Descarga de datos diaria</span>
<span class="door-pill door-pill-green">Vista de todo el Estado</span>
<span class="door-pill door-pill-green">Vista por Institución</span>
</div>
</div>
<div class="door-footer">
<span class="door-meta">ACTUALIZADO HACE 2 HORAS</span>
<span class="door-cta" style="color:{hDiario ? '#4A8B6E' : '#6B6860'}">Explorar →</span>
</div>
</div>
</div>
<!-- Archivo -->
<div use:reveal={{ id: 'door-a', delay: 160 }} class="reveal" class:revealed={revealStates['door-a']}>
<div class="door door-warm" class:door-hovered={hArchivo} on:mouseenter={() => hArchivo = true} on:mouseleave={() => hArchivo = false} role="button" tabindex="0">
<div class="door-content">
<div class="door-header">
<div class="door-icon" style="opacity:{hArchivo ? 1 : 0.4}">
<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>
</div>
<span class="door-badge-text" style="color:#A68A3E">2005 — 2025</span>
</div>
<h3 class="door-title door-title-dark">Históricamente</h3>
<div class="door-pills">
<span class="door-pill door-pill-light">Tendencias</span>
<span class="door-pill door-pill-light">Tipos de ingreso</span>
<span class="door-pill door-pill-light">Tipos de gasto</span>
<span class="door-pill door-pill-light">Por institución</span>
<span class="door-pill door-pill-light">Por sector</span>
<span class="door-pill door-pill-light">Por autonomías</span>
<span class="door-pill door-pill-light">Comparar entidades</span>
</div>
</div>
<div class="door-footer">
<span class="door-meta">8M REGISTROS · 7 CLASIFICADORES</span>
<span class="door-cta" style="color:{hArchivo ? '#C9A751' : '#6B6860'}">Explorar →</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ═══ CLASIFICADORES ═══ -->
<section id="clasificadores" class="clasificadores" data-section="clasificadores">
<div class="clas-container">
<!-- Header -->
<div use:reveal={{ id: 'clas-head' }} class="reveal clas-header-section" class:revealed={revealStates['clas-head']}>
<div class="clas-header-row">
<div>
<p class="section-tag">CLASIFICADORES</p>
<h2 class="section-title">Elige cómo explorar el presupuesto</h2>
<p class="clas-subtitle">Navega por cada una de las 2,626 piezas de los 8 clasificadores.</p>
</div>
<a href="/clasificadores" class="clas-ver-todos">Ver todos →</a>
</div>
</div>
<!-- ¿Quién? -->
<div use:reveal={{ id: 'clas-quien', delay: 100 }} class="reveal" class:revealed={revealStates['clas-quien']}>
<div class="clas-group-card">
<div class="clas-group-header">
<div class="clas-group-icon" style="--c:#1C1C1A">
<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>
</div>
<div>
<div class="clas-group-label">¿QUIÉN?</div>
<div class="clas-group-sub">La entidad que gasta o recibe</div>
</div>
</div>
<a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} on:mouseenter={() => hWidget = 'quien'} on:mouseleave={() => hWidget = null}>
<div class="widget-left">
<div class="widget-headline">
<span class="widget-num-big">756</span>
<span class="widget-label">instituciones públicas</span>
</div>
<div class="widget-desc">Ministerios, gobernaciones, municipios, empresas públicas, universidades</div>
</div>
<div class="widget-right">
<span class="widget-tech">INSTITUCIONAL</span>
<span class="widget-arrow" class:arrow-show={hWidget === 'quien'}>→</span>
</div>
</a>
</div>
</div>
<!-- Grid: Ingresos + Gastos -->
<div class="clas-main-grid">
<!-- INGRESOS -->
<div use:reveal={{ id: 'clas-ing', delay: 200 }} class="reveal clas-col" class:revealed={revealStates['clas-ing']}>
<div class="clas-group-card">
<div class="clas-group-header">
<div class="clas-group-icon" style="--c:#C9A751">
<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>
</div>
<div>
<div class="clas-group-label" style="color:#C9A751">INGRESOS</div>
</div>
</div>
<div class="widgets-col">
{#each WIDGETS_INGRESOS as w}
<a href={w.href} class="widget-card widget-grow" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}>
<div class="widget-headline">
<span class="widget-num-big widget-num-gold">{w.n}</span>
<span class="widget-label">{w.label}</span>
</div>
<p class="widget-desc">{w.d}</p>
<div class="widget-footer">
<span class="widget-tech">{w.t}</span>
<span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span>
</div>
</a>
{/each}
</div>
</div>
</div>
<!-- GASTOS -->
<div use:reveal={{ id: 'clas-gas', delay: 300 }} class="reveal clas-col" class:revealed={revealStates['clas-gas']}>
<div class="clas-group-card">
<div class="clas-group-header">
<div class="clas-group-icon" style="--c:#4A8B6E">
<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>
</div>
<div>
<div class="clas-group-label" style="color:#4A8B6E">GASTOS</div>
</div>
</div>
<!-- Gastos content wrapper -->
<div class="gastos-content">
<!-- ¿En qué? sub-group -->
<div class="clas-subgroup">
<div class="clas-subgroup-header">
<div class="clas-subgroup-icon" style="--c:#4A8B6E">
<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>
</div>
<div>
<div class="clas-subgroup-label">¿EN QUÉ?</div>
</div>
</div>
<div class="widgets-row">
{#each WIDGETS_GASTOS_QUE as w}
<a href={w.href} class="widget-card widget-sm" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}>
<div class="widget-headline">
<span class="widget-num-big widget-num-green">{w.n}</span>
<span class="widget-label">{w.label}</span>
</div>
<p class="widget-desc widget-desc-sm">{w.d}</p>
<div class="widget-footer">
<span class="widget-tech">{w.t}</span>
<span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span>
</div>
</a>
{/each}
</div>
</div>
<!-- Bottom: Para qué + Dónde -->
<div class="widgets-row">
<!-- ¿Para qué? -->
<div class="clas-subgroup">
<div class="clas-subgroup-header">
<div class="clas-subgroup-icon" style="--c:#4A8B6E">
<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>
</div>
<div>
<div class="clas-subgroup-label">¿PARA QUÉ?</div>
</div>
</div>
<a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} on:mouseenter={() => hWidget = 'finalidad'} on:mouseleave={() => hWidget = null}>
<div class="widget-headline">
<span class="widget-num-big widget-num-green">132</span>
<span class="widget-label">finalidades y funciones</span>
</div>
<p class="widget-desc">Educación, salud, defensa</p>
<div class="widget-footer">
<span class="widget-tech">Finalidad</span>
<span class="widget-arrow" class:arrow-show={hWidget === 'finalidad'}>→</span>
</div>
</a>
</div>
<!-- ¿Dónde? -->
<div class="clas-subgroup">
<div class="clas-subgroup-header">
<div class="clas-subgroup-icon" style="--c:#4A8B6E">
<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>
</div>
<div>
<div class="clas-subgroup-label">¿DÓNDE?</div>
</div>
</div>
<a href="/clasificadores/geografico" class="widget-card widget-full" class:widget-hover={hWidget === 'donde'} on:mouseenter={() => hWidget = 'donde'} on:mouseleave={() => hWidget = null}>
<div class="widget-headline">
<span class="widget-num-big widget-num-green">352</span>
<span class="widget-label">ubicaciones geográficas</span>
</div>
<p class="widget-desc">Departamentos, municipios</p>
<div class="widget-footer">
<span class="widget-tech">Geográfico</span>
<span class="widget-arrow" class:arrow-show={hWidget === 'donde'}>→</span>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ═══ HISTORIAS ═══ -->
<section id="historias" class="historias" data-section="historias">
<div class="container">
<div use:reveal={{ id: 'hist-head' }} class="reveal" class:revealed={revealStates['hist-head']}>
<div class="hist-header">
<div>
<div class="hist-tag-row">
<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>
<p class="section-tag">HISTORIAS</p>
</div>
<h2 class="section-title">Los datos, contados.</h2>
<p class="hist-subtitle">Investigaciones y análisis narrativos sobre el presupuesto público.</p>
</div>
<a href="/historias" class="gold-link">Ver todas →</a>
</div>
</div>
<div class="hist-grid">
{#each HISTORIAS as h, i}
<div use:reveal={{ id: `hist-${i}`, delay: 60 + i * 80 }} class="reveal hist-cell" class:revealed={revealStates[`hist-${i}`]} class:hist-tall={h.tall}>
<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}">
<div class="hist-accent" style="background:{h.color};transform:scaleX({hHist === i ? 1 : 0})"></div>
<div class="hist-content">
<span class="hist-tag" style="color:{h.color}">{h.tag}</span>
<h4 class="hist-title">{h.title}</h4>
<p class="hist-desc">{h.desc}</p>
</div>
<div class="hist-cta" style="color:{hHist === i ? h.color : '#6B6860'}">
Leer historia <span class="hist-arrow" class:hist-arrow-moved={hHist === i}>→</span>
</div>
</div>
</div>
<!-- Leyenda -->
<div class="legend">
{#each prepared.series as serie}
{@const color = colorScale(serie.key)}
<button
class="legend-item"
onclick={() => handleClick(+serie.key, serie.descripcion)}
>
<span class="legend-color" style="background: {color}"></span>
<span class="legend-label">{serie.descripcion}</span>
</button>
{/each}
</div>
</div>
</section>
<!-- ═══ VISUALIZACIONES ═══ -->
<section id="visualizaciones" class="visualizaciones" data-section="visualizaciones">
<div class="container">
<div use:reveal={{ id: 'vis-head' }} class="reveal" class:revealed={revealStates['vis-head']}>
<div class="vis-header">
<div>
<div class="vis-tag-row">
<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>
<p class="section-tag">VISUALIZACIONES</p>
</div>
<h2 class="section-title">Explora los datos.</h2>
<p class="vis-subtitle">Herramientas interactivas para navegar el presupuesto.</p>
</div>
<a href="/visualizaciones" class="gold-link">Ver todas →</a>
</div>
{:else}
<div class="no-data">No hay datos para este nivel</div>
{/if}
</div>
<!-- Tooltip -->
{#if tooltip.show && tooltip.data}
<div
class="tooltip"
style="left: {tooltip.x + 15}px; top: {tooltip.y - 10}px;"
>
<div class="tooltip-header" style="border-color: {tooltip.data.color}">
<span class="tooltip-code">{tooltip.data.codigo}</span>
<span class="tooltip-desc">{tooltip.data.descripcion}</span>
</div>
<div class="vis-grid-simple">
{#each VISUALIZACIONES as v, i}
<div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}>
<div class="vis-card" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0">
<div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div>
<div>
<span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span>
<h4 class="vis-title">{v.title}</h4>
<p class="vis-desc">{v.desc}</p>
</div>
<div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}">
Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span>
</div>
</div>
</div>
{/each}
<div class="tooltip-value">
Total acumulado: {formatMoney(tooltip.data.total)}
</div>
{#if currentLevel !== 'subcuenta'}
<div class="tooltip-hint">Clic para ver detalle</div>
{/if}
</div>
</section>
<!-- ═══ MANIFIESTO ═══ -->
<section class="manifiesto" data-section="manifiesto">
<div class="dot-pattern"></div>
<div class="manifiesto-inner">
<div use:reveal={{ id: 'man' }} class="reveal" class:revealed={revealStates['man']}>
<p class="section-tag-light">POR QUÉ EXISTE ESTO</p>
<h2 class="manifiesto-title">El presupuesto público no es de los gobiernos.<br/><span class="gold">Es de todos.</span></h2>
</div>
</div>
</section>
<!-- ═══ DESCARGAS ═══ -->
<section id="descargas" class="descargas" data-section="descargas">
<div class="descargas-inner">
<div use:reveal={{ id: 'desc' }} class="reveal" class:revealed={revealStates['desc']}>
<h2 class="descargas-title">Descarga los datos.</h2>
<div class="descargas-formats">
<span class="format-pill">.csv</span>
<span class="format-pill">.parquet</span>
<span class="format-pill">.json</span>
</div>
<a href="/descargas" class="descargas-link">
<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>
Ir a página de descargas
</a>
</div>
</div>
</section>
<!-- ═══ FOOTER ═══ -->
<footer>
<div class="footer-inner">
<div class="footer-org">MINISTERIO DE ECONOMÍA Y FINANZAS PÚBLICAS</div>
<div class="footer-right">DATOS ABIERTOS · 2005 — 2026</div>
</div>
</footer>
{/if}
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap');
:global(*){margin:0;padding:0;box-sizing:border-box}
:global(html){width:100%;height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;scroll-behavior:smooth}
:global(body){width:100%;min-height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A}
:global(::selection){background:#C9A751;color:#1C1C1A}
:global(::-webkit-scrollbar){width:5px}
:global(::-webkit-scrollbar-track){background:#1C1C1A}
:global(::-webkit-scrollbar-thumb){background:#444;border-radius:3px}
:global(a){color:inherit;text-decoration:none}
@keyframes pulse{0%,100%{opacity:0.35}50%{opacity:1}}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
@keyframes scrollHint{0%,100%{transform:translateY(0);opacity:0.6}50%{transform:translateY(5px);opacity:1}}
@keyframes heartbeat{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:48}}
.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}
/* Section Navigation Dots */
.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}
.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}
.section-dot:hover{border-color:rgba(100,100,100,0.6);background:rgba(150,150,150,0.4);transform:scale(1.2)}
.section-dot-active{background:#C9A751;border-color:#C9A751}
.section-dot-active:hover{transform:scale(1.2)}
.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}
.section-dot:hover .section-dot-label{opacity:1}
.gold{color:#C9A751}
.container{max-width:1060px;margin:0 auto}
/* Reveal */
.reveal{opacity:0;transform:translateY(28px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)}
.revealed{opacity:1!important;transform:translateY(0)!important}
.anim{opacity:0;transform:translateY(18px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1);transition-delay:var(--d,0ms)}
.mounted .anim{opacity:1;transform:translateY(0)}
/* ── NAV ── */
nav{background:#1C1C1A;padding:12px 48px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.nav-left{display:flex;align-items:center}
.nav-right{display:flex;align-items:center;gap:24px}
.nav-link{font-family:var(--sans);font-size:14px;color:#B8B5AD;letter-spacing:0.01em;font-weight:400;transition:color 0.2s}
.nav-link:hover{color:#F5F0E8}
.nav-link-icon{display:flex;align-items:center;gap:5px}
.heartbeat-line{animation:heartbeat 3s ease-in-out infinite}
.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}
.date-label{color:#9B9890}
.date-sep{margin:0 3px;opacity:0.3}
.pulse-dot{width:5px;height:5px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite}
.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)}
.nav-sep{width:1px;height:16px;background:#2A2A27}
.nav-logo{height:88px;display:flex;align-items:center;flex-shrink:0}
.nav-logo img{height:100%;width:auto;object-fit:contain}
/* ── HERO ── */
.landing-screen{height:100vh;background:#1C1C1A;display:flex;flex-direction:column;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden}
.hero{background:#1C1C1A;color:#F5F0E8;flex:1;display:flex;flex-direction:column;justify-content:center;padding:40px 48px 32px;position:relative;overflow:hidden}
.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}
.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}
.hero-inner{max-width:1060px;margin:0 auto;width:100%;position:relative;margin-top:-80px;text-align:center}
.hero-identity{margin-bottom:20px}
.hero-search{display:flex;justify-content:center;position:relative;z-index:50}
.hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD}
.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}
.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}
.hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1}
.stat-num{color:#F5F0E8}
.stat-dot{margin:0 10px;opacity:0.3}
/* Search */
.search-wrap{max-width:860px;width:100%;margin:0 auto;position:relative;z-index:9999}
.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}
.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)}
.search-has-results{border-radius:16px 16px 0 0}
.toggle-bar{display:flex;border-radius:10px;padding:2px;background:rgba(0,0,0,0.2);flex-shrink:0}
.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}
.toggle-btn svg{display:block;flex-shrink:0}
.toggle-btn:hover{color:#B8B5AD}
.toggle-active{background:rgba(255,255,255,0.1);font-weight:600;color:#F5F0E8}
.search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0}
.search-icon{flex-shrink:0;transition:stroke 0.3s}
.search-input-wrap{flex:1;position:relative;display:flex;align-items:center}
.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}
.search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none}
.blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1.1s step-end infinite;border-radius:1px;flex-shrink:0}
.placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD}
.placeholder-mobile{display:none}
.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}
.gear-hover{background:rgba(255,255,255,0.12)}
/* Mobile settings button (hidden on desktop) */
.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}
.mobile-settings-btn:hover,.mobile-settings-open{background:rgba(255,255,255,0.12)}
.mobile-mode-indicator{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%}
/* Mobile settings panel (hidden on desktop) */
.mobile-settings-panel{display:none;overflow:hidden;max-height:0;opacity:0;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)}
.mobile-settings-panel-open{max-height:200px;opacity:1;margin-top:12px}
.mobile-toggle-bar{display:flex;gap:10px}
.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}
.mobile-toggle-btn.toggle-active{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.15)}
.mobile-toggle-btn span{font-family:var(--sans);font-size:14px;font-weight:600;color:#F5F0E8}
.mobile-toggle-btn svg{margin-bottom:4px}
.mobile-toggle-sub{font-size:11px!important;font-weight:400!important;color:#9B9890!important;font-family:var(--mono)!important;letter-spacing:0.05em}
/* Search clear button */
.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}
.search-clear:hover{color:#F5F0E8}
/* Search Results Dropdown */
.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}
.search-loading,.search-no-results{padding:16px 20px;font-family:var(--sans);font-size:14px;color:#9B9890;text-align:center}
.search-error{margin-top:8px;font-family:var(--sans);font-size:13px;color:#C44B3F;text-align:center}
.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}
.search-result-item:first-child{border-top:none}
.search-result-item:hover,.result-selected{background:rgba(255,255,255,0.06)}
.result-main{display:flex;align-items:center;gap:8px;flex:1;min-width:0}
.result-name{font-family:var(--sans);font-size:14px;color:#F5F0E8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.result-sigla{font-family:var(--sans);font-size:12px;color:#9B9890;flex-shrink:0}
.result-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:12px}
.result-code{font-family:var(--mono);font-size:10px;color:#9B9890;background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px}
.result-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;font-weight:600}
.pills{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;align-items:center;justify-content:center}
.pills-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#B8B5AD;margin-right:2px}
.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}
.pill::before{content:'';position:absolute;inset:0;background:rgba(255,255,255,0.12);opacity:0;transition:opacity 0.25s}
.pill:hover::before{opacity:1}
.pill:hover{transform:translateY(-1px)}
.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}
.scroll-hint-hidden{opacity:0;pointer-events:none}
.scroll-hint span{font-family:var(--mono);font-size:10px;letter-spacing:0.2em;color:#B8B5AD;opacity:0.7}
/* ── DIRECTORY ── */
.directory{background:#f5f5f7;min-height:100vh;display:flex;align-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:48px}
.directory-inner{display:grid;grid-template-columns:280px 1fr;gap:48px;width:100%;max-width:1200px;margin:0 auto;align-items:center}
.directory-left{}
.directory-right{display:flex;flex-direction:column;gap:20px}
.section-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#6B6860;margin-bottom:10px}
.section-tag-light{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#B8B5AD}
.section-title{font-size:clamp(40px,4.5vw,56px);font-weight:900;letter-spacing:-0.025em;line-height:1.05;color:#1C1C1A}
.section-title-light{font-size:clamp(32px,4vw,46px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8}
/* Door cards */
.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}
.door-hovered{transform:translateY(-4px)}
.door-dark{background:#1C1C1A;color:#F5F0E8;box-shadow:none}
.door-dark.door-hovered{box-shadow:0 24px 64px rgba(0,0,0,0.2)}
.door-warm{background:#ffffff;color:#1C1C1A;box-shadow:none}
.door-warm.door-hovered{box-shadow:0 24px 64px rgba(201,167,81,0.12)}
.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)}
.door-content{position:relative}
.door-header{display:flex;align-items:center;gap:10px;margin-bottom:16px}
.door-icon{transition:opacity 0.3s}
.door-badge{display:flex;align-items:center;gap:6px}
.door-badge-text{font-family:var(--mono);font-size:11px;letter-spacing:0.16em;font-weight:500}
.door-title{font-size:clamp(28px,3vw,38px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8}
.door-title-dark{color:#1C1C1A}
.door-desc{font-family:var(--sans);font-size:17px;color:#6B6860;line-height:1.55;margin-top:10px;max-width:480px}
.door-desc-light{color:#9B9890}
.door-pills{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.door-pill{font-family:var(--sans);font-size:16px;padding:10px 16px;border-radius:8px;line-height:1.3}
.door-pill-dark{background:rgba(74,139,110,0.12);color:#8ac4a8;font-weight:500}
.door-pill-light{background:rgba(166,138,62,0.1);color:#a68a3e;font-weight:500}
.door-pill-green{background:rgba(74,139,110,0.1);color:#3d7a5e;font-weight:500}
.door-footer{position:relative;display:flex;justify-content:space-between;align-items:center;margin-top:24px}
.door-meta{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#4A4843}
.door-meta-light{color:#B8B5AD}
.door-cta{font-family:var(--sans);font-size:16px;font-weight:400;transition:color 0.3s}
/* ── CLASIFICADORES ── */
.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}
.clas-container{max-width:1100px;margin:0 auto;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column}
.clas-header-section{margin-bottom:24px;flex-shrink:0}
.clas-header-row{display:flex;justify-content:space-between;align-items:flex-end}
.clas-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px}
.clas-ver-todos{font-family:var(--sans);font-size:15px;font-weight:500;color:#1C1C1A;transition:color 0.2s}
.clas-ver-todos:hover{color:#4A8B6E}
/* Group cards */
.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}
/* Parent reacts to child hover */
.clas-group-card:has(.widget-card:hover){background:#efefef;border-color:rgba(0,0,0,0.06)}
.clas-group-card:has(.widget-card:hover) > .clas-group-header .clas-group-label{transform:scale(1.05);transform-origin:left}
/* Parent hover highlights children */
.clas-group-card:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)}
.clas-group-card:hover .clas-subgroup{background:#f5f5f7}
.gastos-content{display:flex;flex-direction:column;flex:1;gap:10px}
.gastos-content .clas-subgroup{flex:1;margin-bottom:0}
.gastos-content > .widgets-row{flex:1}
.clas-group-header{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.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}
.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)}
.clas-group-sub{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px}
/* Subgroup */
.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}
/* Subgroup parent reacts to child hover */
.clas-subgroup:has(.widget-card:hover){background:#f3f3f5;border-color:rgba(0,0,0,0.05)}
.clas-subgroup:has(.widget-card:hover) > .clas-subgroup-header .clas-subgroup-label{transform:scale(1.05);transform-origin:left}
/* Subgroup hover highlights children */
.clas-subgroup:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)}
.clas-subgroup .widgets-row{flex:1}
.clas-subgroup-header{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.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}
.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)}
.clas-subgroup-tag{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px}
/* Main grid */
.clas-main-grid{display:grid;grid-template-columns:1fr 2fr;gap:12px;align-items:stretch;flex:1;min-height:0}
.clas-col{display:flex;flex-direction:column}
.clas-col .clas-group-card{flex:1}
/* Widget cards */
.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)}
.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)}
.widget-h{display:flex;justify-content:space-between;align-items:center}
.widget-left{flex:1}
.widget-right{display:flex;align-items:center;gap:12px;flex-shrink:0}
.widget-grow{flex:1}
.widget-sm{padding:12px}
.widget-full{flex:1}
.widgets-col{display:flex;flex-direction:column;gap:8px;flex:1}
.widgets-col .widget-card{flex:1}
.widgets-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;flex:1}
.widgets-row .widget-card{display:flex;flex-direction:column;justify-content:space-between}
.widget-headline{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}
.widget-num-big{font-family:var(--mono);font-size:24px;font-weight:700;line-height:1}
.widget-num-gold{color:#a68a3e}
.widget-num-green{color:#4A8B6E}
.widget-label{font-family:var(--sans);font-size:14px;font-weight:700;color:#1C1C1A;line-height:1.2}
.widget-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.4;font-weight:400}
.widget-desc-sm{font-size:13px}
.widget-footer{display:flex;justify-content:space-between;align-items:center;margin-top:8px}
.widget-tech{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;color:#9B9890;text-transform:uppercase}
.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)}
.arrow-show{color:#1C1C1A;transform:translateX(0)}
/* ── HISTORIAS ── */
.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}
.historias .container{width:100%;max-width:1060px;margin:0 auto}
.hist-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px}
.hist-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.hist-tag-row svg{stroke:#9B9890}
.hist-tag-row .section-tag{color:#9B9890}
.historias .section-title{color:#F5F0E8}
.hist-subtitle{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:10px}
.hist-grid{display:grid;grid-template-columns:repeat(3, 1fr);grid-auto-rows:minmax(120px, auto);gap:16px}
.hist-cell{grid-row:span 1}
.hist-tall{grid-row:span 2}
.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}
.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)}
.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)}
.hist-content{flex:1}
.hist-tag{font-family:var(--mono);font-size:10px;letter-spacing:0.14em;font-weight:500}
.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}
.hist-tall .hist-title{font-size:22px}
.hist-desc{font-family:var(--sans);font-size:14px;color:#9B9890;line-height:1.5;margin-top:10px}
.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}
.hist-arrow{transition:transform 0.3s;display:inline-block}
.hist-arrow-moved{transform:translateX(3px)}
/* ── VISUALIZACIONES ── */
.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}
.visualizaciones .container{width:100%;max-width:1060px;margin:0 auto}
.vis-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px}
.vis-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.vis-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px}
.gold-link{font-family:var(--sans);font-size:15px;color:#C9A751;font-weight:600}
.vis-grid-simple{display:grid;grid-template-columns:repeat(3, 1fr);gap:16px}
.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}
.vis-card-hovered{transform:translateY(-2px);box-shadow:0 16px 40px rgba(0,0,0,0.05)}
.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)}
.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}
.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}
.vis-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.5;margin-top:10px}
.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}
.vis-arrow{transition:transform 0.3s;display:inline-block}
.vis-arrow-moved{transform:translateX(3px)}
/* ── MANIFIESTO ── */
.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}
.manifiesto-inner{max-width:640px;width:100%;margin:0 auto;position:relative;text-align:center}
.manifiesto-title{font-size:clamp(36px,4.5vw,56px);font-weight:900;line-height:1.08;letter-spacing:-0.025em;margin-top:18px}
.manifiesto-text{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:20px;line-height:1.7}
/* ── DESCARGAS ── */
.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}
.descargas-inner{max-width:640px;width:100%;margin:0 auto;text-align:center}
.descargas-title{font-size:clamp(32px,4vw,48px);font-weight:900;line-height:1.1;letter-spacing:-0.02em;color:#F5F0E8}
.descargas-formats{display:flex;justify-content:center;gap:12px;margin-top:24px}
.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}
.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}
.descargas-link:hover{color:#d4b45a;gap:10px}
/* ── FOOTER ── */
footer{background:#1C1C1A;padding:40px 48px;border-top:1px solid #2A2A27}
.footer-inner{max-width:1060px;margin:0 auto;display:flex;justify-content:space-between;align-items:center}
.footer-org{font-family:var(--mono);font-size:12px;letter-spacing:0.12em;color:#6B6860}
.footer-unit{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:3px}
.footer-right{font-family:var(--mono);font-size:12px;letter-spacing:0.1em;color:#6B6860}
/* ══════════════════════════════════════════════════════════════════════════
RESPONSIVE - TABLET (768px)
══════════════════════════════════════════════════════════════════════════ */
@media(max-width:768px){
.page{scroll-snap-type:y proximity}
.landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal}
/* Section dots - bottom horizontal bar */
.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}
.section-dot{width:8px;height:8px}
.section-dot-label{display:none}
/* Nav */
nav{padding:12px 24px}
.nav-logo{height:60px}
/* Hero */
.hero{padding:32px 24px}
.hero-inner{margin-top:0}
.hero-title{font-size:clamp(36px,8vw,48px)}
.hero-stats{font-size:11px;line-height:1.6}
.stat-dot{margin:0 6px}
/* Search bar - mobile compact */
.search-bar{flex-wrap:nowrap;padding:8px 12px;gap:8px}
.toggle-desktop{display:none}
.search-divider{display:none}
.gear-desktop{display:none}
.mobile-settings-btn{display:flex}
.mobile-settings-panel{display:block}
.search-icon{margin-left:0}
.search-input-wrap{order:1}
.search-input-wrap input{font-size:15px;padding:10px 8px}
.placeholder-desktop{display:none}
.placeholder-mobile{display:inline;font-size:14px}
/* Search results mobile */
.search-results{max-height:260px;border-radius:0 0 12px 12px}
.search-result-item{padding:12px 16px}
.result-name{font-size:13px}
.result-meta{flex-direction:column;align-items:flex-end;gap:4px}
.result-code{font-size:9px}
.result-type{font-size:8px}
.pills{gap:6px}
.pill{font-size:11px;padding:4px 10px}
.scroll-hint{bottom:20px}
/* Directory */
.directory{padding:48px 24px}
.directory-inner{grid-template-columns:1fr;gap:32px}
.directory-left{text-align:center}
.section-title{font-size:clamp(32px,7vw,42px)}
.door{padding:28px 24px 24px;min-height:auto}
.door-title{font-size:clamp(24px,5vw,32px)}
.door-pills{gap:6px}
.door-pill{font-size:14px;padding:8px 12px}
.door-meta{font-size:10px}
/* Clasificadores */
.clasificadores{padding:32px 20px;height:auto;min-height:100vh;overflow-y:auto}
.clas-container{max-height:none}
.clas-header-row{flex-direction:column;align-items:flex-start;gap:12px}
.clas-ver-todos{align-self:flex-start}
.clas-main-grid{grid-template-columns:1fr;gap:12px}
.widgets-row{grid-template-columns:1fr}
.widget-h{flex-direction:column;align-items:flex-start;gap:12px}
.widget-right{width:100%;justify-content:space-between}
.widget-num-big{font-size:22px}
.clas-group-icon,.clas-subgroup-icon{width:28px;height:28px}
/* Historias */
.historias{padding:60px 24px}
.hist-header{flex-direction:column;align-items:flex-start;gap:16px}
.hist-grid{grid-template-columns:1fr 1fr;grid-auto-rows:auto;gap:12px}
.hist-tall{grid-row:span 1}
.hist-card{padding:20px 18px 18px}
.hist-title{font-size:16px}
.hist-tall .hist-title{font-size:18px}
.hist-desc{font-size:13px}
/* Visualizaciones */
.visualizaciones{padding:60px 24px}
.vis-header{flex-direction:column;align-items:flex-start;gap:16px}
.vis-grid-simple{grid-template-columns:1fr 1fr;gap:12px}
.vis-card{padding:20px 18px 18px}
.vis-title{font-size:16px}
/* Manifiesto */
.manifiesto{padding:60px 24px}
.manifiesto-title{font-size:clamp(28px,6vw,40px)}
/* Descargas */
.descargas{padding:60px 24px}
.descargas-title{font-size:clamp(28px,6vw,40px)}
.descargas-formats{gap:8px}
.format-pill{font-size:12px;padding:6px 12px}
/* Footer */
footer{padding:32px 24px}
.footer-inner{flex-direction:column;gap:12px;text-align:center}
.footer-org,.footer-right{font-size:10px}
.page {
min-height: 100vh;
background: #1a1a1a;
color: #f5f5f5;
padding: 2rem;
font-family: system-ui, -apple-system, sans-serif;
}
header {
text-align: center;
margin-bottom: 2rem;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin: 0;
color: #f5f5f5;
}
.subtitle {
color: #888;
margin-top: 0.5rem;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.separator {
color: #555;
}
.crumb {
background: #2a2a2a;
border: 1px solid #333;
color: #aaa;
padding: 0.4rem 0.8rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.crumb:hover:not(:disabled) {
background: #333;
color: #fff;
}
.crumb.active {
background: #C9A751;
color: #1a1a1a;
font-weight: 600;
cursor: default;
}
.crumb:disabled {
cursor: default;
}
.level-indicator {
color: #666;
font-size: 0.85rem;
margin-bottom: 1rem;
}
.level-indicator strong {
color: #C9A751;
text-transform: uppercase;
}
.hint {
color: #555;
}
.chart-container {
background: #222;
border-radius: 12px;
padding: 1rem;
min-height: 500px;
position: relative;
}
.loading, .no-data {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
color: #666;
}
svg {
display: block;
}
.axis-label {
fill: #666;
font-size: 12px;
}
.grid-line {
stroke: #333;
stroke-width: 0.5;
stroke-dasharray: 2, 4;
}
.stream-path {
transition: fill-opacity 0.2s, transform 0.2s;
}
.stream-path:hover {
fill-opacity: 1;
}
.stream-path.clickable {
cursor: pointer;
}
.stream-path.clickable:hover {
filter: brightness(1.1);
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #333;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.4rem;
background: #2a2a2a;
border: 1px solid #333;
padding: 0.3rem 0.6rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.legend-item:hover {
background: #333;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-label {
font-size: 0.75rem;
color: #aaa;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tooltip {
position: fixed;
background: rgba(30, 30, 30, 0.95);
backdrop-filter: blur(8px);
border: 1px solid #444;
border-radius: 8px;
padding: 0.75rem 1rem;
pointer-events: none;
z-index: 1000;
max-width: 300px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.tooltip-header {
border-left: 3px solid;
padding-left: 0.5rem;
margin-bottom: 0.5rem;
}
.tooltip-code {
font-family: monospace;
font-size: 0.75rem;
color: #888;
display: block;
}
.tooltip-desc {
font-size: 0.9rem;
color: #f5f5f5;
font-weight: 500;
}
.tooltip-value {
font-size: 0.85rem;
color: #C9A751;
font-weight: 600;
}
/* ══════════════════════════════════════════════════════════════════════════
RESPONSIVE - MOBILE (480px)
══════════════════════════════════════════════════════════════════════════ */
@media(max-width:480px){
/* Section dots - bottom bar smaller */
.section-dots{bottom:16px;gap:8px;padding:10px 16px;border-radius:20px}
.section-dot{width:7px;height:7px}
/* Nav */
nav{padding:10px 16px}
.nav-logo{height:48px}
/* Hero */
.hero{padding:24px 16px}
.hero-title{font-size:clamp(32px,9vw,40px)}
.hero-sub{font-size:15px}
.hero-stats{font-size:10px;display:flex;flex-wrap:wrap;justify-content:center;gap:4px 0}
.stat-dot{margin:0 4px}
/* Search */
.search-wrap{padding:0}
.mobile-settings-btn{width:36px;height:36px}
.mobile-toggle-btn{padding:14px 10px}
.mobile-toggle-btn span{font-size:13px}
.search-results{max-height:220px}
.search-result-item{padding:10px 14px}
.result-name{font-size:12px}
.result-sigla{font-size:10px}
.pills-label{display:none}
.pill{font-size:10px;padding:4px 8px}
.scroll-hint{display:none}
/* Directory */
.directory{padding:40px 16px}
.directory-inner{gap:24px}
.section-tag{font-size:10px}
.section-title{font-size:clamp(28px,8vw,36px)}
.door{padding:24px 20px 20px;border-radius:18px}
.door-icon svg{width:22px;height:22px}
.door-title{font-size:clamp(22px,6vw,28px)}
.door-pill{font-size:13px;padding:7px 10px}
.door-footer{flex-direction:column;align-items:flex-start;gap:8px}
.door-meta{font-size:9px}
.door-cta{font-size:14px}
/* Clasificadores */
.clasificadores{padding:24px 16px}
.clas-header-section{margin-bottom:16px}
.section-title{font-size:clamp(24px,6vw,32px)}
.clas-subtitle{font-size:14px}
.clas-group-card{padding:12px;border-radius:14px;margin-bottom:10px}
.clas-subgroup{padding:10px;border-radius:10px}
.clas-group-header,.clas-subgroup-header{margin-bottom:8px}
.clas-group-label,.clas-subgroup-label{font-size:10px}
.clas-group-sub{font-size:12px}
.widget-card{padding:12px;border-radius:10px}
.widget-num-big{font-size:20px}
.widget-label{font-size:13px}
.widget-desc{font-size:12px}
.widget-tech{font-size:8px}
/* Historias */
.historias{padding:48px 16px}
.hist-subtitle{font-size:14px}
.hist-grid{grid-template-columns:1fr;gap:10px}
.hist-card{padding:18px 16px 16px;border-radius:14px}
.hist-tag{font-size:9px}
.hist-title{font-size:16px}
.hist-desc{font-size:12px;margin-top:8px}
.hist-cta{font-size:13px;margin-top:14px}
/* Visualizaciones */
.visualizaciones{padding:48px 16px}
.vis-subtitle{font-size:14px}
.vis-grid-simple{grid-template-columns:1fr;gap:10px}
.vis-card{padding:18px 16px 16px;border-radius:14px}
.vis-type{font-size:9px;padding:4px 8px}
.vis-title{font-size:16px}
.vis-desc{font-size:12px}
.vis-cta{font-size:13px;margin-top:14px}
/* Manifiesto */
.manifiesto{padding:48px 16px}
.section-tag-light{font-size:10px}
.manifiesto-title{font-size:clamp(24px,7vw,32px)}
/* Descargas */
.descargas{padding:48px 16px}
.descargas-title{font-size:clamp(24px,7vw,32px)}
.descargas-formats{flex-wrap:wrap;gap:6px}
.format-pill{font-size:11px;padding:5px 10px}
.descargas-link{font-size:14px;margin-top:24px}
/* Footer */
footer{padding:24px 16px}
.footer-org{font-size:9px;letter-spacing:0.08em}
.footer-right{font-size:9px}
.tooltip-hint {
font-size: 0.75rem;
color: #666;
margin-top: 0.3rem;
}
</style>
\ No newline at end of file
</style>
......
This diff could not be displayed because it is too large.