Paso 1 de 5
La estructura y la rejilla
El buscador, los botones de categoría y una lista vacía donde irán las fotos. Con grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)), la rejilla pone tantas columnas como quepan, de al menos 220 px: tres en el ordenador y una en el móvil, sin ninguna consulta de medios.
Las fotos son de dominio público y están alojadas en esta web; en tu proyecto puedes cambiarlas por las tuyas.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Galería</title>
<link rel="stylesheet" href="estilos.css">
</head>
<body>
<main class="pagina">
<h1>Galería</h1>
<div class="controles">
<label for="buscar" class="oculto">Buscar fotos</label>
<input type="search" id="buscar" placeholder="Buscar: gato, calle, cámara…" autocomplete="off">
<div class="categorias" role="group" aria-label="Categoría">
<button type="button" data-categoria="todas" aria-pressed="true">Todas</button>
<button type="button" data-categoria="naturaleza" aria-pressed="false">Naturaleza</button>
<button type="button" data-categoria="ciudad" aria-pressed="false">Ciudad</button>
<button type="button" data-categoria="objetos" aria-pressed="false">Objetos</button>
</div>
</div>
<p id="estado" class="estado" aria-live="polite"></p>
<ul id="galeria" class="galeria"></ul>
<p class="creditos">Fotos de dominio público (CC0 y Public Domain Mark) vía Openverse.</p>
</main>
<script src="app.js"></script>
</body>
</html>
* { box-sizing: border-box; }
body { margin: 0; background: #f8fafc; color: #0f172a; font-family: system-ui, sans-serif; }
.pagina { max-width: 1000px; margin: 0 auto; padding: 24px 16px; }
h1 { margin: 0 0 16px; }
.oculto { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.controles { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
input[type="search"] {
flex: 1 1 240px;
padding: 10px 12px;
border: 2px solid #cbd5e1;
border-radius: 10px;
font: inherit;
}
.categorias { display: flex; flex-wrap: wrap; gap: 6px; }
button {
padding: 8px 14px;
border: 0;
border-radius: 999px;
background: #e2e8f0;
color: #0f172a;
font: inherit;
font-weight: 600;
cursor: pointer;
}
button[aria-pressed="true"] { background: #0f172a; color: #ffffff; }
button:focus-visible, input:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
.estado { min-height: 1.5em; color: #475569; }
.galeria {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.galeria button { display: block; width: 100%; padding: 0; border-radius: 12px; overflow: hidden; background: none; }
.galeria img { display: block; width: 100%; height: 100%; aspect-ratio: 3 / 2; object-fit: cover; transition: transform 300ms ease; }
.galeria button:hover img { transform: scale(1.05); }
.creditos { margin-top: 24px; font-size: 0.85rem; color: #64748b; }
.visor { max-width: min(92vw, 900px); padding: 0; border: 0; border-radius: 14px; background: #0f172a; color: #f8fafc; }
.visor::backdrop { background: rgb(2 6 23 / 0.8); }
.visor img { display: block; width: 100%; max-height: 75vh; object-fit: contain; }
.visor p { margin: 0; padding: 12px 56px 14px 16px; }
.cerrar { position: absolute; top: 8px; right: 8px; width: 40px; height: 40px; padding: 0; font-size: 1.5rem; }
.flecha { position: absolute; top: 40%; width: 44px; height: 44px; padding: 0; font-size: 1.8rem; background: rgb(255 255 255 / 0.85); }
.flecha--izq { left: 8px; }
.flecha--der { right: 8px; }
@media (prefers-reduced-motion: reduce) { .galeria img { transition: none; } }
// El JavaScript llega en el paso 2