El acceso deja de ser un salto de página: «Entrar» abre un diálogo con la imagen de marca a la izquierda y el formulario a la derecha, y al entrar se vuelve a donde se estaba en vez de aterrizar en /cuenta. `/entrar` sigue existiendo con el mismo formulario y la misma estampa, para quien llegue por un enlace directo o sin JavaScript. Tres cosas que salieron de probarlo, no de suponerlo: - `showModal()` enfocaba el campo trampa contra envíos automáticos: está fuera de pantalla y fuera del recorrido de tabulación, pero se puede enfocar. Ahora el campo se enfoca a mano. - Una regla de `display` en el diálogo le gana a la del navegador, que es hoja de usuario. El diálogo cerrado se pintaba igual: una pantalla de vacío al final de todas las páginas del sitio. - Los enlaces legales navegaban por debajo del modal. Van en pestaña aparte: se consultan y se vuelve con lo escrito intacto. Y aparte, el carrusel de la portada. Cada lámina lleva su fondo con `z-index: -1`, pero con `opacity: 1` la lámina no crea contexto de apilamiento, así que ese fondo se escapaba y se pintaba detrás del carrusel entero. Al pasar de lámina asomaba la foto de la de al lado y el fondo de la que entraba saltaba hacia atrás al terminar el fundido. Parecía que el shader se desincronizaba; lo que se movía era el orden de pintado. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>master
parent
f248ad57da
commit
383d0ce913
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Quién abre el modal de acceso y desde dónde.
|
||||
*
|
||||
* Vive fuera del componente porque quien lo abre —la cabecera, el carrito, un
|
||||
* botón de valorar— no es quien lo pinta: el diálogo se monta una sola vez en
|
||||
* el layout, y así no hay uno por página ni hace falta pasarse una propiedad
|
||||
* de arriba abajo.
|
||||
*/
|
||||
|
||||
class ControlDeAcceso {
|
||||
/** Si el diálogo está pedido. Lo baja el propio componente al cerrarse. */
|
||||
abierto = $state(false);
|
||||
|
||||
/**
|
||||
* A dónde volver cuando la sesión se abra.
|
||||
*
|
||||
* Desde el modal es la página en la que se está: quien pulsa «Entrar»
|
||||
* mientras mira un disco quiere seguir mirándolo, no aterrizar en su cuenta.
|
||||
*/
|
||||
volverA = $state('/cuenta');
|
||||
|
||||
/** El elemento que lo abrió, para devolverle el foco al cerrar. */
|
||||
#origen: HTMLElement | null = null;
|
||||
|
||||
abrir(volverA?: string): void {
|
||||
this.volverA = volverA ?? (typeof location === 'undefined' ? '/cuenta' : location.pathname);
|
||||
this.#origen = typeof document === 'undefined' ? null : (document.activeElement as HTMLElement);
|
||||
this.abierto = true;
|
||||
}
|
||||
|
||||
cerrar(): void {
|
||||
this.abierto = false;
|
||||
// Sin esto, el foco vuelve al principio del documento y quien navega con
|
||||
// teclado tiene que recorrer la cabecera entera para seguir donde estaba.
|
||||
this.#origen?.focus?.();
|
||||
this.#origen = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const acceso = new ControlDeAcceso();
|
||||
@ -0,0 +1,310 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
type Estado = {
|
||||
paso: 'email' | 'codigo';
|
||||
email: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
/** Estado inicial: en la página viene del servidor; en el modal, vacío. */
|
||||
inicial = { paso: 'email', email: '' } as Estado,
|
||||
/** A dónde ir cuando la sesión se abra. */
|
||||
volverA = '/cuenta',
|
||||
/**
|
||||
* En el modal no hay recarga: la respuesta se aplica aquí y se avisa. En
|
||||
* la página, `verificar` responde con una redirección y no llega a
|
||||
* llamarse.
|
||||
*/
|
||||
alEntrar = undefined as (() => void) | undefined,
|
||||
/** El campo del correo se enfoca solo cuando el formulario aparece. */
|
||||
enfocar = false
|
||||
} = $props();
|
||||
|
||||
/*
|
||||
* A partir de aquí el estado es de este componente y solo cambia con lo que
|
||||
* responda el servidor. `untrack` lo dice explícitamente: de `inicial` se
|
||||
* toma el valor de partida y nada más, que es justo lo que se quiere.
|
||||
*/
|
||||
let estado = $state<Estado>(untrack(() => ({ ...inicial })));
|
||||
let enviando = $state(false);
|
||||
let campoEmail = $state<HTMLInputElement | null>(null);
|
||||
let campoCodigo = $state<HTMLInputElement | null>(null);
|
||||
|
||||
/**
|
||||
* En el marcado, `{` abre una expresión de Svelte, así que el patrón del
|
||||
* campo se declara aquí en vez de pelearse con el escapado.
|
||||
*/
|
||||
const PATRON_CODIGO = '[0-9]{6}';
|
||||
|
||||
/*
|
||||
* Las acciones son siempre las de /entrar, aunque el formulario se pinte
|
||||
* dentro de un diálogo en cualquier otra página: la ruta es la dueña de la
|
||||
* lógica y del limitador de envíos, y no hay motivo para duplicarla.
|
||||
*/
|
||||
const ACCION_SOLICITAR = `${resolve('/entrar')}?/solicitar`;
|
||||
const ACCION_VERIFICAR = `${resolve('/entrar')}?/verificar`;
|
||||
|
||||
$effect(() => {
|
||||
if (!enfocar) return;
|
||||
// Al pasar al segundo paso, el foco salta al código: quien acaba de
|
||||
// recibirlo lo pega y ya está.
|
||||
const campo = estado.paso === 'codigo' ? campoCodigo : campoEmail;
|
||||
campo?.focus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Aplica la respuesta del servidor sin recargar la página.
|
||||
*
|
||||
* Se hace a mano en vez de con el `update()` de siempre porque este
|
||||
* formulario puede estar en una página que no es la dueña de la acción: allí
|
||||
* `form` no le llega, y el estado tiene que quedarse aquí.
|
||||
*/
|
||||
function alEnviar() {
|
||||
enviando = true;
|
||||
|
||||
return async ({ result }: { result: import('@sveltejs/kit').ActionResult }) => {
|
||||
enviando = false;
|
||||
|
||||
if (result.type === 'failure' || result.type === 'success') {
|
||||
const datos = result.data as Estado | undefined;
|
||||
if (datos?.paso) estado = { paso: datos.paso, email: datos.email, error: datos.error };
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.type === 'redirect') {
|
||||
// La sesión ya está abierta. Si hay quien quiera enterarse —el
|
||||
// modal, para cerrarse—, se le avisa; si no, se navega como haría
|
||||
// la página.
|
||||
await invalidateAll();
|
||||
if (alEntrar) alEntrar();
|
||||
else await applyAction(result);
|
||||
return;
|
||||
}
|
||||
|
||||
await applyAction(result);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if estado.paso === 'email'}
|
||||
<p class="entradilla">
|
||||
Escribe tu correo y te enviamos un código de seis dígitos. No hace falta contraseña: si es la
|
||||
primera vez, la cuenta se crea sola.
|
||||
</p>
|
||||
|
||||
{#if estado.error}
|
||||
<p class="error" role="alert">{estado.error}</p>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action={ACCION_SOLICITAR} use:enhance={alEnviar}>
|
||||
<input type="hidden" name="volverA" value={volverA} />
|
||||
|
||||
<!--
|
||||
Campo trampa: invisible y fuera del recorrido de tabulación, pero
|
||||
presente en el HTML. Los envíos automáticos suelen rellenarlo, y este
|
||||
formulario manda correo a terceros, así que conviene el freno.
|
||||
-->
|
||||
<div class="trampa" aria-hidden="true">
|
||||
<label for="companiaWeb">No rellenes este campo</label>
|
||||
<input id="companiaWeb" name="companiaWeb" type="text" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="campo">
|
||||
<label for="email">Correo electrónico</label>
|
||||
<input
|
||||
bind:this={campoEmail}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="tu@correo.com"
|
||||
value={estado.email}
|
||||
aria-invalid={estado.error ? 'true' : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary" type="submit" disabled={enviando}>
|
||||
{enviando ? 'Enviando…' : 'Enviarme el código'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!--
|
||||
En pestaña aparte, no en la misma.
|
||||
Desde el diálogo, un enlace normal navega por debajo del modal: la página
|
||||
cambia sin que se vea, y al cerrar apareces en otro sitio con el correo a
|
||||
medio escribir perdido. Aquí el texto legal se consulta y se vuelve.
|
||||
-->
|
||||
<p class="letra-pequena muted">
|
||||
Al entrar aceptas las <a
|
||||
href={resolve('/legal/[slug]', { slug: 'condiciones-de-uso' })}
|
||||
target="_blank"
|
||||
rel="noopener">condiciones de uso</a
|
||||
>
|
||||
y la
|
||||
<a href={resolve('/legal/[slug]', { slug: 'privacidad' })} target="_blank" rel="noopener"
|
||||
>política de privacidad</a
|
||||
>.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="entradilla">
|
||||
Hemos enviado un código a <strong>{estado.email}</strong>. Caduca en quince minutos.
|
||||
</p>
|
||||
|
||||
{#if estado.error}
|
||||
<p class="error" role="alert">{estado.error}</p>
|
||||
{/if}
|
||||
|
||||
<form method="POST" action={ACCION_VERIFICAR} use:enhance={alEnviar}>
|
||||
<input type="hidden" name="email" value={estado.email} />
|
||||
<input type="hidden" name="volverA" value={volverA} />
|
||||
|
||||
<div class="campo">
|
||||
<label for="codigo">Código</label>
|
||||
<input
|
||||
bind:this={campoCodigo}
|
||||
id="codigo"
|
||||
name="codigo"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
pattern={PATRON_CODIGO}
|
||||
maxlength="6"
|
||||
required
|
||||
class="codigo"
|
||||
aria-invalid={estado.error ? 'true' : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary" type="submit" disabled={enviando}>
|
||||
{enviando ? 'Comprobando…' : 'Entrar'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="secundarias">
|
||||
<form method="POST" action={ACCION_SOLICITAR} use:enhance={alEnviar}>
|
||||
<input type="hidden" name="email" value={estado.email} />
|
||||
<input type="hidden" name="volverA" value={volverA} />
|
||||
<button type="submit" class="enlace">Enviar otro código</button>
|
||||
</form>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="enlace"
|
||||
onclick={() => (estado = { paso: 'email', email: estado.email })}
|
||||
>
|
||||
Usar otro correo
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.entradilla {
|
||||
color: var(--c-text-muted);
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
gap: var(--sp-4);
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.campo {
|
||||
display: grid;
|
||||
gap: var(--sp-1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
padding: var(--sp-3);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-boton);
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
input:focus-visible {
|
||||
outline: 2px solid var(--c-accent);
|
||||
outline-offset: 1px;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.codigo {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--fs-xl);
|
||||
letter-spacing: 0.3em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[aria-invalid='true'] {
|
||||
border-color: var(--c-danger);
|
||||
}
|
||||
|
||||
/* Oculta sin display:none, que algunos clientes automáticos detectan. */
|
||||
.trampa {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: var(--sp-3);
|
||||
border: 1px solid var(--c-danger);
|
||||
border-radius: var(--radius-boton);
|
||||
color: var(--c-danger);
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
/* El botón ocupa el ancho: es la única acción del formulario. */
|
||||
form :global(button[type='submit']) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.secundarias {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-4);
|
||||
margin-top: var(--sp-4);
|
||||
}
|
||||
|
||||
.secundarias form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.enlace {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--c-text-muted);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.enlace:hover {
|
||||
color: var(--c-accent);
|
||||
}
|
||||
|
||||
.letra-pequena {
|
||||
margin: var(--sp-5) 0 0;
|
||||
font-size: var(--fs-xs);
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,292 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* El acceso, en un diálogo de dos mitades: la imagen de marca a la izquierda
|
||||
* y el formulario a la derecha.
|
||||
*
|
||||
* Se monta una sola vez, en el layout, y lo abre quien haga falta a través
|
||||
* de `acceso`. Entrar no es cambiar de sitio: quien pulsa «Entrar» mientras
|
||||
* mira un disco quiere seguir mirándolo, y mandarlo a otra página para
|
||||
* escribir un correo le hace perder lo que estaba haciendo.
|
||||
*
|
||||
* `/entrar` sigue existiendo y funcionando por su cuenta —sin JavaScript, o
|
||||
* desde un enlace directo—, con el mismo formulario y la misma estampa. Esto
|
||||
* es la versión cómoda, no la única.
|
||||
*
|
||||
* Se usa `<dialog>` de verdad, no un div con `role="dialog"`: el navegador ya
|
||||
* trae el fondo modal, el cierre con Escape, el foco atrapado dentro y el
|
||||
* resto de la página marcada como inerte. Reescribir todo eso a mano es la
|
||||
* forma habitual de acabar con un modal del que no se puede salir con el
|
||||
* teclado.
|
||||
*/
|
||||
import { acceso } from '$lib/acceso.svelte';
|
||||
import { heroe } from '$lib/site';
|
||||
import FormularioAcceso from './FormularioAcceso.svelte';
|
||||
import Icono from './Icono.svelte';
|
||||
|
||||
let dialogo = $state<HTMLDialogElement | null>(null);
|
||||
|
||||
/** La imagen de marca, la misma de la portada. */
|
||||
const imagen = heroe.fondo.oscuro;
|
||||
|
||||
$effect(() => {
|
||||
if (!dialogo) return;
|
||||
|
||||
if (acceso.abierto && !dialogo.open) {
|
||||
dialogo.showModal();
|
||||
|
||||
/*
|
||||
* Y se enfoca el campo a mano.
|
||||
*
|
||||
* `showModal()` enfoca por su cuenta lo primero que encuentre alcanzable
|
||||
* dentro del diálogo, y lo primero es el campo trampa contra envíos
|
||||
* automáticos: está fuera de la pantalla y fuera del recorrido de
|
||||
* tabulación, pero se puede enfocar. El resultado era un diálogo que se
|
||||
* abría con el foco en ninguna parte visible.
|
||||
*/
|
||||
dialogo.querySelector<HTMLInputElement>('input[name="email"], input[name="codigo"]')?.focus();
|
||||
} else if (!acceso.abierto && dialogo.open) {
|
||||
dialogo.close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
`onclose` cubre las salidas que no pasan por nuestro botón: Escape, y el
|
||||
cierre del propio navegador. Sin esto el estado se quedaría en «abierto» y no
|
||||
se podría volver a abrir.
|
||||
-->
|
||||
<dialog
|
||||
bind:this={dialogo}
|
||||
class="acceso"
|
||||
aria-labelledby="titulo-acceso"
|
||||
onclose={() => acceso.cerrar()}
|
||||
onclick={(evento) => {
|
||||
// Pulsar en el fondo cierra. El fondo es el propio <dialog>: su caja
|
||||
// ocupa la pantalla entera y el contenido va en un hijo, así que un
|
||||
// clic cuyo destino sea el diálogo mismo es un clic fuera.
|
||||
if (evento.target === dialogo) acceso.cerrar();
|
||||
}}
|
||||
>
|
||||
<div class="marco">
|
||||
<!--
|
||||
La mitad de la imagen. `aria-hidden` porque no cuenta nada que no esté
|
||||
escrito al lado; para quien no la ve, el diálogo empieza en el título.
|
||||
-->
|
||||
<div class="ilustracion" aria-hidden="true">
|
||||
<img
|
||||
src={imagen.src}
|
||||
srcset={imagen.srcset}
|
||||
sizes="20rem"
|
||||
width={imagen.ancho}
|
||||
height={imagen.alto}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
<div class="sobreimpreso">
|
||||
<p class="marca">Senza Paura</p>
|
||||
<p class="lema">
|
||||
Una cuenta guarda lo que compras, lo que marcas como favorito y lo que has valorado. Sin
|
||||
contraseñas que recordar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2 id="titulo-acceso">Entrar o crear cuenta</h2>
|
||||
|
||||
{#if acceso.abierto}
|
||||
<!--
|
||||
Se monta y se desmonta con el diálogo: así el formulario vuelve al
|
||||
primer paso cada vez que se abre, en vez de reaparecer con el
|
||||
código de la vez anterior a medio escribir.
|
||||
-->
|
||||
<FormularioAcceso volverA={acceso.volverA} enfocar alEntrar={() => acceso.cerrar()} />
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
Al final a propósito. `showModal()` enfoca el primer elemento
|
||||
alcanzable del diálogo: con el botón de cerrar delante, el foco caía
|
||||
ahí y había que tabular para llegar al campo. Puesto aquí, se entra
|
||||
directamente en el correo y el cierre queda al terminar, que es
|
||||
cuando hace falta.
|
||||
-->
|
||||
<button type="button" class="cerrar" onclick={() => acceso.cerrar()} aria-label="Cerrar">
|
||||
<Icono nombre="cerrar" tamano={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
/*
|
||||
* El diálogo ocupa toda la pantalla y hace de fondo; el contenido va en
|
||||
* `.marco`, centrado. Así el clic fuera se detecta sin poner un div extra
|
||||
* que tape el fondo nativo.
|
||||
*/
|
||||
.acceso {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
margin: 0;
|
||||
padding: var(--sp-4);
|
||||
border: none;
|
||||
background: transparent;
|
||||
overflow: auto;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
/*
|
||||
* El `display` va SOLO en el estado abierto.
|
||||
*
|
||||
* El navegador oculta un `<dialog>` cerrado con `display: none`, pero eso es
|
||||
* hoja de usuario y cualquier regla nuestra le gana, por poca especificidad
|
||||
* que tenga. Con `display: grid` suelto en `.acceso`, el diálogo cerrado se
|
||||
* pintaba igualmente: novecientos píxeles transparentes al final de todas las
|
||||
* páginas del sitio, invisibles pero con su scroll y con su encabezado
|
||||
* contando en el esquema de la página.
|
||||
*/
|
||||
.acceso[open] {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.acceso::backdrop {
|
||||
/* En variable: el minificador junta las funciones del filtro sin espacio. */
|
||||
background: rgb(3 8 20 / 72%);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.marco {
|
||||
display: grid;
|
||||
grid-template-columns: 20rem minmax(0, 24rem);
|
||||
width: 100%;
|
||||
max-width: 44rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-tarjeta);
|
||||
background: var(--c-bg-raised);
|
||||
box-shadow: 0 24px 64px rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
/* --- La mitad de la imagen ------------------------------------------- */
|
||||
|
||||
.ilustracion {
|
||||
position: relative;
|
||||
background: var(--c-bg-sunken);
|
||||
}
|
||||
|
||||
/*
|
||||
* Recortada por la izquierda: la imagen tiene las pinceladas en las esquinas
|
||||
* y el centro casi liso, así que centrarla dejaba un rectángulo azul plano.
|
||||
*/
|
||||
.ilustracion img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 12% center;
|
||||
}
|
||||
|
||||
/*
|
||||
* El velo carga abajo, que es donde va el texto, y deja arriba la textura a
|
||||
* la vista: si se atenúa entera, la imagen deja de aportar y solo estorba.
|
||||
*/
|
||||
.sobreimpreso {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-5);
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
color-mix(in oklab, #030814 88%, transparent) 0%,
|
||||
color-mix(in oklab, #030814 40%, transparent) 45%,
|
||||
transparent 75%
|
||||
);
|
||||
}
|
||||
|
||||
.marca {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--fs-xl);
|
||||
background: var(--oro);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.lema {
|
||||
margin: 0;
|
||||
font-size: var(--fs-sm);
|
||||
line-height: 1.5;
|
||||
/* Sobre la imagen el color va fijo: no lo cambia el tema. */
|
||||
color: rgb(255 255 255 / 78%);
|
||||
}
|
||||
|
||||
/* --- La mitad del formulario ----------------------------------------- */
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
padding: var(--sp-6);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--sp-3);
|
||||
font-size: var(--fs-xl);
|
||||
}
|
||||
|
||||
.cerrar {
|
||||
position: absolute;
|
||||
top: var(--sp-3);
|
||||
right: var(--sp-3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 2.25rem: lo mínimo para acertarle con el dedo. */
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--radius-boton);
|
||||
background: transparent;
|
||||
color: var(--c-text-muted);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s var(--suave);
|
||||
}
|
||||
|
||||
.cerrar:hover,
|
||||
.cerrar:focus-visible {
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
/*
|
||||
* En pantallas estrechas la imagen pasa a ser una franja arriba: dos
|
||||
* columnas de diez centímetros no son dos columnas, son dos rendijas.
|
||||
*/
|
||||
@media (max-width: 44rem) {
|
||||
.marco {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
max-width: 26rem;
|
||||
}
|
||||
|
||||
.ilustracion {
|
||||
height: 9rem;
|
||||
}
|
||||
|
||||
.sobreimpreso {
|
||||
padding: var(--sp-4);
|
||||
}
|
||||
|
||||
.lema {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Con la pantalla baja —un móvil en horizontal— la franja sobra del todo. */
|
||||
@media (max-height: 34rem) {
|
||||
.ilustracion {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue