Cantos menos redondos, tarjetas coherentes y portada a ras

Los botones dejan de ser cápsulas y las tarjetas pierden la mitad del canto. De
paso se ordena el sistema: había tres radios sueltos y una píldora de 999px
repetida en quince archivos, y quedan dos —uno para lo que se pulsa, otro para
las superficies—. Los botones de icono suelto siguen redondos, que ahí sí tiene
sentido.

Las fichas del taller no usaban la tarjeta compartida: tenían su propio borde,
su propio canto y su propio hover, así que se veían de otra familia que las de
música. Ahora usan `.tarjeta`, con el mismo reflejo bajo el puntero y el mismo
subrayado.

Y la portada arranca en el borde de la ventana, con la cabecera flotando encima.
Para eso la cabecera pasa a medir su propio alto y publicarlo: estaba escrito a
mano en 4,75rem cuando mide 68px, y todo lo que dependía de ese número —el salto
a un ancla, las barras laterales pegajosas— quedaba desplazado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
master
dev 1 month ago
parent 2213368d5b
commit a918d55864

@ -663,6 +663,17 @@ videos:
## La cabecera y las anclas
La portada **empieza en el borde de la ventana** y la cabecera flota encima:
por eso es translúcida con desenfoque, para apoyarse sobre la imagen en vez de
tapar una franja. Se consigue tirando de la portada hacia arriba con un margen
negativo del alto de la cabecera, que cada lámina devuelve en relleno para que
su contenido no se mueva.
Ese alto **lo mide la cabecera** y lo publica en `--altura-cabecera`, igual que
hace la barra del reproductor con el suyo. Estaba escrito a mano en 4,75rem
cuando la cabecera mide 68px, así que todo lo que dependía de él —el salto a un
ancla, las barras laterales pegajosas— quedaba desplazado.
La cabecera **acompaña al desplazamiento** (`position: sticky`) y deja ver lo
que pasa por debajo. La transparencia va **con desenfoque, no sola**: sobre la
portada, un fondo solo translúcido dejaría el contraste del menú a merced de la
@ -706,6 +717,18 @@ Tres cosas que estaban copiadas en varios sitios y ahora se escriben una vez:
un enlace dentro de otro deja una zona donde el clic hace una cosa distinta
según el píxel—, así que es una clase y no un componente.
## Cantos y radios
Dos valores y no más, para que nada se vea de otra familia:
`--radius-boton` (6px) para lo que se pulsa o se escribe —botones, campos,
pastillas—, y `--radius-tarjeta` (8px) para las superficies —tarjetas, paneles,
portadas—. Antes había tres radios sueltos y una píldora de 999px repetida en
quince archivos.
Los botones de **icono suelto** siguen siendo redondos: el de reproducir sobre
una portada, el del tema, los puntos del carrusel. Un icono en un círculo tiene
sentido; un botón con texto en forma de cápsula es otra cosa.
## Tarjetas
Las tres clases —estilo, álbum y canción— comparten `.tarjeta` en `app.css` y

@ -1,35 +1,28 @@
# Nginx por delante del servidor de Node.
#
# Copiar a /etc/nginx/sites-available/senzapaura, sustituir DOMINIO, y luego:
# sudo cp senzapaura.nginx.conf /etc/nginx/sites-available/senzapaura
# sudo ln -sf /etc/nginx/sites-available/senzapaura /etc/nginx/sites-enabled/
# sudo rm -f /etc/nginx/sites-enabled/default
# sudo nginx -t && sudo systemctl reload nginx
#
# sudo ln -s /etc/nginx/sites-available/senzapaura /etc/nginx/sites-enabled/
# sudo nginx -t # comprueba la sintaxis antes de recargar
# sudo systemctl reload nginx
# Ese `rm` del sitio por defecto importa: mientras esté, es él quien contesta a
# quien entra por IP, y se ve la página de bienvenida de Nginx en lugar de la
# web.
#
# El certificado lo pone certbot, que reescribe este archivo al ejecutarse:
#
# sudo certbot --nginx -d DOMINIO -d www.DOMINIO
#
# Sin HTTPS no se puede publicar: aquí se escriben correos y se paga.
# Todo lo que llegue sin cifrar se manda a la versión segura. Certbot añade
# este bloque por su cuenta, pero se deja escrito para que no dependa de ello.
server {
listen 80;
listen [::]:80;
server_name DOMINIO www.DOMINIO;
return 301 https://$host$request_uri;
}
# Tal como está, sirve por IP y sin cifrar, que es lo que se puede hacer en una
# red local sin dominio. Al final del archivo está lo que hay que cambiar
# cuando lo haya.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name DOMINIO www.DOMINIO;
# `default_server` es lo que hace que este sitio conteste a quien entra por
# la IP o por un nombre que no coincida con ninguno. Sin esto, Nginx elige
# el sitio `default` y enseña su bienvenida.
listen 80 default_server;
listen [::]:80 default_server;
# certbot rellena estas dos.
# ssl_certificate /etc/letsencrypt/live/DOMINIO/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/DOMINIO/privkey.pem;
# `_` no casa con ningún nombre real: es la forma de decir «cualquiera».
# Cuando haya dominio, aquí va el dominio.
server_name _;
# Los mp3 completos pesan varios megas; los fragmentos, cientos de kilos.
client_max_body_size 20m;
@ -46,7 +39,7 @@ server {
#
# Todo lo de `_app/immutable` lleva el hash del contenido en el nombre: si
# cambia el archivo, cambia la URL. Por eso se puede cachear para siempre y
# servirlo desde nginx sin molestar a Node, que es lo que hace que abrir la
# servirlo desde Nginx sin molestar a Node, que es lo que hace que abrir la
# web no despierte al proceso para devolver un CSS.
# -----------------------------------------------------------------------
location /_app/immutable/ {
@ -57,9 +50,9 @@ server {
try_files $uri =404;
}
# Imágenes, fuentes, vídeo y los fragmentos de muestra. No llevan huella, así
# que se cachean un día: lo justo para no pedirlos en cada visita sin que un
# cambio tarde una semana en verse.
# Imágenes, fuentes, vídeo, los fragmentos de muestra y el panel. No llevan
# huella, así que se cachean un día: lo justo para no pedirlos en cada
# visita sin que un cambio tarde una semana en verse.
location ~* ^/(images|background|video|muestras|admin)/ {
root /var/www/senzapaura/build/client;
access_log off;
@ -73,7 +66,7 @@ server {
#
# Incluidas /descargas/… —que comprueban la compra antes de entregar el
# archivo— y el webhook de Stripe. Ojo: las descargas NO se pueden servir
# desde nginx; el audio completo vive fuera de `build/` justamente para que
# desde Nginx; el audio completo vive fuera de `build/` justamente para que
# no haya forma de pedirlo por URL sin haberlo comprado.
# -----------------------------------------------------------------------
location / {
@ -93,7 +86,26 @@ server {
# Un mp3 completo tarda en salir por una conexión lenta.
proxy_read_timeout 300s;
# Sin buffer: la descarga empieza a fluir en cuanto Node la manda, en
# vez de esperar a que nginx tenga el archivo entero.
# vez de esperar a que Nginx tenga el archivo entero.
proxy_buffering off;
}
}
# ---------------------------------------------------------------------------
# Cuando haya dominio
#
# 1. Cambiar `server_name _;` por `server_name DOMINIO www.DOMINIO;` y quitar
# `default_server` de las dos líneas `listen` de arriba.
# 2. Poner el certificado:
#
# sudo apt install -y certbot python3-certbot-nginx
# sudo certbot --nginx -d DOMINIO -d www.DOMINIO
#
# Certbot añade solo el bloque de 443 y la redirección desde el 80.
# 3. Cambiar `ORIGIN` en /var/www/senzapaura/.env a `https://DOMINIO` y
# reiniciar el servicio. Sin eso, SvelteKit rechaza todos los formularios
# con un 403 por comprobación de origen, y parece que la web está rota.
#
# Sin HTTPS no se puede publicar de cara al exterior: aquí se escriben correos
# y se paga.
# ---------------------------------------------------------------------------

@ -94,9 +94,18 @@
var(--c-accent-sombra) 100%
);
--radius: 4px;
/* Radio propio: 4px en una tarjeta grande se lee como un recorte, no como una pieza. */
--radius-tarjeta: 14px;
/*
* Dos radios y no más, para que nada se vea de otra familia:
*
* - `--radius-boton` para lo que se pulsa o se escribe: botones, campos,
* pastillas.
* - `--radius-tarjeta` para las superficies: tarjetas, paneles, portadas.
*
* Un canto muy redondo tira a aplicación de móvil, y esto es una web de
* letras: la esquina corta acompaña mejor a una serif.
*/
--radius-boton: 6px;
--radius-tarjeta: 8px;
/*
* Curvas del movimiento. `--sal` sale rápido y frena largo, que es como se
@ -319,7 +328,7 @@ button {
gap: var(--sp-2);
padding: var(--sp-3) var(--sp-5);
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
/*
* `background-color` y no el atajo `background`: el atajo reinicia también
* `background-image`, así que una variante con degradado —el botón dorado—
@ -406,7 +415,7 @@ button {
display: inline-block;
padding: 0.2em 0.7em;
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
font-size: var(--fs-xs);
color: var(--c-text-muted);
text-decoration: none;

@ -57,7 +57,7 @@
.boton {
padding: var(--sp-1) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
background: transparent;
font-size: var(--fs-sm);
cursor: pointer;

@ -81,7 +81,7 @@
gap: var(--sp-2);
padding: var(--sp-1) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
background: transparent;
color: var(--c-text-muted);
font: inherit;

@ -109,6 +109,8 @@
}
.solido {
/* Con texto ya no es un icono en un círculo, sino un botón. */
border-radius: var(--radius-boton);
padding: var(--sp-2) var(--sp-4);
background-color: var(--c-accent);
background-image: var(--oro);

@ -7,6 +7,34 @@
import { navegacion, site } from '$lib/site';
let menuAbierto = $state(false);
let cabecera = $state<HTMLElement | null>(null);
/**
* La cabecera publica su altura real en `--altura-cabecera`.
*
* Ese valor lo usan tres cosas: el desplazamiento a un ancla, las barras
* laterales pegajosas y la portada, que se mete por debajo de la cabecera y
* necesita saber cuánto para compensarlo. Estaba escrito a mano en
* `app.css` y no coincidía —4,75rem contra los 70px reales—, así que todo
* lo que dependía de él quedaba desplazado.
*
* Se mide en vez de calcularse porque el menú móvil la hace crecer al
* desplegarse.
*/
$effect(() => {
if (!cabecera) return;
const observador = new ResizeObserver(([entrada]) => {
const alto = entrada.borderBoxSize?.[0]?.blockSize ?? entrada.contentRect.height;
document.documentElement.style.setProperty('--altura-cabecera', `${Math.ceil(alto)}px`);
});
observador.observe(cabecera);
return () => {
observador.disconnect();
document.documentElement.style.removeProperty('--altura-cabecera');
};
});
/** Una sección está activa también en sus páginas hijas. */
function activa(href: string): boolean {
@ -23,7 +51,7 @@
});
</script>
<header class="cabecera">
<header class="cabecera" bind:this={cabecera}>
<div class="container fila">
<a class="marca" href={resolve('/')}>{site.nombre}</a>
@ -184,7 +212,7 @@
min-width: 1.4em;
height: 1.4em;
padding-inline: 0.35em;
border-radius: 999px;
border-radius: var(--radius-boton);
background: var(--c-accent);
color: var(--c-accent-text);
font-size: var(--fs-xs);
@ -196,7 +224,7 @@
display: none;
padding: var(--sp-2);
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-boton);
background: transparent;
cursor: pointer;
}

@ -44,7 +44,7 @@
gap: var(--sp-2);
padding: var(--sp-1) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
font-size: var(--fs-sm);
text-decoration: none;
}

@ -120,7 +120,8 @@
display: flex;
align-items: center;
min-height: min(88vh, 46rem);
padding-block: var(--sp-8);
/* El alto de la cabecera, que flota encima, se devuelve aquí. */
padding-block: calc(var(--sp-8) + var(--altura-cabecera)) var(--sp-8);
overflow: hidden;
isolation: isolate;
}

@ -43,7 +43,8 @@
display: flex;
align-items: center;
min-height: min(88vh, 46rem);
padding-block: var(--sp-8);
/* El alto de la cabecera, que flota encima, se devuelve aquí. */
padding-block: calc(var(--sp-8) + var(--altura-cabecera)) var(--sp-8);
overflow: hidden;
isolation: isolate;
}

@ -125,7 +125,7 @@
.marca {
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
padding: 0 0.4em;
margin-right: var(--sp-1);
}

@ -173,7 +173,7 @@
.estrellas label:has(input:focus-visible) {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
border-radius: var(--radius);
border-radius: var(--radius-boton);
}
.retirar {

@ -65,7 +65,7 @@
aspect-ratio: 16 / 9;
overflow: hidden;
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-tarjeta);
background: var(--c-bg-sunken);
}

@ -258,7 +258,7 @@
flex: none;
padding: 0 0.4em;
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.06em;

@ -43,7 +43,7 @@
display: inline-flex;
padding: var(--sp-2);
border: none;
border-radius: var(--radius);
border-radius: var(--radius-boton);
background: transparent;
cursor: pointer;
}

@ -64,7 +64,7 @@
background: var(--c-bg-raised);
border: 1px solid var(--c-border);
border-bottom: 0;
border-radius: var(--radius) 0 0 0;
border-radius: var(--radius-tarjeta) 0 0 0;
}
.cabecera {
@ -88,7 +88,7 @@
border: none;
background: transparent;
cursor: pointer;
border-radius: var(--radius);
border-radius: var(--radius-boton);
}
.cerrar:hover {
@ -115,7 +115,7 @@
width: 100%;
padding: var(--sp-2);
border: none;
border-radius: var(--radius);
border-radius: var(--radius-boton);
background: transparent;
text-align: left;
cursor: pointer;

@ -64,7 +64,7 @@
padding: var(--sp-2) var(--sp-4);
background: var(--c-accent);
color: var(--c-accent-text);
border-radius: var(--radius);
border-radius: var(--radius-boton);
transition: top 0.15s ease;
}

@ -30,41 +30,52 @@
<Meta titulo={site.nombre} />
<Carrusel {laminas}>
{#snippet lamina(i)}
{#if i === 0}
<Heroe album={data.heroe} />
{:else if i === 1}
<LaminaCta
imagen={heroe.taller}
eyebrow="Taller de letras"
titulo="¿Quieres escribir tus propias letras?"
texto="Lo que he aprendido escribiendo, puesto en orden: los principios que hacen que una letra funcione y una ficha de creación por cada estilo hispanoamericano, con lo que hay que tener delante para escribir en él."
>
{#snippet acciones()}
<a class="btn btn--primary" href={resolve('/taller')}>Entrar al taller</a>
<a class="btn" href={resolve('/taller/lo-que-hace-que-una-letra-funcione')}>
Empezar por el principio
</a>
{/snippet}
</LaminaCta>
{:else}
<LaminaCta
imagen={heroe.cafe}
eyebrow="La tienda"
titulo="{CANCIONES_POR_PACK} canciones por un café"
texto="{formatearPrecio(
PRECIO_PACK_CENTIMOS
)} el pack, en MP3 y con todas sus versiones. En la web suena un fragmento; la descarga es el archivo completo, para llevártelo donde quieras."
>
{#snippet acciones()}
<a class="btn btn--primary" href={resolve('/musica')}>Elegir canciones</a>
<a class="btn" href={resolve('/carrito')}>Ver el carrito</a>
{/snippet}
</LaminaCta>
{/if}
{/snippet}
</Carrusel>
<!--
La portada empieza en el borde de la ventana y la cabecera flota encima. Por
eso la cabecera es translúcida con desenfoque: se apoya sobre la imagen en
vez de tapar una franja.
Se sube tirando de ella con un margen negativo del alto de la cabecera —que
la propia cabecera mide y publica—, y cada lámina se lo devuelve en relleno
para que su contenido siga cayendo donde estaba.
-->
<div class="portada">
<Carrusel {laminas}>
{#snippet lamina(i)}
{#if i === 0}
<Heroe album={data.heroe} />
{:else if i === 1}
<LaminaCta
imagen={heroe.taller}
eyebrow="Taller de letras"
titulo="¿Quieres escribir tus propias letras?"
texto="Lo que he aprendido escribiendo, puesto en orden: los principios que hacen que una letra funcione y una ficha de creación por cada estilo hispanoamericano, con lo que hay que tener delante para escribir en él."
>
{#snippet acciones()}
<a class="btn btn--primary" href={resolve('/taller')}>Entrar al taller</a>
<a class="btn" href={resolve('/taller/lo-que-hace-que-una-letra-funcione')}>
Empezar por el principio
</a>
{/snippet}
</LaminaCta>
{:else}
<LaminaCta
imagen={heroe.cafe}
eyebrow="La tienda"
titulo="{CANCIONES_POR_PACK} canciones por un café"
texto="{formatearPrecio(
PRECIO_PACK_CENTIMOS
)} el pack, en MP3 y con todas sus versiones. En la web suena un fragmento; la descarga es el archivo completo, para llevártelo donde quieras."
>
{#snippet acciones()}
<a class="btn btn--primary" href={resolve('/musica')}>Elegir canciones</a>
<a class="btn" href={resolve('/carrito')}>Ver el carrito</a>
{/snippet}
</LaminaCta>
{/if}
{/snippet}
</Carrusel>
</div>
{#if principal}
<!--
@ -155,6 +166,10 @@
{/if}
<style>
.portada {
margin-top: calc(var(--altura-cabecera) * -1);
}
.principal {
padding-block: var(--sp-7) var(--sp-5);
}

@ -180,7 +180,7 @@
aspect-ratio: 1;
object-fit: cover;
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-tarjeta);
background: var(--c-bg-sunken);
}

@ -112,7 +112,7 @@
width: 100%;
height: auto;
margin-block: var(--sp-5);
border-radius: var(--radius);
border-radius: var(--radius-tarjeta);
}
/* Prosa para leer: medida corta y aire entre párrafos. */

@ -310,7 +310,7 @@
aspect-ratio: 1;
object-fit: cover;
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-tarjeta);
background: var(--c-bg-sunken);
}

@ -272,7 +272,7 @@
.resumen {
padding: var(--sp-4);
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-tarjeta);
background: var(--c-bg-raised);
}
@ -333,7 +333,7 @@
.aviso {
padding: var(--sp-3);
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-tarjeta);
margin-bottom: var(--sp-4);
}

@ -170,7 +170,7 @@
color: inherit;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-boton);
background: var(--c-bg);
width: 100%;
}
@ -193,7 +193,7 @@
max-width: 34rem;
padding: var(--sp-4);
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-tarjeta);
margin-bottom: var(--sp-6);
}

@ -155,7 +155,7 @@
width: 100%;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-boton);
background: var(--c-bg);
}
@ -182,7 +182,7 @@
.error {
padding: var(--sp-3);
border: 1px solid var(--c-danger);
border-radius: var(--radius);
border-radius: var(--radius-boton);
color: var(--c-danger);
font-size: var(--fs-sm);
}

@ -66,7 +66,7 @@
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-4);
border: 1px solid var(--c-border);
border-radius: 999px;
border-radius: var(--radius-boton);
text-decoration: none;
transition:
border-color 0.2s ease,

@ -147,7 +147,7 @@
width: 100%;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-boton);
background: var(--c-bg-sunken);
color: inherit;
font: inherit;
@ -162,7 +162,7 @@
.vistas {
display: flex;
border: 1px solid var(--c-border);
border-radius: var(--radius);
border-radius: var(--radius-boton);
overflow: hidden;
}

@ -1,6 +1,7 @@
<script lang="ts">
import { resolve } from '$app/paths';
import Meta from '$lib/components/Meta.svelte';
import { brillo } from '$lib/acciones/brillo';
import { revelar } from '$lib/acciones/revelar';
let { data } = $props();
@ -52,9 +53,13 @@
<ul class="fichas">
{#each data.fichas as capitulo, i (capitulo.slug)}
<li use:revelar={Math.min(i, 8) * 45}>
<a class="ficha" href={resolve('/taller/[slug]', { slug: capitulo.slug })}>
<span class="titulo">{capitulo.titulo}</span>
<li class="tarjeta" use:revelar={Math.min(i, 8) * 45} use:brillo>
<div class="tarjeta__cuerpo">
<span class="titulo">
<a class="tarjeta__enlace" href={resolve('/taller/[slug]', { slug: capitulo.slug })}>
{capitulo.titulo}
</a>
</span>
{#if capitulo.ficha.origen}
<span class="origen muted">{capitulo.ficha.origen}</span>
{/if}
@ -62,7 +67,7 @@
{#if capitulo.ficha.compas}
<span class="compas">{capitulo.ficha.compas.split(';')[0]}</span>
{/if}
</a>
</div>
</li>
{/each}
</ul>
@ -144,35 +149,19 @@
padding: 0;
}
.ficha {
display: flex;
flex-direction: column;
gap: var(--sp-1);
height: 100%;
padding: var(--sp-4);
border: 1px solid var(--c-border);
border-radius: var(--radius);
text-decoration: none;
transition:
border-color 0.25s ease,
transform 0.25s ease;
}
.ficha:hover,
.ficha:focus-visible {
border-color: var(--c-accent);
transform: translateY(-3px);
.origen {
font-size: var(--fs-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.ficha .titulo {
.fichas .titulo {
font-family: var(--font-display);
font-size: var(--fs-xl);
}
.origen {
font-size: var(--fs-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
.fichas .titulo a {
text-decoration: none;
}
.compas {
@ -184,14 +173,8 @@
}
@media (prefers-reduced-motion: reduce) {
.ficha,
.principios .titulo {
transition: none;
}
.ficha:hover,
.ficha:focus-visible {
transform: none;
}
}
</style>

Loading…
Cancel
Save

Powered by TurnKey Linux.