@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  /* `min-height` y no `height`. Con `height: 100%` el `<body>` mide UNA ventana
     aunque el documento tenga 14.654 px, y eso rompe cualquier
     `position: sticky` que cuelgue de acá: un sticky solo se desplaza dentro de
     la caja de su contenedor, así que el menú del sitio se despegaba al pasar
     la primera pantalla. Reportado el 2026-08-12 sobre AnnurTV con el scroll
     infinito prendido, que es donde más se nota (la página crece sin límite),
     pero pasaba igual en cualquier nota larga. */
  min-height: 100%;
  /* `--site-*`: base default = look actual de Pupila NEWS, redefinible
     por portal vía `portals.header_scripts`. */
  font-family: var(--site-font-body, 'Open Sans', sans-serif);
  background: var(--site-bg, oklch(0.98 0.003 250));
  color: var(--site-text, oklch(0.2 0.01 250));
}

/* `100vh` y no `100%`: con el `<body>` en `min-height` el porcentaje no tiene
   contra qué resolver. Es el contenedor del que cuelga el shell del sitio en
   las páginas de nota, así que de su alto depende hasta dónde llega el menú
   pegajoso. */
#app { min-height: 100vh; }

/* Scrollbar chica y unificada en todos los navegadores/elementos. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border, oklch(0.8 0.008 250)) transparent;
}

::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border, oklch(0.8 0.008 250));
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-faint, oklch(0.62 0.01 250)); }
::-webkit-scrollbar-corner { background: transparent; }

input::placeholder { color: oklch(0.62 0.01 250); }

button { font-family: inherit; cursor: pointer; }

input:focus, textarea:focus, select:focus {
  outline: none !important;
  border-color: var(--accent, oklch(0.55 0.18 250)) !important;
}

/* Pedido explícito: "no debe tener loader ni nada... debe cargar
   inmediatamente" — con el caché persistente de compilados (ver
   lib/svelte-loader.js) la carga real ya es casi instantánea en visitas
   repetidas, pero SIEMPRE hay una primera carga (o una con caché fría)
   que tarda algo. En vez de mostrar el loader desde el frame 0 (parpadeo
   feo incluso cuando la carga es rápida), queda invisible los primeros
   400ms — si la app ya montó para entonces, `main.js` vacía `#app` antes
   de que el loader llegue a hacerse visible y el usuario nunca lo ve. Si
   de verdad tarda más que eso, aparece como fallback (mejor que una
   pantalla en blanco que parece colgada). */
.boot-loader {
  /* Antes heredaba el alto de un `#app` que medía exactamente una ventana. Con
     `#app` creciendo con el contenido, un `height: 100%` acá se resuelve contra
     `auto` y el loader colapsa: se fija a la ventana, que es lo que quiere
     decir. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  opacity: 0;
  animation: boot-loader-reveal 0s 400ms forwards;
}

@keyframes boot-loader-reveal {
  to { opacity: 1; }
}

.boot-bar {
  width: 160px;
  height: 4px;
  border-radius: 2px;
  background: oklch(0.9 0.005 250);
  overflow: hidden;
  position: relative;
}

.boot-bar-fill {
  position: absolute;
  top: 0;
  left: -40%;
  height: 100%;
  width: 40%;
  border-radius: 2px;
  background: linear-gradient(90deg, oklch(0.5 0.19 320), oklch(0.55 0.18 250), oklch(0.55 0.15 200));
  animation: boot-bar-slide 1.1s ease-in-out infinite;
}

.boot-label {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 12px;
  color: oklch(0.55 0.012 250);
  letter-spacing: 0.02em;
}

@keyframes boot-bar-slide {
  0% { left: -40%; }
  100% { left: 100%; }
}

/* ------------------------------------------------------------------ */
/* Contenido público — el HTML de acá abajo lo arma BlockRenderer del  */
/* lado del backend (ver PublicSiteController) e inyecta PageView.svelte */
/* con `{@html}` tal cual; estas clases son las que ya usan los bloques  */
/* reales cargados en Configuración → Bloques (Bienvenida/Últimas       */
/* notas/Detalle de nota/páginas estáticas) — ver PUPILA_CMS.md.        */
/* ------------------------------------------------------------------ */

.hero {
  padding: 40px 0 32px;
  margin-bottom: 8px;
  border-bottom: 1px solid oklch(0.9 0.005 250);
}
.hero-title {
  font-size: 34px;
  font-weight: 700;
  letter-spacing: -0.03em;
  margin: 0 0 10px;
  color: oklch(0.2 0.01 250);
}
.hero-lead {
  font-size: 15.5px;
  line-height: 1.6;
  color: oklch(0.48 0.012 250);
  max-width: 640px;
  margin: 0;
}

.note-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 20px;
  margin-top: 24px;
}

.note-card {
  border: 1px solid oklch(0.9 0.005 250);
  border-radius: 8px;
  padding: 18px;
  background: oklch(1 0 0);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.note-card-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.note-chip {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: oklch(0.55 0.18 250);
  background: oklch(0.94 0.03 250);
  border-radius: 3px;
  padding: 3px 7px;
}
.note-card-title { font-size: 16.5px; font-weight: 700; line-height: 1.35; margin: 0; }
.note-card-title a { color: oklch(0.2 0.01 250); text-decoration: none; }
.note-card-title a:hover { color: oklch(0.55 0.18 250); }
.note-card-bajada { font-size: 13.5px; line-height: 1.5; color: oklch(0.5 0.012 250); margin: 0; }
.note-card-bajada p, .note-card-bajada ul { margin: 0; padding-left: 18px; }

.note-detail { display: flex; flex-direction: column; gap: 14px; }
.note-detail-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.note-detail-bajada { font-size: 17px; line-height: 1.55; color: oklch(0.35 0.012 250); font-weight: 500; }
.note-detail-autores { font-size: 12.5px; color: oklch(0.55 0.012 250); font-weight: 600; margin: 0; }
.note-detail-autores a { color: oklch(0.55 0.18 250); text-decoration: none; }
.note-detail-autores a:hover { text-decoration: underline; }
.note-detail-cuerpo { font-size: 15.5px; line-height: 1.75; color: oklch(0.25 0.01 250); }
.note-detail-cuerpo p { margin: 0 0 16px; }
.note-detail-cuerpo ul, .note-detail-cuerpo ol { margin: 0 0 16px; padding-left: 22px; }

/* Relacionadas/comentarios — el `{{each}}` de BlockRenderer no borra el
   contenedor cuando no hay filas (no hay `{{if}}` real, ver
   `resolveIfBlocks()`), así que la lista vacía se oculta con `:has()`
   en vez de con lógica del lado del servidor. */
.note-related { margin-top: 8px; }
.note-related:has(.note-related-list:empty) { display: none; }
.note-related-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: oklch(0.45 0.012 250); margin: 0 0 8px; }
.note-related-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.note-related-list a { color: oklch(0.2 0.01 250); font-weight: 600; font-size: 14px; text-decoration: none; }
.note-related-list a:hover { color: oklch(0.55 0.18 250); }

.note-comments { margin-top: 24px; border-top: 1px solid oklch(0.9 0.005 250); padding-top: 20px; }
.note-comments-title { font-size: 16px; font-weight: 700; margin: 0 0 12px; }
.note-comments-list { list-style: none; margin: 0 0 20px; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.note-comments-list li { border: 1px solid oklch(0.92 0.005 250); border-radius: 6px; padding: 10px 12px; }
.note-comments-list strong { display: block; font-size: 12.5px; margin-bottom: 4px; }
.note-comments-list p { margin: 0; font-size: 13.5px; color: oklch(0.35 0.012 250); line-height: 1.5; }
.note-comment-form { display: flex; flex-direction: column; gap: 8px; max-width: 420px; }
.note-comment-form input, .note-comment-form textarea {
  border: 1px solid oklch(0.85 0.008 250); border-radius: 4px; padding: 8px 10px; font-size: 13px; font-family: inherit;
}
.note-comment-form textarea { min-height: 70px; resize: vertical; }
.note-comment-form button {
  border: none; background: oklch(0.55 0.18 250); color: white; border-radius: 4px; padding: 8px 14px;
  font-size: 13px; font-weight: 600; align-self: flex-start;
}
.ajax-form-status { font-size: 12px; color: oklch(0.5 0.15 150); margin: 4px 0 0; }
.ajax-form-status.ajax-form-error { color: oklch(0.5 0.2 25); }

/* Paginador de los listados de sección y de tag — lo emite
   SiteRenderer::paginadorHtml() como HTML plano (no es un Bloque), y este CSS
   lo cargan las DOS vías de render: el SPA y el HTML pre-renderizado de
   SitePageHtml, que linkean este mismo archivo.
   Los colores usan `var(--site-*)` con fallback, igual que el resto del shell,
   para que un portal con su propia identidad en `header_scripts` lo pise sin
   tocar nada acá. */
.pn-pagination {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  gap: 6px; margin: 32px 0 8px; font-size: 13.5px;
  font-family: var(--site-font-body, inherit);
}
.pn-pagination-pages { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.pn-pagination-page, .pn-pagination-step {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 34px; height: 34px; padding: 0 10px;
  border: 1px solid oklch(0.9 0.005 250); border-radius: 4px;
  color: var(--site-text, oklch(0.2 0.01 250)); text-decoration: none; font-weight: 600;
}
.pn-pagination-page:hover, .pn-pagination-step:hover {
  border-color: var(--site-accent, oklch(0.55 0.18 250));
  color: var(--site-accent, oklch(0.55 0.18 250));
}
.pn-pagination-page.is-current {
  background: var(--site-accent, oklch(0.55 0.18 250));
  border-color: var(--site-accent, oklch(0.55 0.18 250));
  color: white; cursor: default;
}
/* "Anterior" en la página 1 y "Siguiente" en la última se siguen renderizando
   —como <span>, no como link— para que la barra no cambie de ancho ni salten
   los números al llegar a un extremo. */
.pn-pagination-step.is-disabled {
  color: var(--site-text-muted, oklch(0.65 0.012 250));
  border-color: oklch(0.94 0.004 250); cursor: default;
}
.pn-pagination-gap { color: var(--site-text-muted, oklch(0.6 0.012 250)); padding: 0 2px; }
@media (max-width: 520px) {
  /* En pantalla chica los números sobran: con "Anterior/Siguiente" alcanza
     para recorrer, y 322 páginas no entran de ninguna forma. */
  .pn-pagination-pages { display: none; }
}

/* Sidebar (nota/autor) — ver PublicSiteController::syntheticTemplate(). */
.content-with-sidebar { display: grid; grid-template-columns: 1fr; gap: 32px; }
@media (min-width: 820px) { .content-with-sidebar { grid-template-columns: 2fr 1fr; align-items: start; } }
.content-sidebar { display: flex; flex-direction: column; gap: 24px; }
.sidebar-widget { border: 1px solid oklch(0.9 0.005 250); border-radius: 8px; padding: 16px; }
.sidebar-widget-title { font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: oklch(0.45 0.012 250); margin: 0 0 10px; }
.sidebar-recent-list { display: flex; flex-direction: column; gap: 8px; }
.sidebar-recent-link { color: oklch(0.2 0.01 250); font-size: 13.5px; font-weight: 600; text-decoration: none; }
.sidebar-recent-link:hover { color: oklch(0.55 0.18 250); }
.sidebar-newsletter-lead { font-size: 12.5px; color: oklch(0.5 0.012 250); margin: 0 0 10px; }
.sidebar-newsletter-form { display: flex; flex-direction: column; gap: 8px; }
.sidebar-newsletter-form input {
  border: 1px solid oklch(0.85 0.008 250); border-radius: 4px; padding: 8px 10px; font-size: 13px; font-family: inherit;
}
.sidebar-newsletter-form button {
  border: none; background: oklch(0.2 0.01 250); color: white; border-radius: 4px; padding: 8px 14px; font-size: 13px; font-weight: 600;
}
.sidebar-newsletter-success { font-size: 12.5px; color: oklch(0.5 0.15 150); font-weight: 600; }

/* Perfil de autor. */
.author-profile { text-align: center; padding-bottom: 20px; border-bottom: 1px solid oklch(0.9 0.005 250); margin-bottom: 20px; }
.author-avatar {
  width: 84px; height: 84px; border-radius: 50%; margin: 0 auto 14px; object-fit: cover;
  display: flex; align-items: center; justify-content: center;
}
.author-avatar-initial { background: oklch(0.55 0.18 250); color: white; font-size: 32px; font-weight: 700; }
.author-profile-name { font-size: 22px; font-weight: 700; margin: 0 0 8px; }
.author-profile-bio { font-size: 14px; color: oklch(0.45 0.012 250); line-height: 1.6; max-width: 520px; margin: 0 auto 12px; }
.author-profile-social { display: flex; justify-content: center; gap: 8px; margin-bottom: 10px; }
.author-social-icon {
  display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid oklch(0.85 0.008 250); color: oklch(0.45 0.012 250); font-size: 10.5px; font-weight: 700; text-decoration: none;
}
.author-social-icon:hover { border-color: oklch(0.55 0.18 250); color: oklch(0.55 0.18 250); }
.author-profile-count { font-size: 12.5px; color: oklch(0.55 0.012 250); margin: 0; }

.note-paywall {
  border: 1px dashed oklch(0.8 0.02 80);
  background: oklch(0.97 0.02 80);
  border-radius: 8px;
  padding: 24px;
}
.note-paywall p { margin: 0 0 10px; font-size: 14.5px; line-height: 1.6; color: oklch(0.4 0.012 250); }
.note-paywall-notice { font-weight: 700; color: oklch(0.5 0.15 80) !important; margin: 0 !important; }

.page-static { font-size: 15px; line-height: 1.75; color: oklch(0.3 0.012 250); }
.page-static p { margin: 0 0 14px; }
.contact-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.contact-list strong { color: oklch(0.2 0.01 250); }

.page-category { padding-top: 4px; }

/* --- Imagen destacada de las notas -----------------------------------------
   Los Bloques genéricos (Detalle de nota, Últimas notas, Card de nota, Notas
   del autor, Notas de sección) no emitían `{{nota.imagen_destacada}}` — el
   frontend mostraba las notas sin foto aunque las 30 la tuvieran cargada.
   Estas clases son el contenedor con proporción fija que necesita la imagen
   para no romper el layout: sin `aspect-ratio` + `object-fit`, una foto de
   1600px de ancho entra a tamaño natural y desarma la tarjeta.

   Van acá, en el CSS genérico del frontend, y NO en el `header_scripts` de un
   portal: son parte del componente, no de la identidad visual de un cliente
   (mismo criterio que `.note-card`/`.note-grid`, que ya vivían acá). Un portal
   que quiera otra proporción la pisa desde su `header_scripts`. */
.note-card-media,
.note-detail-media {
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 5px;
  background: oklch(0.95 0.005 250);
}
.note-card-media { margin-bottom: 2px; }
.note-card-media img,
.note-detail-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Red de seguridad: una nota (o un autor) sin imagen cargada emitía
   `<img src="">`, que el navegador dibuja como ícono de imagen rota. El motor
   de tokens no tiene condicionales reales (`resolveIfBlocks()` solo borra los
   marcadores), así que no se puede evitar emitir el tag — pero sí que se vea.
   Caso concreto: los 6 autores sin avatar de la portada. */
img[src=""] { display: none; }


/* === Shell del sitio (header + footer) ==========================
   Movido acá desde los <style> scoped de los componentes Header/Footer (ya
   borrados: el shell ahora son los Bloques "Cabecera del sitio"/"Pie del
   sitio"): el
   HTML pre-renderizado que genera StaticSiteGenerator no pasa por Svelte,
   así que un estilo scoped no existiría ahí y el shell se vería sin
   formato. Mismas reglas, mismas variables `--site-*`, mismo fallback.
   ============================================================== */

/* --- shell: cabecera --- */
/* Pedido explícito: "todo es configurable por backed" — este shell es
   el mismo para cualquier portal (ver App.svelte), así que su look NO
   puede quedar hardcodeado acá: cada color/tipografía sale de una
   custom property `--site-*`, con el look actual de Pupila NEWS como
   fallback (así un portal que no cargó nada en
   `portals.header_scripts` se ve exactamente igual que hasta ahora).
   Un portal con identidad propia redefine estas variables en un
   `:root{...}` dentro de su propio `header_scripts` — ver
   PublicSiteController::nav()/App.svelte. */
.site-header {
  border-bottom: 1px solid var(--site-border, oklch(0.9 0.005 250));
  background: var(--site-header-bg, oklch(1 0 0));
  position: sticky;
  top: 0;
  z-index: 10;
}
.site-header-inner {
  max-width: var(--site-max-width, 960px);
  margin: 0 auto;
  padding: 16px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}
.site-brand {
  font-family: var(--site-font-heading, inherit);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--site-text, oklch(0.2 0.01 250));
  text-decoration: none;
  display: flex;
  align-items: center;
}
/* Alto fijo y ancho automático: el logo que suba cada portal puede tener
   cualquier proporción. `--site-logo-height` deja ajustarlo desde el
   `header_scripts` del portal sin tocar este componente, igual que el
   resto de las `--site-*`. */
.site-logo {
  display: block;
  height: var(--site-logo-height, 34px);
  width: auto;
  max-width: 260px;
  object-fit: contain;
}
.site-nav {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}
.site-nav a {
  font-family: var(--site-font-nav, inherit);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--site-text-muted, oklch(0.45 0.012 250));
  text-decoration: none;
  padding: 4px 0;
  border-bottom: 2px solid transparent;
}
.site-nav a:hover { color: var(--site-text, oklch(0.2 0.01 250)); }
.site-nav a[aria-current="page"] {
  color: var(--site-accent, oklch(0.55 0.18 250));
  border-bottom-color: var(--site-accent, oklch(0.55 0.18 250));
}

/* --- shell: pie --- */
/* Mismo criterio de variables `--site-*` que la cabecera — ver el
   comentario ahí. */
.site-footer {
  border-top: 1px solid var(--site-border, oklch(0.9 0.005 250));
  background: var(--site-footer-bg, transparent);
  margin-top: 48px;
  padding: 24px;
  text-align: center;
}
.site-footer p {
  font-family: var(--site-font-body, inherit);
  margin: 4px 0;
  font-size: 12.5px;
  color: var(--site-footer-text, oklch(0.55 0.012 250));
}
.site-footer-note { color: var(--site-footer-text-muted, oklch(0.65 0.01 250)); font-size: 11.5px; }

/* Marca del sitio sin logo cargado: el Bloque "Cabecera del sitio" emite
   SIEMPRE la imagen y el nombre, porque el motor de tokens no tiene
   condicionales reales (`resolveIfBlocks()` solo borra los marcadores). El
   nombre queda oculto salvo que la imagen venga vacía — que es el caso de un
   portal sin logo (The Java Paper). Sin esto ese header quedaba SIN marca:
   `<img src="">` no dibuja nada y no había fallback. La regla general
   `img[src=""] { display: none }` de más arriba oculta la imagen vacía. */
.site-brand-name { display: none; }
.site-logo[src=""] + .site-brand-name { display: inline; }

/* Contenido atenuado mientras llega la página siguiente.
   ------------------------------------------------------
   Pedido: al paginar una sección el scroll salta arriba pero las notas tardan
   en cambiar, y "parece un error". Atenuar da la señal de que algo está
   pasando sin vaciar la pantalla.

   `pointer-events: none` evita el doble clic impaciente sobre un paginador que
   ya está trabajando — que además pediría dos páginas y mostraría la que
   conteste última, no la que el lector eligió.

   La transición es asimétrica a propósito: entra en 120ms (suave, acompaña la
   espera) y sale en 60ms, para que el contenido nuevo aparezca despierto y no
   dé la sensación de seguir cargando. */
.page-view-cargando,
.page-shell-cargando {
  opacity: 0.45;
  pointer-events: none;
  cursor: progress;
  transition: opacity 120ms ease-out;
}

.page-view,
.page-shell {
  transition: opacity 60ms ease-in;
}

@media (prefers-reduced-motion: reduce) {
  .page-view-cargando,
  .page-shell-cargando,
  .page-view,
  .page-shell { transition: none; }
}
