Rafael Lopez

descargas y diario

......@@ -1194,6 +1194,24 @@
</div>
</div>
<!-- Links directos -->
<div class="quick-links">
<a href="/vista-diaria" class="quick-link quick-link-diaria">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" 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>
Ver ejecución diaria
<span class="quick-link-arrow">→</span>
</a>
<a href="/descargas" class="quick-link quick-link-descargas">
<svg width="15" height="15" 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>
Descarga bases de datos
<span class="quick-link-arrow">→</span>
</a>
</div>
</div>
<!-- Contenedor de resultados (overlay absoluto) -->
......@@ -1531,6 +1549,7 @@
<!-- Scroll hint -->
</main>
<!-- Secciones removidas - el landing es solo el buscador -->
......@@ -2801,6 +2820,23 @@
:global(html:not(.dark)) .footer-org{color:#999}
:global(html:not(.dark)) .footer-right{color:#999}
/* ── Quick links (debajo de mode cards) ── */
/* ── Quick links (debajo de mode cards) ── */
.quick-links{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}
.quick-link{display:flex;align-items:center;gap:8px;font-family:var(--sans);font-size:16px;font-weight:600;letter-spacing:-0.01em;text-decoration:none;padding:14px 16px;border-radius:12px;transition:all 0.25s ease}
.page a.quick-link-diaria{color:#C2EDD9;background:rgba(74,139,110,0.18);border:none}
.page a.quick-link-diaria:hover{color:#E0F5EC;background:rgba(74,139,110,0.28)}
.page a.quick-link-descargas{color:#F5E6A8;background:rgba(201,167,81,0.18);border:none}
.page a.quick-link-descargas:hover{color:#FBF2D0;background:rgba(201,167,81,0.28)}
.quick-link svg{opacity:0.8;transition:opacity 0.25s;flex-shrink:0}
.quick-link:hover svg{opacity:1}
.quick-link-arrow{margin-left:auto;opacity:0.4;transition:all 0.25s;transform:translateX(-4px)}
.quick-link:hover .quick-link-arrow{opacity:0.8;transform:translateX(0)}
:global(html:not(.dark)) .page a.quick-link-diaria{color:#3A7A5E;background:rgba(74,139,110,0.14)}
:global(html:not(.dark)) .page a.quick-link-diaria:hover{color:#2D6B4F;background:rgba(74,139,110,0.22)}
:global(html:not(.dark)) .page a.quick-link-descargas{color:#8A7030;background:rgba(201,167,81,0.14)}
:global(html:not(.dark)) .page a.quick-link-descargas:hover{color:#7A6228;background:rgba(201,167,81,0.22)}
/* ══════════════════════════════════════════════════════════════════════════
RESPONSIVE - TABLET (768px)
══════════════════════════════════════════════════════════════════════════ */
......@@ -2808,6 +2844,7 @@
.page{scroll-snap-type:y proximity}
main[data-section="hero"]{height:auto;min-height:100vh}
main[data-section="hero"],.landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal}
.quick-links{grid-template-columns:1fr}
/* 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}
......
......@@ -11,13 +11,21 @@ export async function GET({ url }) {
timeline: `${API_BASE}/timeline`
};
const apiUrl = endpoints[tipo];
let apiUrl = endpoints[tipo];
if (!apiUrl) {
return new Response(JSON.stringify({ error: 'Invalid tipo' }), {
status: 400, headers: { 'Content-Type': 'application/json' }
});
}
// Pasar parámetros adicionales
const params = new URLSearchParams();
const entidad = url.searchParams.get('entidad');
const dias = url.searchParams.get('dias');
if (entidad) params.set('entidad', entidad);
if (dias) params.set('dias', dias);
if (params.toString()) apiUrl += `?${params.toString()}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
......
......@@ -4,9 +4,13 @@
const YEARS = Array.from({ length: 10 }, (_, i) => 2016 + i);
let mounted = $state(false);
let dataset = $state('gasto');
let datasetMasivo = $state('gasto');
// Sección 1: Por gestión
let datasetGestion = $state('gasto');
let formatoGestion = $state('csv');
// Sección 2: Por entidad
let datasetEntidad = $state('gasto');
let formatoEntidad = $state('csv');
let entities = $state([]);
let entitySearch = $state('');
......@@ -15,30 +19,27 @@
let selectedIdx = $state(-1);
let loadingEntities = $state(true);
let entityError = $state(false);
// Sección 3: Base completa
let datasetMasivo = $state('gasto');
// Sección 4: Ejecución semanal (próximamente)
// Sección 5: Desarrolladores
let copiedUrl = $state(null);
function normalizeText(val) {
return val.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
}
function buildUrl(partition, value, formato = 'csv') {
if (partition === 'completo') {
if (formato === 'parquet') {
return `${DOWNLOAD_BASE}${dataset}/ultimo/${dataset}.parquet`;
}
return `${DOWNLOAD_BASE}${dataset}/ultimo/${dataset}.zip`;
} else if (partition === 'gestion') {
if (formato === 'xlsx') {
return `${DOWNLOAD_BASE}${dataset}/gestiones/gestion_${value}_xlsx.zip`;
}
return `${DOWNLOAD_BASE}${dataset}/gestiones/gestion_${value}.zip`;
} else if (partition === 'entidad') {
if (formato === 'xlsx') {
return `${DOWNLOAD_BASE}${dataset}/entidades/entidad_${value}_xlsx.zip`;
}
return `${DOWNLOAD_BASE}${dataset}/entidades/entidad_${value}.zip`;
}
return `${DOWNLOAD_BASE}${dataset}/${partition}/${value}`;
function buildGestionUrl(year) {
const suffix = formatoGestion === 'xlsx' ? `_xlsx.zip` : `.zip`;
return `${DOWNLOAD_BASE}${datasetGestion}/gestiones/gestion_${year}${suffix}`;
}
function buildEntidadUrl(codigo) {
const suffix = formatoEntidad === 'xlsx' ? `_xlsx.zip` : `.zip`;
return `${DOWNLOAD_BASE}${datasetEntidad}/entidades/entidad_${codigo}${suffix}`;
}
function buildMasivoUrl(formato) {
......@@ -113,112 +114,125 @@
<p class="subtitle">Descarga los datos del presupuesto público boliviano en distintos formatos</p>
<!-- ═══════════════════════════════════════════ -->
<!-- SECCIÓN 1: Por gestión o entidad -->
<!-- SECCIÓN 1: Por gestión -->
<!-- ═══════════════════════════════════════════ -->
<section class="section">
<h2 class="section-title">Por gestión o entidad</h2>
<p class="section-desc">Descarga los datos segmentados por año fiscal o por entidad pública</p>
<!-- Toggle Gastos/Ingresos -->
<div class="control-row">
<span class="control-label">Base</span>
<div class="toggle-group">
<button class="toggle-btn" class:active={dataset === 'gasto'} onclick={() => { dataset = 'gasto'; selectedEntity = null; }}>Gastos</button>
<button class="toggle-btn" class:active={dataset === 'ingreso'} onclick={() => { dataset = 'ingreso'; selectedEntity = null; }}>Ingresos</button>
</div>
</div>
<!-- Por gestión -->
<div class="subsection">
<div class="subsection-header">
<h3 class="subsection-title">Por gestión</h3>
<div class="formato-toggle">
<span class="formato-label">Formato:</span>
<button class="fmt-btn" class:active={formatoGestion === 'csv'} onclick={() => formatoGestion = 'csv'}>CSV</button>
<button class="fmt-btn" class:active={formatoGestion === 'xlsx'} onclick={() => formatoGestion = 'xlsx'}>Excel</button>
<h2 class="section-title">Por gestión</h2>
<p class="section-desc">Datos segmentados por año fiscal</p>
<div class="controls-row">
<div class="control-row">
<span class="control-label">Base</span>
<div class="toggle-group">
<button class="toggle-btn" class:active={datasetGestion === 'gasto'} onclick={() => datasetGestion = 'gasto'}>Gastos</button>
<button class="toggle-btn" class:active={datasetGestion === 'ingreso'} onclick={() => datasetGestion = 'ingreso'}>Ingresos</button>
</div>
</div>
<div class="year-grid">
{#each YEARS as year}
<a href={buildUrl('gestion', year, formatoGestion)} download rel="external" class="year-link">
<span class="year-num">{year}</span>
<span class="year-fmt">.{formatoGestion === 'xlsx' ? 'xlsx' : 'csv'}</span>
</a>
{/each}
<div class="control-row">
<span class="control-label">Formato</span>
<div class="toggle-group">
<button class="toggle-btn" class:active={formatoGestion === 'csv'} onclick={() => formatoGestion = 'csv'}>CSV</button>
<button class="toggle-btn" class:active={formatoGestion === 'xlsx'} onclick={() => formatoGestion = 'xlsx'}>Excel</button>
</div>
</div>
</div>
<!-- Por entidad -->
<div class="subsection">
<div class="subsection-header">
<h3 class="subsection-title">Por entidad</h3>
<div class="formato-toggle">
<span class="formato-label">Formato:</span>
<button class="fmt-btn" class:active={formatoEntidad === 'csv'} onclick={() => formatoEntidad = 'csv'}>CSV</button>
<button class="fmt-btn" class:active={formatoEntidad === 'xlsx'} onclick={() => formatoEntidad = 'xlsx'}>Excel</button>
<div class="year-grid">
{#each YEARS as year}
<a href={buildGestionUrl(year)} download rel="external" class="year-link">
<span class="year-num">{year}</span>
<span class="year-fmt">.{formatoGestion === 'xlsx' ? 'xlsx' : 'csv'}</span>
</a>
{/each}
</div>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- SECCIÓN 2: Por entidad -->
<!-- ═══════════════════════════════════════════ -->
<section class="section">
<h2 class="section-title">Por entidad</h2>
<p class="section-desc">Datos históricos de una entidad pública específica</p>
<div class="controls-row">
<div class="control-row">
<span class="control-label">Base</span>
<div class="toggle-group">
<button class="toggle-btn" class:active={datasetEntidad === 'gasto'} onclick={() => { datasetEntidad = 'gasto'; selectedEntity = null; }}>Gastos</button>
<button class="toggle-btn" class:active={datasetEntidad === 'ingreso'} onclick={() => { datasetEntidad = 'ingreso'; selectedEntity = null; }}>Ingresos</button>
</div>
</div>
<div class="entity-search">
<input
type="text"
bind:value={entitySearch}
onfocus={() => { entityFocused = true; selectedEntity = null; }}
onblur={() => setTimeout(() => entityFocused = false, 150)}
onkeydown={handleKeydown}
placeholder="Buscar entidad..."
class="search-input"
/>
{#if loadingEntities}
<span class="search-status">Cargando entidades...</span>
{:else if entityError}
<span class="search-status error">No se pudieron cargar las entidades</span>
{/if}
{#if entityFocused && filteredEntities.length > 0}
<div class="search-dropdown">
{#each filteredEntities as entity, i}
<button
class="search-item"
class:active={selectedIdx === i}
onmouseenter={() => selectedIdx = i}
onclick={() => selectEntity(entity)}
>
<span class="item-name">{entity.desc_entidad}</span>
<span class="item-code">Código {entity.entidad}</span>
</button>
{/each}
</div>
{/if}
{#if entityFocused && entitySearch.length >= 2 && filteredEntities.length === 0 && !loadingEntities}
<span class="search-status error">Sin resultados</span>
{/if}
<div class="control-row">
<span class="control-label">Formato</span>
<div class="toggle-group">
<button class="toggle-btn" class:active={formatoEntidad === 'csv'} onclick={() => formatoEntidad = 'csv'}>CSV</button>
<button class="toggle-btn" class:active={formatoEntidad === 'xlsx'} onclick={() => formatoEntidad = 'xlsx'}>Excel</button>
</div>
</div>
</div>
{#if selectedEntity}
<div class="entity-result">
<span class="entity-name">{selectedEntity.desc_entidad}</span>
<a href={buildUrl('entidad', selectedEntity.entidad, formatoEntidad)} download rel="external" class="download-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Descargar .{formatoEntidad === 'xlsx' ? 'xlsx' : 'csv'}
</a>
<div class="entity-search">
<input
type="text"
bind:value={entitySearch}
onfocus={() => { entityFocused = true; selectedEntity = null; }}
onblur={() => setTimeout(() => entityFocused = false, 150)}
onkeydown={handleKeydown}
placeholder="Buscar entidad..."
class="search-input"
/>
{#if loadingEntities}
<span class="search-status">Cargando entidades...</span>
{:else if entityError}
<span class="search-status error">No se pudieron cargar las entidades</span>
{/if}
{#if entityFocused && filteredEntities.length > 0}
<div class="search-dropdown">
{#each filteredEntities as entity, i}
<button
class="search-item"
class:active={selectedIdx === i}
onmouseenter={() => selectedIdx = i}
onclick={() => selectEntity(entity)}
>
<span class="item-name">{entity.desc_entidad}</span>
<span class="item-code">Código {entity.entidad}</span>
</button>
{/each}
</div>
{/if}
{#if entityFocused && entitySearch.length >= 2 && filteredEntities.length === 0 && !loadingEntities}
<span class="search-status error">Sin resultados</span>
{/if}
</div>
{#if selectedEntity}
<div class="entity-result">
<span class="entity-name">{selectedEntity.desc_entidad}</span>
<a href={buildEntidadUrl(selectedEntity.entidad)} download rel="external" class="download-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Descargar .{formatoEntidad === 'xlsx' ? 'xlsx' : 'csv'}
</a>
</div>
{/if}
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- SECCIÓN 2: Base completa -->
<!-- SECCIÓN 3: Base completa -->
<!-- ═══════════════════════════════════════════ -->
<section class="section">
<h2 class="section-title">Base completa</h2>
<p class="section-desc">Toda la base histórica: todas las entidades y todos los años en un solo archivo</p>
<div class="control-row">
<span class="control-label">Base</span>
<div class="toggle-group">
<button class="toggle-btn" class:active={datasetMasivo === 'gasto'} onclick={() => datasetMasivo = 'gasto'}>Gastos</button>
<button class="toggle-btn" class:active={datasetMasivo === 'ingreso'} onclick={() => datasetMasivo = 'ingreso'}>Ingresos</button>
<div class="controls-row">
<div class="control-row">
<span class="control-label">Base</span>
<div class="toggle-group">
<button class="toggle-btn" class:active={datasetMasivo === 'gasto'} onclick={() => datasetMasivo = 'gasto'}>Gastos</button>
<button class="toggle-btn" class:active={datasetMasivo === 'ingreso'} onclick={() => datasetMasivo = 'ingreso'}>Ingresos</button>
</div>
</div>
</div>
......@@ -245,14 +259,13 @@
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- SECCIÓN 3: Ejecución semanal -->
<!-- SECCIÓN 4: Ejecución semanal -->
<!-- ═══════════════════════════════════════════ -->
<section class="section">
<h2 class="section-title">Ejecución semanal</h2>
<p class="section-desc">Reporte semanal de ejecución presupuestaria con datos agregados</p>
<p class="section-desc">Reporte semanal de ejecución presupuestaria</p>
<div class="semanal-downloads disabled">
<span class="semanal-badge">Próximamente</span>
<div class="masivo-downloads">
<span class="download-card download-card-disabled">
<div class="download-card-icon">
......@@ -277,7 +290,7 @@
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- SECCIÓN 4: Para desarrolladores -->
<!-- SECCIÓN 5: Para desarrolladores -->
<!-- ═══════════════════════════════════════════ -->
<section class="section section-last">
<h2 class="section-title">Para desarrolladores</h2>
......@@ -326,9 +339,8 @@
}
.title {
font-family: 'DM Serif Display', serif;
font-size: 2rem;
font-weight: 400;
font-weight: 700;
margin: 0 0 0.25rem;
letter-spacing: -0.02em;
}
......@@ -364,12 +376,18 @@
line-height: 1.5;
}
/* Control row */
/* Controls */
.controls-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1.25rem;
}
.control-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.control-label {
......@@ -415,65 +433,6 @@
color: var(--theme-titulo);
}
/* Subsections */
.subsection {
margin-bottom: 1.5rem;
}
.subsection:last-child { margin-bottom: 0; }
.subsection-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.subsection-title {
font-size: 0.9375rem;
font-weight: 600;
margin: 0;
}
/* Formato toggle */
.formato-toggle {
display: flex;
align-items: center;
gap: 0.375rem;
}
.formato-label {
font-size: 0.6875rem;
color: var(--theme-texto);
opacity: 0.5;
margin-right: 0.25rem;
}
.fmt-btn {
font-family: 'Qanelas', var(--font-sans);
font-size: 0.6875rem;
font-weight: 600;
padding: 0.3rem 0.75rem;
border: 1px solid var(--theme-borde);
border-radius: 6px;
background: transparent;
color: var(--theme-texto);
cursor: pointer;
transition: all 0.15s;
}
.fmt-btn:hover { border-color: var(--theme-accent); color: var(--theme-titulo); }
.fmt-btn.active {
background: var(--theme-titulo);
border-color: var(--theme-titulo);
color: var(--theme-body);
}
:global(.dark) .fmt-btn.active {
background: var(--theme-accent);
border-color: var(--theme-accent);
color: var(--theme-body);
}
/* Years */
.year-grid {
display: grid;
......@@ -777,10 +736,10 @@
@media (max-width: 640px) {
.layout { padding: 1rem 1rem 2rem; }
.title { font-size: 1.75rem; }
.controls-row { flex-direction: column; gap: 0.75rem; }
.year-grid { grid-template-columns: repeat(auto-fill, minmax(75px, 1fr)); }
.masivo-downloads { grid-template-columns: 1fr; }
.search-input, .search-dropdown, .entity-result { max-width: 100%; }
.subsection-header { flex-direction: column; align-items: flex-start; }
.entity-result { flex-direction: column; align-items: flex-start; }
}
</style>
......
......@@ -10,6 +10,8 @@
let timeline = $state([]);
let loading = $state(true);
let error = $state(null);
let fechaActualizacion = $state(null);
let fechaActualizacionRaw = $state('');
// Controles interactivos
let diasSeleccionados = $state('total');
......@@ -93,8 +95,99 @@
document.removeEventListener('mouseup', handleRangeMouseUp);
}
// Fecha narrativa
let fechaNarrativa = $derived.by(() => {
if (!fechaActualizacion) return '';
const dias = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
const meses = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
const dia = dias[fechaActualizacion.getDay()];
const num = fechaActualizacion.getDate();
const mes = meses[fechaActualizacion.getMonth()];
const anio = fechaActualizacion.getFullYear();
return `${dia} ${num} de ${mes} de ${anio}`;
});
let horaActualizacion = $derived.by(() => {
if (!fechaActualizacionRaw) return '';
// Extraer hora directamente del string ISO tal cual viene
const match = fechaActualizacionRaw.match(/T(\d{2}):(\d{2}):(\d{2})/);
if (!match) return '';
return `${match[1]}:${match[2]}:${match[3]}`;
});
// Búsqueda para timeline cards
let busquedaTimeline = $state('');
let searchResults = $state([]);
let searchLoading = $state(false);
let searchTimer = null;
let searchEntities = $state([]);
async function buscarEnAPI(query) {
if (!query || query.trim().length < 2) {
searchResults = [];
searchEntities = [];
return;
}
searchLoading = true;
try {
const params = new URLSearchParams({
q: query,
per_page: '10',
is_class: 'true',
class_: 'entidad'
});
const res = await fetch(`/api/search?${params}`);
if (!res.ok) throw new Error(`Error ${res.status}`);
const data = await res.json();
searchResults = data.hits || [];
// Para cada entidad encontrada, cargar sus programas del timeline
const entities = await Promise.all(searchResults.map(async (hit) => {
const doc = hit.document;
const meta = parseSearchMetadatos(doc.metadatos);
const codigo = meta.entidad || '';
let programas = [];
try {
const tlRes = await fetch(`/api/diario?tipo=timeline&entidad=${codigo}`);
if (tlRes.ok) {
const tlData = await tlRes.json();
programas = (Array.isArray(tlData) ? tlData : []).map(item => ({
programa: item.desc_programa || '',
actividad: item.desc_actividad_proyecto || '',
devengado: item.devengado || 0
})).sort((a, b) => b.devengado - a.devengado);
}
} catch { /* silencioso */ }
const total = programas.reduce((s, p) => s + p.devengado, 0);
return {
entidad: String(codigo),
entidad_desc_area: meta.entidad_desc_area || '',
entidad_desc_entidad: doc.texto || '',
total: total || doc.devengado || 0,
programas
};
}));
searchEntities = entities;
} catch (e) {
console.error('Error en búsqueda:', e);
searchResults = [];
searchEntities = [];
} finally {
searchLoading = false;
}
}
function handleSearchInput() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => buscarEnAPI(busquedaTimeline), 300);
}
function parseSearchMetadatos(meta) {
if (!meta) return {};
if (typeof meta === 'object') return meta;
try { return JSON.parse(meta); } catch { return {}; }
}
// Labels dinámicos para periodos de tiempo
function formatearFechaCorta(fecha) {
......@@ -130,11 +223,6 @@
function irASeccion(index) {
seccionActiva = index;
const elemento = document.getElementById(secciones[index].id);
if (elemento && vistaContainer) {
elemento.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Actualizar URL hash
history.replaceState(null, '', `#${secciones[index].id}`);
}
......@@ -192,7 +280,7 @@
// Colores del tema
const colores = {
base: '#363534',
claro: '#F3EAD8',
claro: '#4A8B6E',
rojo: '#DB281C',
amarillo: '#F4E410',
verde: '#007C38',
......@@ -208,7 +296,7 @@
function getChartColors() {
return {
barDefault: getCSSVar('--chart-bar-default') || '#e0e0e0',
barHighlight: getCSSVar('--chart-bar-highlight') || '#F3EAD8',
barHighlight: getCSSVar('--chart-bar-highlight') || '#4A8B6E',
barStroke: getCSSVar('--chart-bar-stroke') || '#b0b0b0',
barStrokeDefault: getCSSVar('--chart-bar-stroke-default') || 'white',
trackBg: getCSSVar('--chart-track-bg') || '#f5f5f5',
......@@ -250,11 +338,12 @@
try {
// Cargar todos los datos en paralelo desde API
const [tsData, entData, clasData, tlData] = await Promise.all([
const [tsData, entData, clasData, tlData, statusData] = await Promise.all([
cargarDiario('timeseries'),
cargarDiario('entidades'),
cargarDiario('clasificadores'),
cargarDiario('timeline')
cargarDiario('timeline'),
cargarDiario('status')
]);
// Los datos ya vienen como objetos
......@@ -262,6 +351,10 @@
ejecucionEntidades = entData;
ejecucionClasificadores = clasData;
timeline = tlData;
if (statusData?.fecha_actualizacion) {
fechaActualizacionRaw = statusData.fecha_actualizacion;
fechaActualizacion = new Date(statusData.fecha_actualizacion);
}
loading = false;
......@@ -285,13 +378,10 @@
// Datos por clasificador para las barras apiladas (sin agrupar, cada hijo es un segmento)
let datosPorClasificador = $derived.by(() => {
const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados);
const result = {};
clasificadores.forEach(({ key }) => {
const filtered = ejecucionClasificadores.filter(d =>
d.transferencias === incluirTransferencias &&
String(d.dias) === String(dias) &&
d.clasificador === key
);
......@@ -443,12 +533,43 @@
}
});
// Re-renderizar gráficos al cambiar de sección (porque display:none → ancho 0)
$effect(() => {
const seccion = seccionActiva;
if (loading) return;
setTimeout(() => {
if (seccion === 1) {
clasificadores.forEach(({ key }) => {
if (barContainers[key]) renderizarBarras(key);
});
} else if (seccion === 2 && entidadesBarContainer) {
renderizarEntidades();
}
}, 50);
});
// Recargar datos cuando cambia el período
$effect(() => {
const dias = diasSeleccionados === 'total' ? '0' : diasSeleccionados;
recargarPorDias(dias);
});
async function recargarPorDias(dias) {
try {
const [entData, clasData] = await Promise.all([
fetch(`/api/diario?tipo=entidades&dias=${dias}`).then(r => r.ok ? r.json() : []),
fetch(`/api/diario?tipo=clasificadores&dias=${dias}`).then(r => r.ok ? r.json() : [])
]);
ejecucionEntidades = entData;
ejecucionClasificadores = clasData;
} catch (e) {
console.error('Error recargando datos:', e);
}
}
// Todas las entidades filtradas (sin límite)
let entidadesTodas = $derived.by(() => {
const dias = diasSeleccionados === 'total' ? 0 : parseInt(diasSeleccionados);
let filtered = ejecucionEntidades.filter(d =>
d.transferencias === incluirTransferencias &&
String(d.dias) === String(dias) &&
d.ejecucion >= rangoEjecucionMin &&
d.ejecucion <= rangoEjecucionMax
);
......@@ -473,9 +594,14 @@
// Agrupar timeline por entidad con sus programas
let timelineByEntity = $derived.by(() => {
// Si hay búsqueda activa, usar entidades cargadas con sus programas
if (busquedaTimeline.trim().length >= 2 && searchEntities.length > 0) {
return searchEntities;
}
// Sin búsqueda: usar datos locales de timeline
if (timeline.length === 0) return [];
// Agrupar por entidad
const grouped = {};
timeline.forEach(item => {
const key = item.entidad;
......@@ -496,24 +622,10 @@
});
});
// Convertir a array y ordenar por total
let result = Object.values(grouped)
.map(g => ({
...g,
programas: g.programas.sort((a, b) => b.devengado - a.devengado)
}))
.sort((a, b) => b.total - a.total);
// Filtrar por búsqueda si hay texto
if (busquedaTimeline.trim()) {
const q = busquedaTimeline.toLowerCase();
result = result.filter(e =>
e.entidad_desc_entidad?.toLowerCase().includes(q) ||
e.entidad_desc_area?.toLowerCase().includes(q)
);
}
return result.slice(0, 10);
return Object.values(grouped)
.map(g => ({ ...g, programas: g.programas.sort((a, b) => b.devengado - a.devengado) }))
.sort((a, b) => b.total - a.total)
.slice(0, 10);
});
// Renderizar barras de entidades con D3
......@@ -666,9 +778,10 @@
.attr('y1', headerHeight)
.attr('y2', height)
.attr('stroke', chartColors.grid)
.attr('stroke-width', 0.5)
.attr('stroke-dasharray', '1,4')
.attr('stroke-width', 0.75)
.attr('stroke-dasharray', 'none')
.attr('stroke-linecap', 'round')
.attr('opacity', 0.6)
.attr('opacity', 0.7)
.style('pointer-events', 'none'); // para que no interfiera con hover
}
......@@ -773,7 +886,7 @@
const colorFuera = isDark ? '#555550' : '#d0d0d0';
const colorFueraFill = isDark ? '#3a3a38' : '#e8e8e8';
// Color de fill activo = mismo que las barras highlight
const colorActivoFill = isDark ? '#C9A751' : '#F3EAD8';
const colorActivoFill = isDark ? '#C9A751' : '#4A8B6E';
const colorActivoLinea = isDark ? '#888880' : '#999999';
// Gráfico 1: Ejecución diaria (diferencia)
......@@ -949,24 +1062,33 @@
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
</svelte:head>
<div class="vista-diaria" style="font-family: var(--font-sans);" bind:this={vistaContainer} onscroll={detectarSeccion}>
<!-- Navegación interna fija -->
<nav class="nav-interna">
{#each secciones as seccion, i}
<button
class="nav-item"
class:active={seccionActiva === i}
onclick={() => irASeccion(i)}
>
<span class="nav-dot"></span>
<span class="nav-label">{seccion.titulo}</span>
</button>
{/each}
<!-- Botón flotante de descargas -->
<a href="/descargas" class="fab-descargas" title="Ir a la página de descarga de datos">
<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="M12 4v12"/><polyline points="8 12 12 16 16 12"/><path d="M4 18h16"/>
</svg>
<span class="fab-tooltip">Ir a la página de descarga de datos</span>
</a>
<!-- Navegación interna fija - barra inferior (fuera del scroll container) -->
<nav class="nav-bottom-bar">
<button class="nav-arrow" onclick={() => irASeccion(Math.max(0, seccionActiva - 1))} disabled={seccionActiva === 0}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/>
</svg>
</button>
<span class="nav-section-label">{secciones[seccionActiva]?.titulo || ''}</span>
<button class="nav-arrow" onclick={() => irASeccion(Math.min(secciones.length - 1, seccionActiva + 1))} disabled={seccionActiva === secciones.length - 1}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>
</nav>
<div class="vista-diaria" style="font-family: var(--font-sans);" bind:this={vistaContainer}>
{#if loading}
<div class="loading">
<p>Cargando datos...</p>
<div class="loader-spinner"></div>
</div>
{:else if error}
<div class="error">
......@@ -974,12 +1096,17 @@
</div>
{:else}
<!-- SECCIÓN 1: Programas (timeline) -->
<section class="vista-seccion" id="programas">
<section class="vista-seccion" class:seccion-activa={seccionActiva === 0} id="programas">
<div class="vista-contenido">
<div class="contenido-centrado">
<header class="seccion-header seccion-header-centrado">
<h1 class="seccion-titulo-grande">¿En qué programas?</h1>
<p class="seccion-descripcion">Detalle de programas y actividades ejecutadas en la última semana</p>
{#if fechaNarrativa}
<p class="vista-fecha-titulo">Ejecución presupuestaria del {fechaNarrativa}</p>
{/if}
<h1 class="seccion-titulo-sub">Por programas, proyectos y entidad</h1>
{#if horaActualizacion}
<p class="vista-fecha-hora">Actualización: {horaActualizacion}</p>
{/if}
</header>
<div class="timeline-search">
......@@ -990,11 +1117,18 @@
type="text"
placeholder="Buscar entidad..."
bind:value={busquedaTimeline}
oninput={handleSearchInput}
class="timeline-search-input"
/>
{#if searchLoading}
<span class="search-spinner"></span>
{/if}
</div>
<div class="timeline-cards">
{#if busquedaTimeline.trim().length >= 2 && !searchLoading && searchResults.length === 0}
<p style="text-align:center;color:var(--theme-texto);opacity:0.5;padding:24px 0;">Sin resultados para "{busquedaTimeline}"</p>
{/if}
{#each timelineByEntity as entidad}
<div class="timeline-card">
<div class="timeline-card-header">
......@@ -1038,7 +1172,7 @@
</section>
<!-- SECCIÓN 2: Clasificadores -->
<section class="vista-seccion" id="clasificadores">
<section class="vista-seccion" class:seccion-activa={seccionActiva === 1} id="clasificadores">
<div class="vista-contenido">
<div class="contenido-centrado">
<!-- Título de sección -->
......@@ -1080,18 +1214,6 @@
{label}
</button>
{/each}
<label class="toggle-transferencias">
<input type="checkbox" bind:checked={incluirTransferencias} />
<span class="toggle-label">Incluir transferencias</span>
<span class="tooltip-trigger">
<svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
<span class="tooltip-content">
Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado.
</span>
</span>
</label>
</div>
</div>
</div>
......@@ -1099,7 +1221,7 @@
<!-- SECCIÓN 2: Entidades -->
<section class="vista-seccion" id="entidades">
<section class="vista-seccion" class:seccion-activa={seccionActiva === 2} id="entidades">
<div class="vista-contenido">
<div class="contenido-centrado">
<!-- Título de sección -->
......@@ -1110,7 +1232,7 @@
<!-- Info de entidad seleccionada -->
{#if displayEntidad}
<div class="entidad-info">
<span class="entidad-pct">{formatearPorcentaje(displayEntidad.ejecucion)}</span>
<span class="entidad-pct">{formatearPorcentaje(displayEntidad.ejecucion)} <span class="entidad-pct-nota">del presupuesto 2026</span></span>
<span class="entidad-nombre">{displayEntidad.entidad_desc_entidad}</span>
<span class="entidad-area">{displayEntidad.entidad_desc_area}</span>
</div>
......@@ -1119,26 +1241,11 @@
<!-- Barras de ejecución -->
<div class="entidades-bar-container" bind:this={entidadesBarContainer}></div>
<!-- Paginación -->
<div class="entidades-paginacion">
<p class="entidades-nota">
Mostrando {entidadesFiltered.length} de {entidadesTodas.length} entidades
</p>
<div class="entidades-botones">
{#if entidadesVisibles > 10}
<button class="entidades-ver-mas" onclick={() => { entidadesVisibles = Math.max(10, entidadesVisibles - 10); }}>
Ver menos
</button>
{/if}
{#if entidadesFiltered.length < entidadesTodas.length}
<button class="entidades-ver-mas" onclick={() => { entidadesVisibles += 10; }}>
Ver {Math.min(10, entidadesTodas.length - entidadesFiltered.length)} más
</button>
{/if}
</div>
</div>
<p class="entidades-nota" style="text-align:center;margin-top:0.5rem;font-size:0.75rem;color:var(--theme-texto);opacity:0.6;">
Top 10 entidades por ejecución presupuestaria
</p>
<!-- Controles centrados debajo (igual que sección 1) -->
<!-- Controles centrados debajo -->
<div class="controles-footer">
{#each labelsPeriodo as { value, label }}
<button
......@@ -1149,57 +1256,6 @@
{label}
</button>
{/each}
<label class="toggle-transferencias">
<input type="checkbox" bind:checked={incluirTransferencias} />
<span class="toggle-label">Incluir transferencias</span>
<span class="tooltip-trigger">
<svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
<span class="tooltip-content">
Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado.
</span>
</span>
</label>
<div class="separador-control"></div>
<!-- Filtro de rango -->
<div class="rango-control-inline">
<label class="rango-label">Rango: {rangoEjecucionMin}% – {rangoEjecucionMax}%</label>
<div class="dual-slider dual-slider-inline" bind:this={sliderContainer}>
<div class="slider-track"></div>
<div
class="slider-range"
style="left: {rangoEjecucionMin}%; right: {100 - rangoEjecucionMax}%;"
onmousedown={handleRangeMouseDown}
role="slider"
tabindex="0"
></div>
<input
type="range"
min="0"
max="100"
bind:value={rangoEjecucionMin}
class="slider-thumb slider-thumb-min"
oninput={() => {
if (rangoEjecucionMin > rangoEjecucionMax - 5) {
rangoEjecucionMin = rangoEjecucionMax - 5;
}
}}
/>
<input
type="range"
min="0"
max="100"
bind:value={rangoEjecucionMax}
class="slider-thumb slider-thumb-max"
oninput={() => {
if (rangoEjecucionMax < rangoEjecucionMin + 5) {
rangoEjecucionMax = rangoEjecucionMin + 5;
}
}}
/>
</div>
</div>
</div>
</div>
</div>
......@@ -1244,18 +1300,6 @@
{label}
</button>
{/each}
<label class="toggle-transferencias">
<input type="checkbox" bind:checked={incluirTransferencias} />
<span class="toggle-label">Incluir transferencias</span>
<span class="tooltip-trigger">
<svg class="tooltip-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
<span class="tooltip-content">
Las transferencias son recursos que el gobierno central envía a gobiernos territoriales o al sector privado. Al desactivar esta opción se evita la doble contabilización, mostrando el gasto consolidado.
</span>
</span>
</label>
</div>
</div>
</div>
......@@ -1267,13 +1311,14 @@
/* Variables de color para gráficos - Modo claro */
:global(:root) {
--chart-bar-default: #e0e0e0;
--chart-bar-highlight: #F3EAD8;
--chart-bar-highlight: #4A8B6E;
--chart-bar-stroke: #b0b0b0;
--chart-bar-stroke-default: white;
--chart-track-bg: #f5f5f5;
--chart-grid: #bbb;
--chart-grid: #999;
--chart-label: #888;
--chart-card-header: #f5f0e6;
--chart-card-header: #ffffff;
--chart-card-body: #fafafa;
}
/* Variables de color para gráficos - Modo oscuro */
......@@ -1283,18 +1328,43 @@
--chart-bar-stroke: #5a5a55;
--chart-bar-stroke-default: #2a2a28;
--chart-track-bg: #2a2a28;
--chart-grid: #4a4a45;
--chart-grid: #6a6a65;
--chart-label: #8a8a85;
--chart-card-header: #3a3835;
--chart-card-header: #2a2825;
--chart-card-body: #222220;
}
.vista-diaria {
background: var(--theme-body);
background: #f2f2f4;
color: var(--theme-titulo);
scroll-snap-type: y mandatory;
overflow-y: scroll;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
:global(.dark) .vista-diaria {
background: #1C1C1A;
}
/* Header fecha */
.vista-fecha-titulo {
font-family: 'Qanelas', var(--font-sans);
font-size: 2rem;
font-weight: 600;
color: var(--theme-titulo);
margin: 0;
}
.seccion-titulo-sub {
font-family: 'Qanelas', var(--font-sans);
font-size: 1.25rem;
font-weight: 600;
color: var(--theme-titulo);
margin: 0.5rem 0 0.25rem;
}
.vista-fecha-hora {
font-size: 0.75rem;
color: var(--theme-titulo);
opacity: 0.8;
margin: 0.2rem 0 0;
}
/* Loading/Error */
......@@ -1305,75 +1375,152 @@
justify-content: center;
color: var(--theme-texto);
}
.loader-spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(128,128,128,0.2);
border-top-color: var(--theme-titulo);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
.error {
color: #DB281C;
}
/* Navegación interna fija */
.nav-interna {
/* Navegación interna fija - barra inferior */
/* Botón flotante descargas */
.fab-descargas {
position: fixed;
right: 1.5rem;
bottom: 24px;
right: 24px;
width: 48px;
height: 48px;
border-radius: 14px;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(0,0,0,0.08);
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--theme-titulo);
text-decoration: none;
z-index: 101;
transition: all 0.25s ease;
cursor: pointer;
}
.fab-descargas:hover {
background: rgba(255,255,255,0.95);
box-shadow: 0 6px 24px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
:global(.dark) .fab-descargas {
background: rgba(28,28,26,0.85);
border-color: rgba(255,255,255,0.1);
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
:global(.dark) .fab-descargas:hover {
background: rgba(42,42,40,0.95);
box-shadow: 0 6px 24px rgba(0,0,0,0.4);
}
.fab-tooltip {
position: absolute;
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%);
background: rgba(0,0,0,0.85);
color: #fff;
font-size: 0.75rem;
font-weight: 500;
padding: 6px 12px;
border-radius: 8px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.fab-descargas:hover .fab-tooltip {
opacity: 1;
}
:global(.dark) .fab-tooltip {
background: rgba(255,255,255,0.9);
color: #1C1C1A;
}
.nav-bottom-bar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
gap: 12px;
padding: 8px 16px;
border-radius: 14px;
background: rgba(255,255,255,0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(0,0,0,0.08);
z-index: 100;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
}
:global(.dark) .nav-bottom-bar {
background: rgba(28,28,26,0.75);
border-color: rgba(255,255,255,0.1);
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.nav-item {
.nav-arrow {
display: flex;
align-items: center;
gap: 0.5rem;
background: none;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: rgba(0,0,0,0.06);
color: var(--theme-titulo);
cursor: pointer;
padding: 0.25rem;
flex-direction: row-reverse;
}
.nav-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.2);
transition: all 0.2s;
}
:global(.dark) .nav-dot {
background: rgba(255, 255, 255, 0.2);
.nav-arrow:hover:not(:disabled) {
background: rgba(0,0,0,0.12);
}
.nav-item.active .nav-dot {
background: var(--theme-accent);
transform: scale(1.2);
.nav-arrow:disabled {
opacity: 0.25;
cursor: default;
}
.nav-label {
font-size: 0.7rem;
color: var(--theme-texto);
opacity: 0;
transition: opacity 0.2s;
white-space: nowrap;
:global(.dark) .nav-arrow {
background: rgba(255,255,255,0.08);
}
:global(.dark) .nav-arrow:hover:not(:disabled) {
background: rgba(255,255,255,0.15);
}
.nav-item:hover .nav-label,
.nav-item.active .nav-label {
opacity: 1;
.nav-section-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--theme-titulo);
white-space: nowrap;
min-width: 120px;
text-align: center;
}
/* Secciones de 100vh - el navbar tiene ~56px de alto */
/* Secciones - solo visible la activa */
.vista-seccion {
min-height: 100vh;
min-height: 100dvh;
height: 100vh;
height: 100dvh;
padding-top: 56px;
padding-bottom: 70px;
box-sizing: border-box;
display: flex;
display: none;
flex-direction: column;
border-bottom: 1px solid var(--theme-borde);
scroll-snap-align: start;
scroll-snap-stop: always;
overflow-y: auto;
}
.vista-seccion.seccion-activa {
display: flex;
}
......@@ -1643,9 +1790,9 @@
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: var(--theme-texto);
width: 18px;
height: 18px;
color: var(--theme-titulo);
opacity: 0.5;
pointer-events: none;
}
......@@ -1655,9 +1802,9 @@
padding: 0.6rem 1rem 0.6rem 2.25rem;
font-size: 0.8rem;
font-family: inherit;
border: 1px solid var(--theme-borde);
border-radius: 20px;
background: var(--theme-body);
border: none;
border-radius: 12px;
background: #ffffff;
color: var(--theme-titulo);
outline: none;
transition: border-color 0.2s;
......@@ -1673,10 +1820,25 @@
color: var(--theme-accent);
}
:global(.dark) .timeline-search-input {
background: #252523;
}
.timeline-search-input::placeholder {
color: var(--theme-texto);
color: var(--theme-titulo);
opacity: 0.5;
font-size: 0.85rem;
}
.search-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(128,128,128,0.3);
border-top-color: var(--theme-texto);
border-radius: 50%;
animation: spin 0.6s linear infinite;
flex-shrink: 0;
margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.timeline-cards {
display: flex;
......@@ -1705,6 +1867,10 @@
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
border-bottom: 1px solid rgba(0,0,0,0.06);
}
:global(.dark) .timeline-card-header {
border-bottom-color: rgba(255,255,255,0.08);
}
.timeline-card-entity {
......@@ -1717,8 +1883,8 @@
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--theme-texto);
opacity: 0.6;
color: var(--theme-titulo);
opacity: 0.7;
}
.timeline-card-nombre {
......@@ -1755,6 +1921,7 @@
.timeline-card-body {
padding: 0.5rem 1rem;
text-align: center;
background: var(--chart-card-body);
}
.timeline-prog-row {
......@@ -1785,8 +1952,8 @@
.timeline-prog-programa {
font-size: 0.6rem;
color: var(--theme-texto);
opacity: 0.5;
color: var(--theme-titulo);
opacity: 0.7;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
......@@ -1848,6 +2015,12 @@
color: var(--theme-titulo);
line-height: 1;
}
.entidad-pct-nota {
font-size: 0.85rem;
font-weight: 500;
color: var(--theme-texto);
opacity: 0.7;
}
.entidad-nombre {
font-size: 1rem;
......@@ -2066,25 +2239,8 @@
}
/* Responsive */
@media (max-width: 900px) {
.nav-interna {
right: 0.75rem;
}
.nav-label {
display: none;
}
}
@media (max-width: 768px) {
.vista-diaria {
scroll-snap-type: y proximity;
}
.vista-seccion {
scroll-snap-stop: normal;
}
.vista-contenido {
padding: 1rem;
}
......