Módulo 12: Diseño responsive

Objetivos del módulo

  • Entender qué es el diseño responsive y sus pilares
  • Configurar correctamente la etiqueta meta viewport
  • Comparar los enfoques mobile first y desktop first
  • Escribir media queries con la sintaxis moderna de rangos
  • Adaptar el diseño a preferencias del usuario: movimiento, tema, tipo de puntero
  • Usar container queries para componentes que se adaptan a su contenedor
  • Servir imágenes responsive con srcset, sizes y <picture>
  • Crear tipografía fluida con clamp()
  • Construir un menú responsive sin JavaScript
  • Probar los diseños con las DevTools y maquetar una página completa adaptable

1. ¿Qué es el diseño responsive?

El diseño web responsive (responsive web design) consiste en crear una sola web que se adapta a cualquier tamaño de pantalla: móvil, tableta, portátil, monitor ultrapanorámico o televisor. El término lo acuñó Ethan Marcotte en 2010 y se apoya en tres pilares:

  1. Rejillas fluidas: tamaños relativos (%, fr, flex) en lugar de anchos fijos en píxeles.
  2. Imágenes flexibles: que nunca desborden su contenedor.
  3. Media queries: reglas CSS que solo se aplican en determinadas condiciones.

📘 Concepto: Piensa en el agua: se adapta a la forma del recipiente que la contiene. Una web responsive no tiene “un tamaño”: fluye y se reorganiza según el espacio disponible. En lugar de diseñar para “el iPhone X” o “el monitor de 1920px”, diseñas para cualquier ancho.

Hoy la mayoría del tráfico web procede de dispositivos móviles, y Google indexa principalmente la versión móvil de las páginas (mobile-first indexing). Una web que no se adapta no es solo incómoda: pierde visitas y posicionamiento.

Lo que ya sabes hacer

Buena parte del trabajo responsive ya lo conoces de módulos anteriores, y conviene aprovecharlo antes de escribir media queries:

Técnica Módulo
max-width: 100% en imágenes 03 y 09
Unidades relativas (rem, %, ch, dvh) 08
min(), max(), clamp() 08
flex-wrap y flex: 1 1 250px 10
repeat(auto-fit, minmax(250px, 1fr)) 11

💡 Consejo: El mejor CSS responsive es el que no necesita media queries: diseños intrínsecamente flexibles que se adaptan solos. Las media queries son para los cambios que no puedes conseguir de otro modo (reorganizar un layout, mostrar u ocultar un menú).


2. La etiqueta meta viewport

Sin esta etiqueta, los navegadores móviles simulan una pantalla de escritorio (normalmente de unos 980px) y la encogen para que quepa: el texto se ve diminuto y tus media queries no funcionan como esperas.

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Mi web responsive</title>
</head>
Parte Significado
width=device-width El ancho del viewport es el ancho real del dispositivo (en píxeles CSS)
initial-scale=1 Sin zoom inicial

⚠️ Importante: Nunca añadas maximum-scale=1 ni user-scalable=no. Impiden que el usuario haga zoom, lo que es un grave problema de accesibilidad para personas con baja visión (y un incumplimiento de las WCAG).


3. Mobile first frente a desktop first

Son dos estrategias para escribir el CSS responsive:

  Mobile first Desktop first
Estilos base (sin media query) Para móvil Para escritorio
Las media queries… Añaden complejidad al crecer Quitan complejidad al encoger
Condición típica width >= 768px (min-width) width < 768px (max-width)
Ventajas CSS más simple; los móviles no descargan ni procesan estilos que no usan; obliga a priorizar el contenido Natural si partes de un diseño de escritorio ya hecho
Usado por Bootstrap, Tailwind y la mayoría de frameworks Proyectos antiguos
/* ✓ MOBILE FIRST: base para móvil, se amplía para pantallas grandes */
.productos {
    display: grid;
    grid-template-columns: 1fr;        /* móvil: una columna */
    gap: 1rem;
}

@media (width >= 600px) {
    .productos { grid-template-columns: repeat(2, 1fr); }   /* tableta: dos */
}

@media (width >= 1000px) {
    .productos { grid-template-columns: repeat(4, 1fr); }   /* escritorio: cuatro */
}
/* DESKTOP FIRST: base para escritorio, se reduce para pantallas pequeñas */
.productos {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1rem;
}

@media (width < 1000px) {
    .productos { grid-template-columns: repeat(2, 1fr); }
}

@media (width < 600px) {
    .productos { grid-template-columns: 1fr; }
}

💡 Consejo: Usa mobile first. En móvil casi todo es una columna apilada, que es justo el comportamiento por defecto de HTML: el CSS base es mínimo y vas añadiendo columnas a medida que hay espacio. Es lo que hace Bootstrap, que estudiarás en el módulo 15.


4. Media queries

4.1. Sintaxis

@media tipo-de-medio and (característica) {
    /* reglas que solo se aplican si se cumple la condición */
}
@media screen and (width >= 768px) { ... }   /* pantallas de al menos 768px */
@media print { ... }                          /* al imprimir */
@media (width >= 768px) { ... }               /* el tipo es opcional: equivale a "all" */
Tipo de medio Uso
all Todos (por defecto)
screen Pantallas
print Impresión y vista previa de impresión

4.2. La sintaxis de rangos moderna

Tradicionalmente se escribía min-width y max-width, que resultan confusos (“¿min-width es mayor o igual?”). Hoy todos los navegadores actuales admiten la sintaxis de rangos, mucho más legible:

Sintaxis clásica Sintaxis de rangos Significado
(min-width: 768px) (width >= 768px) Ancho de 768px o más
(max-width: 767.98px) (width < 768px) Ancho menor de 768px
(min-width: 768px) and (max-width: 1023.98px) (768px <= width < 1024px) Entre 768 y 1024px (sin incluir 1024)
(min-height: 600px) (height >= 600px) Alto de 600px o más
/* Solo tabletas: entre 600px (incluido) y 1000px (excluido) */
@media (600px <= width < 1000px) {
    .aviso-tableta { display: block; }
}

💡 Consejo: Con la sintaxis clásica, la combinación min-width: 768px / max-width: 768px hacía que en exactamente 768px se cumplieran ambas; por eso verás valores como 767.98px en código antiguo (incluido Bootstrap). Con >= y < el problema desaparece.

4.3. Combinar condiciones

@media (width >= 768px) and (orientation: landscape) { ... }   /* y */
@media (width < 600px), print { ... }                          /* o (la coma) */
@media not print { ... }                                       /* negación */

4.4. Puntos de ruptura (breakpoints)

Los breakpoints son los anchos en los que cambia el diseño. Estos son los de Bootstrap 5, una referencia muy extendida:

Nombre Desde Dispositivos típicos
(base) 0 Móviles en vertical
sm 576px Móviles grandes, en horizontal
md 768px Tabletas
lg 992px Portátiles, tabletas en horizontal
xl 1200px Escritorio
xxl 1400px Monitores grandes

⚠️ Importante: No elijas breakpoints pensando en dispositivos concretos (salen modelos nuevos cada mes). Elige los puntos en los que tu contenido empieza a verse mal: abre la página, ensancha poco a poco la ventana y, cuando algo “se rompa” o quede demasiado vacío, ahí va un breakpoint.

⚠️ Importante: Las media queries no suman especificidad. Si una regla dentro de una media query no se aplica, comprueba que va después de la regla base en el fichero: a igual especificidad, gana la última.

4.5. Orientación

@media (orientation: landscape) {
    .galeria { grid-template-columns: repeat(4, 1fr); }   /* más ancho que alto */
}

@media (orientation: portrait) {
    .galeria { grid-template-columns: repeat(2, 1fr); }   /* más alto que ancho */
}

4.6. Preferencias del usuario

Las media queries no solo miran el tamaño: también detectan preferencias configuradas en el sistema operativo.

/* El usuario ha pedido reducir el movimiento (vértigo, trastornos vestibulares...) */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* El usuario prefiere el tema oscuro (lo viste en el módulo 08) */
@media (prefers-color-scheme: dark) {
    :root { --color-fondo: #0f172a; --color-texto: #e2e8f0; }
}

/* El usuario ha pedido más contraste */
@media (prefers-contrast: more) {
    :root { --color-texto-suave: #000; }
}

4.7. Tipo de puntero: hover y pointer

El ancho de pantalla no te dice si el usuario usa ratón o dedo (hay tabletas enormes y portátiles táctiles). Para eso existen:

Característica Valores Significado
hover hover / none ¿El dispositivo principal permite pasar el puntero por encima?
pointer fine / coarse / none ¿Puntero preciso (ratón) o impreciso (dedo)?
any-hover, any-pointer Igual Considera cualquier dispositivo conectado, no solo el principal
/* Efectos de hover solo donde existe el hover real (evita el "hover pegado" en móviles) */
@media (hover: hover) {
    .tarjeta:hover { transform: translateY(-4px); box-shadow: 0 10px 20px rgb(0 0 0 / 0.15); }
}

/* En pantallas táctiles, objetivos más grandes */
@media (pointer: coarse) {
    .boton { min-height: 48px; padding-inline: 1.25rem; }
}

4.8. Hoja de estilos para impresión

@media print {
    nav, footer, .anuncios, .boton { display: none; }   /* fuera lo que no tiene sentido en papel */
    body { color: black; background: white; font-size: 12pt; }
    a[href^="http"]::after { content: " (" attr(href) ")"; }  /* muestra la URL de los enlaces */
    h2, h3 { break-after: avoid; }                       /* no dejar un título solo al final de página */
}

5. Container queries

Las media queries miran el tamaño de la ventana. Pero un componente (una tarjeta, por ejemplo) puede aparecer en una columna estrecha lateral y en el contenido principal ancho en la misma pantalla. Lo que de verdad le importa es el tamaño de su contenedor.

📘 Concepto: Una container query aplica estilos según el tamaño de un contenedor ancestro, no de la ventana. Es como un mueble modular que se reorganiza según el hueco donde lo pongas, sin importar el tamaño de la casa. Los componentes se vuelven verdaderamente reutilizables.

5.1. Declarar un contenedor y consultarlo

<div class="zona-tarjeta">
    <article class="tarjeta">
        <img src="cafe.jpg" alt="Taza de café sobre una mesa de madera">
        <div class="tarjeta__cuerpo">
            <h3>Café de especialidad</h3>
            <p>Granos seleccionados de Etiopía y Colombia, tostados cada semana.</p>
        </div>
    </article>
</div>
/* 1. Convertimos el envoltorio en un contenedor consultable */
.zona-tarjeta {
    container-type: inline-size;   /* podremos preguntar por su ancho */
    container-name: tarjeta;       /* nombre opcional */
}

/* 2. Diseño base: imagen arriba, texto abajo */
.tarjeta {
    display: grid;
    gap: 1rem;
}

.tarjeta img {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
}

/* 3. Si el CONTENEDOR mide 450px o más: imagen a la izquierda */
@container tarjeta (width >= 450px) {
    .tarjeta {
        grid-template-columns: 200px 1fr;
        align-items: center;
    }
    .tarjeta img {
        aspect-ratio: 1;
        height: 100%;
    }
}

Ahora la misma tarjeta se muestra en vertical dentro de una barra lateral de 300px y en horizontal en el contenido principal de 800px, sin que tengas que crear clases distintas.

Valor de container-type Se puede consultar
inline-size El ancho (lo más habitual)
size Ancho y alto (el contenedor necesita un alto definido)
normal Nada de tamaño (valor por defecto)
/* Abreviada: container: nombre / tipo */
.zona-tarjeta { container: tarjeta / inline-size; }

⚠️ Importante: Un elemento no puede consultarse a sí mismo: las reglas dentro de @container se aplican a los descendientes del contenedor. Por eso se usa un envoltorio. Además, un contenedor con inline-size ya no puede tomar su ancho de su contenido, así que debe recibir el ancho de su padre (lo normal en elementos de bloque, ítems grid o ítems flex con flex: 1).

5.2. Unidades de contenedor

Unidad Equivale a
cqi 1 % del tamaño inline (ancho) del contenedor
cqb 1 % del tamaño block (alto) del contenedor
cqw / cqh 1 % del ancho / alto del contenedor
cqmin / cqmax 1 % de la dimensión menor / mayor
/* El título de la tarjeta escala con el ancho de su contenedor, no de la ventana */
.tarjeta h3 {
    font-size: clamp(1.1rem, 4cqi + 0.5rem, 1.75rem);
}

Media queries frente a container queries

  Media queries Container queries
Consultan La ventana y el dispositivo Un contenedor ancestro
Ideales para Layout general de la página Componentes reutilizables
Preferencias del usuario Sí (prefers-*, hover…) No
Requiere preparar el HTML/CSS No Sí: declarar container-type

6. Imágenes responsive

6.1. La regla básica

img, video {
    max-width: 100%;   /* nunca más anchas que su contenedor */
    height: auto;      /* mantienen la proporción */
}

Indica siempre width y height en el HTML: el navegador calcula la proporción y reserva el hueco antes de que la imagen cargue, evitando saltos de contenido (CLS). El CSS anterior la sigue haciendo fluida.

<img src="paisaje.jpg" alt="Lago entre montañas" width="1200" height="800" loading="lazy">

6.2. Repaso: srcset y sizes

Como viste en el módulo 03, srcset ofrece varias versiones de la misma imagen y el navegador elige la más adecuada según el ancho de pantalla y la densidad de píxeles. Así un móvil no descarga una imagen de 2000px.

<img
    src="paisaje-800.jpg"
    srcset="paisaje-400.jpg 400w,
            paisaje-800.jpg 800w,
            paisaje-1600.jpg 1600w"
    sizes="(width >= 1000px) 50vw, 100vw"
    alt="Lago entre montañas al amanecer"
    width="1600" height="1067">
  • 400w, 800w… indican el ancho real de cada fichero.
  • sizes indica cuánto ocupará la imagen en pantalla: a partir de 1000px, la mitad del viewport; por debajo, todo el ancho.

6.3. Repaso: <picture> para dirección de arte

Cuando no quieres la misma imagen más pequeña, sino otro encuadre (un retrato recortado en móvil, una panorámica en escritorio) o un formato moderno con respaldo:

<picture>
    <source media="(width >= 900px)" srcset="equipo-panoramica.avif" type="image/avif">
    <source media="(width >= 900px)" srcset="equipo-panoramica.jpg">
    <source srcset="equipo-recorte.avif" type="image/avif">
    <img src="equipo-recorte.jpg" alt="El equipo de la empresa en la oficina" width="800" height="1000">
</picture>

💡 Consejo: Para imágenes de fondo en CSS puedes cambiar de fichero dentro de una media query (background-image distinto en móvil y escritorio): el navegador solo descarga la que se aplica.


7. Tipografía fluida con clamp()

En lugar de saltar de tamaño en cada breakpoint (16px, luego 18px, luego 20px), la tipografía fluida crece de forma continua con la ventana, entre un mínimo y un máximo.

/* clamp(MÍNIMO, PREFERIDO, MÁXIMO) */
h1 {
    font-size: clamp(2rem, 1.2rem + 4vw, 4rem);
}
  • En pantallas pequeñas, el valor preferido sería menor de 2rem: se usa 2rem (32px).
  • En pantallas grandes, superaría 4rem: se usa 4rem (64px).
  • Entre medias, el tamaño crece suavemente con el ancho de la ventana.

⚠️ Importante: No uses solo vw como valor preferido (clamp(2rem, 5vw, 4rem)): las unidades vw no responden al zoom del navegador, y un usuario que amplía la página no vería crecer el texto. Sumar una parte en rem (1.2rem + 4vw) garantiza que el zoom siga funcionando.

Escala tipográfica fluida con variables

:root {
    --texto-sm:  clamp(0.875rem, 0.85rem + 0.1vw, 0.95rem);
    --texto-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
    --texto-lg:  clamp(1.25rem, 1.1rem + 0.6vw, 1.6rem);
    --texto-xl:  clamp(1.6rem, 1.3rem + 1.4vw, 2.5rem);
    --texto-xxl: clamp(2.2rem, 1.5rem + 3vw, 4rem);

    /* También los espacios pueden ser fluidos */
    --espacio-seccion: clamp(3rem, 2rem + 5vw, 7rem);
}

body  { font-size: var(--texto-base); }
h1    { font-size: var(--texto-xxl); line-height: 1.1; }
h2    { font-size: var(--texto-xl); line-height: 1.2; }
h3    { font-size: var(--texto-lg); }
small { font-size: var(--texto-sm); }

section { padding-block: var(--espacio-seccion); }

💡 Consejo: Existen generadores en línea de escalas tipográficas fluidas (busca “fluid type scale calculator”): eliges el tamaño mínimo y máximo y los anchos de pantalla, y te dan las fórmulas clamp() listas para copiar.


8. Menú responsive

El patrón más clásico: en escritorio, los enlaces en fila; en móvil, ocultos tras un botón “hamburguesa” (☰). Veamos dos versiones sin JavaScript.

8.1. Con <details> y <summary>

El elemento <details> ya sabe abrirse y cerrarse solo, es accesible con teclado y los lectores de pantalla anuncian si está expandido.

<header class="cabecera">
    <a href="/" class="cabecera__logo">MiMarca</a>
    <details class="menu-movil">
        <summary class="menu-movil__boton" aria-label="Abrir menú">☰</summary>
        <nav class="menu-movil__panel">
            <a href="#">Inicio</a>
            <a href="#">Servicios</a>
            <a href="#">Contacto</a>
        </nav>
    </details>
</header>
.cabecera {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    position: relative;
}

.menu-movil__boton {
    list-style: none;        /* quita el triángulo por defecto */
    cursor: pointer;
    font-size: 1.75rem;
    padding: 0.25rem 0.75rem;
}
.menu-movil__boton::-webkit-details-marker { display: none; }  /* Safari */

.menu-movil__panel {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    background-color: white;
    box-shadow: 0 8px 16px rgb(0 0 0 / 0.1);
}

.menu-movil__panel a {
    padding: 1rem;
    border-top: 1px solid #e5e7eb;
}

En escritorio, lo más sencillo es tener una segunda <nav> horizontal y mostrar u ocultar cada una con una media query:

.menu-escritorio { display: none; }

@media (width >= 768px) {
    .menu-movil      { display: none; }
    .menu-escritorio { display: flex; gap: 1.5rem; }
}

8.2. Con un checkbox oculto

Una técnica muy extendida (el checkbox hack): un <label> actúa como botón que marca y desmarca un checkbox invisible, y el CSS muestra el menú cuando está marcado.

<header class="cabecera">
    <a href="/" class="cabecera__logo">MiMarca</a>

    <input type="checkbox" id="menu-toggle" class="menu-toggle">
    <label for="menu-toggle" class="menu-icono" aria-label="Mostrar u ocultar el menú">☰</label>

    <nav class="menu">
        <a href="#">Inicio</a>
        <a href="#">Servicios</a>
        <a href="#">Blog</a>
        <a href="#">Contacto</a>
    </nav>
</header>
/* ---------- Móvil (base) ---------- */
.cabecera {
    display: flex;
    flex-wrap: wrap;               /* el menú pasa a la línea de abajo */
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
}

.menu-toggle {
    position: absolute;            /* oculto visualmente, pero enfocable con teclado */
    opacity: 0;
}

.menu-icono {
    font-size: 1.75rem;
    cursor: pointer;
}

.menu-toggle:focus-visible + .menu-icono {
    outline: 3px solid #f59e0b;    /* foco visible en el "botón" al usar el teclado */
}

.menu {
    display: none;                 /* oculto por defecto en móvil */
    flex-basis: 100%;              /* ocupa toda una línea */
    flex-direction: column;
}

.menu-toggle:checked ~ .menu {
    display: flex;                 /* ¡marcado = menú visible! */
}

.menu a {
    padding: 0.75rem 0;
    border-top: 1px solid #e5e7eb;
}

/* ---------- Escritorio ---------- */
@media (width >= 768px) {
    .menu-icono { display: none; }

    .menu {
        display: flex;             /* siempre visible */
        flex-basis: auto;
        flex-direction: row;
        gap: 1.5rem;
    }

    .menu a { border: none; padding: 0; }
}

⚠️ Importante: Ambas técnicas son válidas para aprender y para webs sencillas, pero tienen límites de accesibilidad: un lector de pantalla anuncia el checkbox como “casilla de verificación”, no como botón de menú. La solución más accesible usa un <button> con aria-expanded y unas líneas de JavaScript, que es exactamente lo que hace el componente navbar de Bootstrap (módulo 16).


9. Probar el diseño responsive

Modo dispositivo de las DevTools

Pulsa Ctrl + Mayús + M con las DevTools abiertas (o el icono de móvil y tableta):

Función Para qué sirve
Modo Responsive Arrastra los bordes para probar cualquier ancho; es el más útil
Lista de dispositivos Simula tamaños y densidad de modelos concretos
Barra de media queries Muestra tus breakpoints como franjas de colores; clic para saltar a cada uno
Rotación Cambia entre vertical y horizontal
Simulación de red (throttling) Prueba la carga con una conexión 3G o 4G lenta
Panel Rendering Emula prefers-color-scheme, prefers-reduced-motion, impresión…

💡 Consejo: No te limites a los tamaños de la lista: arrastra el ancho lentamente desde 320px hasta 2000px y observa cuándo algo se ve mal. Comprueba también con el zoom del navegador al 200 % (requisito de accesibilidad) y prueba en al menos un móvil real: la simulación no reproduce el tacto, la barra del navegador ni el rendimiento.

Lista de comprobación

  • ¿Hay scroll horizontal en algún ancho? (busca el elemento culpable en DevTools)
  • ¿Las imágenes se adaptan sin deformarse?
  • ¿El texto es legible en móvil (mínimo 16px) y no forma líneas larguísimas en escritorio?
  • ¿Los botones y enlaces son fáciles de tocar (unos 44-48px)?
  • ¿El menú funciona con teclado?
  • ¿Las tablas anchas tienen scroll propio?

10. Caso práctico: página completa adaptable

Vamos a maquetar la portada de una cafetería con enfoque mobile first: cabecera con menú, sección principal (hero), rejilla de productos, bloque de testimonios con container queries y pie.

HTML

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Café Aroma</title>
    <link rel="stylesheet" href="css/estilos.css">
</head>
<body>
    <header class="cabecera">
        <a href="/" class="cabecera__logo">☕ Café Aroma</a>
        <input type="checkbox" id="menu-toggle" class="menu-toggle">
        <label for="menu-toggle" class="menu-icono" aria-label="Mostrar u ocultar el menú">☰</label>
        <nav class="menu">
            <a href="#carta">Carta</a>
            <a href="#opiniones">Opiniones</a>
            <a href="#contacto">Contacto</a>
        </nav>
    </header>

    <main>
        <section class="hero">
            <h1>El mejor café del barrio</h1>
            <p>Tostado artesanal cada semana. Ven a probarlo.</p>
            <a href="#carta" class="boton">Ver la carta</a>
        </section>

        <section id="carta" class="seccion">
            <h2>Nuestra carta</h2>
            <div class="productos">
                <article class="producto">
                    <img src="img/espresso.jpg" alt="Taza de espresso" width="600" height="450">
                    <h3>Espresso</h3>
                    <p>1,50 €</p>
                </article>
                <!-- ...más productos... -->
            </div>
        </section>

        <section id="opiniones" class="seccion opiniones">
            <h2>Lo que dicen de nosotros</h2>
            <div class="opiniones__lista">
                <blockquote class="opinion">
                    <p>"El café con leche más rico de la ciudad."</p>
                    <footer>— Marta G.</footer>
                </blockquote>
                <!-- ...más opiniones... -->
            </div>
        </section>
    </main>

    <footer id="contacto" class="pie">
        <p>Calle Mayor 12, Valencia · Abierto de 8:00 a 20:00</p>
    </footer>
</body>
</html>

CSS

/* ========== Base y variables ========== */
*, *::before, *::after { box-sizing: border-box; }

:root {
    --color-marca: #6f4e37;
    --color-fondo: #fdf8f3;
    --color-texto: #2b2118;
    --texto-xxl: clamp(2.2rem, 1.5rem + 3vw, 4rem);
    --espacio-seccion: clamp(3rem, 2rem + 4vw, 6rem);
}

body {
    margin: 0;
    font-family: system-ui, sans-serif;
    line-height: 1.6;
    color: var(--color-texto);
    background-color: var(--color-fondo);
}

img { max-width: 100%; height: auto; display: block; }

.seccion {
    width: min(100% - 2rem, 1100px);
    margin-inline: auto;
    padding-block: var(--espacio-seccion);
}

/* ========== Cabecera (móvil) ========== */
.cabecera {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    background-color: white;
    box-shadow: 0 2px 6px rgb(0 0 0 / 0.08);
}

.cabecera__logo { font-weight: 700; color: var(--color-marca); text-decoration: none; }
.menu-toggle    { position: absolute; opacity: 0; }
.menu-icono     { font-size: 1.75rem; cursor: pointer; }
.menu           { display: none; flex-basis: 100%; flex-direction: column; }
.menu a         { padding: 0.75rem 0; color: inherit; text-decoration: none; }
.menu-toggle:checked ~ .menu { display: flex; }
.menu-toggle:focus-visible + .menu-icono { outline: 3px solid var(--color-marca); }

/* ========== Hero ========== */
.hero {
    display: grid;
    place-items: center;
    gap: 1rem;
    min-height: 70svh;
    padding: 2rem 1rem;
    text-align: center;
    color: white;
    background:
        linear-gradient(rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.35)),
        url("../img/cafeteria.jpg") center / cover;
}

.hero h1 { margin: 0; font-size: var(--texto-xxl); line-height: 1.1; }

.boton {
    display: inline-block;
    padding: 0.75em 1.5em;
    border-radius: 999px;
    background-color: var(--color-marca);
    color: white;
    text-decoration: none;
}

/* ========== Productos: rejilla automática ========== */
.productos {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
    gap: 1.5rem;
}

.producto img { aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; }

/* ========== Opiniones: container queries ========== */
.opiniones__lista {
    display: grid;
    gap: 1rem;
}

.opinion {
    container-type: inline-size;
    margin: 0;
    padding: 1.5rem;
    background-color: white;
    border-left: 4px solid var(--color-marca);
}

.opinion p {
    margin: 0;
    font-size: clamp(1rem, 3cqi + 0.5rem, 1.5rem);   /* escala con la tarjeta */
}

/* ========== Pie ========== */
.pie {
    padding: 2rem 1rem;
    text-align: center;
    color: white;
    background-color: var(--color-texto);
}

/* ========== Tableta y superior ========== */
@media (width >= 768px) {
    .menu-icono { display: none; }
    .menu       { display: flex; flex-basis: auto; flex-direction: row; gap: 2rem; }
    .menu a     { padding: 0; }

    .opiniones__lista { grid-template-columns: 2fr 1fr; }   /* una opinión grande y otra pequeña */
}

/* ========== Preferencias del usuario ========== */
@media (hover: hover) {
    .boton:hover { filter: brightness(1.15); }
}

@media (prefers-reduced-motion: no-preference) {
    html { scroll-behavior: smooth; }   /* desplazamiento suave solo si el usuario no lo ha desactivado */
}

📘 Concepto: Observa cuántas cosas se adaptan sin media queries: el ancho de las secciones (min()), los tamaños de letra y espacios (clamp()), la rejilla de productos (auto-fit) y el texto de las opiniones (unidades cqi). Solo hemos necesitado una media query de tamaño, para el menú y la disposición de las opiniones.


11. Ejercicios

Ejercicio 1: Sin viewport

Crea una página con un título, un párrafo largo y una media query que cambie el color de fondo a partir de 600px. Ábrela en el modo dispositivo de DevTools sin la etiqueta meta viewport y después con ella. Describe las diferencias.

Ejercicio 2: De desktop first a mobile first

Toma este CSS desktop first y reescríbelo como mobile first con la sintaxis de rangos, obteniendo el mismo resultado:

.columnas { display: flex; gap: 2rem; }
.columnas > * { flex: 1; }
@media (max-width: 767.98px) {
    .columnas { flex-direction: column; gap: 1rem; }
}

Ejercicio 3: Preferencias del usuario

Crea una tarjeta con una animación de entrada, un efecto :hover y colores mediante variables. Haz que: el :hover solo se aplique con (hover: hover); la animación se desactive con prefers-reduced-motion: reduce; los colores cambien con prefers-color-scheme: dark; y los botones crezcan hasta 48px de alto con pointer: coarse. Prueba cada caso con el panel Rendering de DevTools.

Ejercicio 4: Tarjeta con container queries

Crea un componente “tarjeta de producto” y colócalo en tres sitios de la misma página: una barra lateral de 280px, una rejilla de tres columnas y una zona destacada a todo el ancho. Con container-type: inline-size y dos @container, consigue tres diseños distintos: compacto (vertical), horizontal y destacado (imagen grande y texto en dos columnas).

Ejercicio 5: Escala tipográfica fluida

Define en :root una escala de 5 tamaños con clamp() (combinando rem y vw) y aplícala a h1, h2, h3, p y small. Comprueba en DevTools los tamaños en 320px, 768px y 1440px, y verifica que el texto sigue creciendo al hacer zoom al 200 %.

Ejercicio 6: Menú hamburguesa

Implementa el menú responsive de la sección 8 de las dos formas (<details> y checkbox). Compara su comportamiento con teclado (Tab, Enter, Espacio) y anota las ventajas e inconvenientes de cada una.

Ejercicio 7: Página completa

Completa el caso práctico de la cafetería: añade al menos 6 productos, 3 opiniones, una sección “Horario y ubicación” con un mapa incrustado (<iframe> con aspect-ratio), una imagen con <picture> que cambie de encuadre entre móvil y escritorio, y una hoja de estilos de impresión. Prueba la página en 320px, 768px, 1024px y 1440px sin que aparezca scroll horizontal.


Resumen

Concepto Sintaxis / idea clave
Viewport <meta name="viewport" content="width=device-width, initial-scale=1">
Mobile first Estilos base para móvil + @media (width >= ...)
Rangos (width >= 768px), (600px <= width < 1000px)
Breakpoints Según el contenido; referencia Bootstrap: 576, 768, 992, 1200, 1400
Orientación (orientation: landscape) / portrait
Preferencias prefers-reduced-motion, prefers-color-scheme, prefers-contrast
Tipo de puntero (hover: hover), (pointer: coarse)
Impresión @media print { ... }
Container queries container-type: inline-size; + @container (width >= 450px)
Unidades de contenedor cqi, cqb, cqw, cqh
Imágenes max-width: 100%, width/height en HTML, srcset + sizes, <picture>
Tipografía fluida clamp(2rem, 1.2rem + 4vw, 4rem) (con parte en rem)
Menú sin JS <details>/<summary> o checkbox + :checked ~ .menu
Pruebas Modo dispositivo (Ctrl + Mayús + M), panel Rendering, móvil real