Has llegado al final del Nivel 1. No hay concepto nuevo en este capítulo: el reto es juntarlo todo en una sola página, tú solo, desde un fichero hasta una home terminada. Es el salto que de verdad cuesta —de “sé hacer cada ejercicio” a “sé construir algo mío entero”— y se aprende construyendo.
Qué vas a construir#
La home del Overwatch Team Builder: la página de entrada del proyecto que ha ido creciendo a lo largo del nivel. Tiene cuatro zonas:
- Cabecera: la marca y la navegación.
- Intro: un título y una frase que explica de qué va la página.
- Plantilla: una rejilla de cartas de héroe que se adapta de una columna en móvil a varias en pantallas anchas. Cada carta ya la has maquetado por partes en capítulos anteriores.
- Pie: discreto, con la nota del proyecto.
El HTML está hecho —semántico y accesible, como en el capítulo de semántica—, para que te centres en darle vida con CSS. Estos son los héroes que verás en la rejilla:
| Héroe | Rol | Partidas | Victorias | Winrate |
|---|---|---|---|---|
| Tracer | Daño | 120 | 78 | 65% |
| Reinhardt | Tanque | 90 | 51 | 57% |
| Mercy | Apoyo | 200 | 130 | 65% |
| Genji | Daño | 110 | 60 | 55% |
| D.Va | Tanque | 140 | 84 | 60% |
| Ana | Apoyo | 95 | 57 | 60% |
Cómo se ataca una página entera#
Lo que más bloquea ante una página en blanco no es no saber CSS: es no saber por dónde empezar. Este es el orden que funciona, y el que usarás en cualquier proyecto real:
- Lee la estructura primero. Antes de escribir una regla, entiende el HTML: qué secciones hay, qué se repite (la carta), qué clase tiene cada cosa. La mitad del trabajo es saber a qué le vas a hablar.
- Empieza por el móvil. Maqueta la versión de una columna sin abrir una sola media query. Es la más sencilla y la base de todo lo demás (mobile-first).
- Resuelve la pieza que se repite. Da estilo a la carta de héroe hasta que se vea bien. Cuando la unidad funciona, la rejilla es casi gratis.
- Amplía hacia arriba. Con
auto-fit+minmax, la rejilla crea columnas sola en pantallas anchas. Sin breakpoints, sin recalcular nada. - Cuida la accesibilidad mientras maquetas, no al final. Contraste suficiente,
foco visible con
:focus-visible, el skip link oculto hasta recibir el foco. Es parte de maquetar, no un parche posterior. - El remate, al final y con mesura. La sombra y el hover del capítulo BONUS son opcionales: ponlos si suman, quítalos si distraen.
No tienes que clavarlo a la primera. Maqueta, mira, ajusta. Y cuando creas que lo tienes, estréchalo a 375px: ahí se ve si está bien hecho.
Comprueba lo que sabes#
Pregunta 1 de 3
Vas a maquetar una página entera desde cero. ¿Por dónde empiezas?
Tu turno#
Este ejercicio se hace en local, en tu editor: es tu primera página completa, y conviene vivirla como un proyecto de verdad, no en un recuadro. Clónalo, lee el HTML, y construye el CSS por fases como acabas de ver. Cuando termines, despliega las soluciones y compáralas con la tuya: fíjate en cómo el «ok» se rompe a 375px y el «mejor» y el «excelente» aguantan.
Ejercicio · hazlo en local
Maqueta la home del Team Builder entera
El HTML está dado, semántico y accesible: léelo entero. Tu trabajo es el CSS (starter/styles.css): convertir ese esqueleto en una home maquetada, responsive (mobile-first) y bien rematada, juntando todo lo del Nivel 1. Clónalo y trabaja en tu editor; es tu primera página completa de principio a fin.
Paso 1: Que funcione
- Todas las secciones maquetadas: cabecera, intro, grid de cartas y pie.
- La carta muestra retrato, nombre, rol y estadísticas, bien espaciados.
- Se ve y se entiende en escritorio.
Paso 2: Sólido y responsive
- Tokens en `:root` (color, espaciado) y tamaños en `rem`, sin números mágicos.
- La rejilla cae a una columna en móvil y crea columnas solas en pantallas anchas (`auto-fit` + `minmax`).
- Accesible: skip link oculto hasta el foco, `:focus-visible` y buen contraste. Aguanta a 375px.
Paso 3: La integración completa
- Cabecera fija al hacer scroll (`position: sticky` + `z-index`).
- Acabado con mesura: sombra sutil y elevación al hover y al foco, con `transition`.
- Respeta `prefers-reduced-motion` y se ve impecable a 375px.
Cómo hacerlo en local
Clona el repositorio del curso, entra en la carpeta del ejercicio y abre el
index.html en tu navegador. Toda tu solución va en
solucion.js.
git clone <repo>
cd exercises/nivel-1/proyecto-home-team-builder
# abre index.html en el navegador y edita solucion.js Ver soluciones
/* SOLUCIÓN OK — la home funciona y se ve, pero el código es frágil.
Rejilla de columnas fijas (se rompe en móvil), números mágicos en px por todo
el fichero, sin tokens, y un par de detalles de accesibilidad a medias. */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: #f7f6fb;
color: #1c1b22;
line-height: 1.5;
}
/* El skip link se esconde con display:none... que lo saca también del recorrido
de tabulación. Visualmente limpio, pero anula la función que debía cumplir. */
.skip-link {
display: none;
}
.site-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #e6e3ee;
}
.brand {
font-size: 20px;
font-weight: 700;
}
.brand span {
color: #b8336a;
}
.site-nav {
display: flex;
gap: 16px;
}
.site-nav a {
color: #5b5966;
text-decoration: none;
font-size: 14px;
}
.intro {
padding: 24px;
}
.intro h1 {
font-size: 28px;
margin: 0 0 8px;
}
.intro-texto {
color: #5b5966;
margin: 0;
max-width: 540px;
}
.section-title {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 1px;
color: #5b5966;
padding: 0 24px;
margin: 8px 0 16px;
}
.hero-grid {
list-style: none;
margin: 0;
padding: 0 24px 24px;
display: grid;
/* Columnas FIJAS: en un móvil de 375px, tres columnas dejan cartas de ~110px
y el contenido se apelmaza. Aquí está el fallo grande. */
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.hero-card {
background: #fff;
border: 1px solid #e6e3ee;
border-radius: 12px;
padding: 20px;
}
.hero-portrait {
width: 48px;
height: 48px;
border-radius: 50%;
background: #b8336a;
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.hero-name {
font-size: 17px;
margin: 0;
}
.hero-role {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: #5b5966;
margin: 2px 0 12px;
}
.hero-stats {
display: flex;
gap: 16px;
margin: 0;
padding-top: 8px;
border-top: 1px solid #e6e3ee;
}
.stat dt {
font-size: 11px;
text-transform: uppercase;
color: #5b5966;
margin: 0;
}
.stat dd {
font-size: 16px;
font-weight: 600;
margin: 2px 0 0;
}
.site-footer {
padding: 20px 24px;
border-top: 1px solid #e6e3ee;
color: #5b5966;
font-size: 13px;
} Por qué este nivel
- Funciona y se ve: todas las secciones están. Pero la rejilla usa columnas fijas (`repeat(3, 1fr)`), así que en un móvil de 375px las cartas se apelmazan. Ese es el fallo grande.
- Números mágicos en px por todo el fichero (16px, 24px, 48px...). Cambiar el espaciado base obliga a buscar y reemplazar a mano.
- El skip link escondido con `display:none` queda limpio, pero lo saca del recorrido de Tab: anula justo la accesibilidad que debía aportar.
/* SOLUCIÓN MEJOR — código sólido y responsive de verdad.
Tokens en :root (una sola fuente de verdad), tamaños en rem, rejilla que se
adapta sola (auto-fit + minmax) de 1 columna en móvil a varias en pantallas
anchas, skip link accesible y foco visible. Sin números mágicos. */
:root {
--acento: #b8336a;
--tinta: #1c1b22;
--tinta-suave: #5b5966;
--linea: #e6e3ee;
--fondo: #f7f6fb;
--blanco: #fff;
--radio: 0.75rem;
--sp: 1.5rem;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: var(--fondo);
color: var(--tinta);
line-height: 1.5;
}
/* Skip link accesible: fuera de la vista hasta que recibe el foco con Tab. */
.skip-link {
position: absolute;
left: 0.5rem;
top: -3rem;
background: var(--tinta);
color: var(--blanco);
padding: 0.5rem 0.85rem;
border-radius: 0.5rem;
text-decoration: none;
transition: top 0.15s ease;
}
.skip-link:focus {
top: 0.5rem;
}
a:focus-visible {
outline: 2px solid var(--acento);
outline-offset: 2px;
border-radius: 2px;
}
.site-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.75rem;
padding: 1rem var(--sp);
border-bottom: 1px solid var(--linea);
}
.brand {
font-size: 1.25rem;
font-weight: 700;
}
.brand span {
color: var(--acento);
}
.site-nav {
display: flex;
gap: 1rem;
}
.site-nav a {
color: var(--tinta-suave);
text-decoration: none;
font-size: 0.9rem;
}
.site-nav a:hover {
color: var(--acento);
}
.intro {
padding: var(--sp);
}
.intro h1 {
font-size: clamp(1.6rem, 4vw, 2.2rem);
margin: 0 0 0.5rem;
}
.intro-texto {
color: var(--tinta-suave);
margin: 0;
max-width: 34rem;
}
.section-title {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--tinta-suave);
padding: 0 var(--sp);
margin: 0.5rem 0 1rem;
}
/* Mobile-first: en móvil cae a una columna; en cuanto hay sitio para 14rem,
crea las columnas que quepan. Sin una sola media query. */
.hero-grid {
list-style: none;
margin: 0;
padding: 0 var(--sp) var(--sp);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 1rem;
}
.hero-card {
background: var(--blanco);
border: 1px solid var(--linea);
border-radius: var(--radio);
padding: 1.25rem;
}
.hero-portrait {
width: 3rem;
height: 3rem;
border-radius: 50%;
background: var(--acento);
color: var(--blanco);
font-weight: 700;
display: grid;
place-items: center;
margin-bottom: 0.75rem;
}
.hero-name {
font-size: 1.05rem;
margin: 0;
}
.hero-role {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--tinta-suave);
margin: 0.15rem 0 1rem;
}
.hero-stats {
display: flex;
gap: 1.25rem;
margin: 0;
padding-top: 0.75rem;
border-top: 1px solid var(--linea);
}
.stat dt {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--tinta-suave);
margin: 0;
}
.stat dd {
font-size: 1.05rem;
font-weight: 600;
margin: 0.15rem 0 0;
}
.site-footer {
padding: 1.25rem var(--sp);
border-top: 1px solid var(--linea);
color: var(--tinta-suave);
font-size: 0.85rem;
} Por qué es mejor que el anterior
- Tokens en `:root`: el acento, la tinta y los espaciados viven en un solo sitio. Tamaños en `rem`, no px. El código aguanta cambios.
- La rejilla con `auto-fit` + `minmax(14rem, 1fr)` cae a una columna en móvil y crea columnas solas en pantallas anchas, sin una sola media query. Aguanta a 375px.
- Accesible de serie: skip link oculto hasta el foco, `:focus-visible` con el acento y contraste correcto.
/* SOLUCIÓN EXCELENTE — todo lo de "mejor", más la integración de lo último del
nivel: cabecera fija (posicionamiento) y el acabado visual con mesura
(sombra + hover suave), siempre respetando la accesibilidad.
- La cabecera se queda arriba al hacer scroll (sticky + z-index).
- Las cartas tienen una sombra sutil y se elevan al pasar el ratón O al
enfocarlas con teclado, de forma suave.
- Respeta prefers-reduced-motion. Y aguanta a 375px sin romperse. */
:root {
--acento: #b8336a;
--acento-fuerte: #9c2a59;
--tinta: #1c1b22;
--tinta-suave: #5b5966;
--linea: #e6e3ee;
--fondo: #f7f6fb;
--blanco: #fff;
--radio: 0.75rem;
--sp: 1.5rem;
--sombra: 0 2px 10px rgba(28, 27, 34, 0.07);
--sombra-alta: 0 10px 24px rgba(28, 27, 34, 0.12);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
sans-serif;
background: var(--fondo);
color: var(--tinta);
line-height: 1.5;
}
.skip-link {
position: absolute;
left: 0.5rem;
top: -3rem;
background: var(--tinta);
color: var(--blanco);
padding: 0.5rem 0.85rem;
border-radius: 0.5rem;
text-decoration: none;
z-index: 20;
transition: top 0.15s ease;
}
.skip-link:focus {
top: 0.5rem;
}
a:focus-visible {
outline: 2px solid var(--acento);
outline-offset: 2px;
border-radius: 2px;
}
/* Cabecera fija: se queda arriba al hacer scroll, por encima de las cartas. */
.site-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.75rem;
padding: 1rem var(--sp);
background: var(--blanco);
border-bottom: 1px solid var(--linea);
}
.brand {
font-size: 1.25rem;
font-weight: 700;
}
.brand span {
color: var(--acento);
}
.site-nav {
display: flex;
gap: 1rem;
}
.site-nav a {
color: var(--tinta-suave);
text-decoration: none;
font-size: 0.9rem;
}
.site-nav a:hover {
color: var(--acento);
}
.intro {
padding: var(--sp);
}
.intro h1 {
font-size: clamp(1.6rem, 4vw, 2.2rem);
margin: 0 0 0.5rem;
}
.intro-texto {
color: var(--tinta-suave);
margin: 0;
max-width: 34rem;
}
.section-title {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--tinta-suave);
padding: 0 var(--sp);
margin: 0.5rem 0 1rem;
}
.hero-grid {
list-style: none;
margin: 0;
padding: 0 var(--sp) var(--sp);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 1rem;
}
.hero-card {
background: var(--blanco);
border: 1px solid var(--linea);
border-radius: var(--radio);
padding: 1.25rem;
/* Acabado con mesura: sombra sutil y elevación suave al interactuar. */
box-shadow: var(--sombra);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.hero-card:hover,
.hero-card:focus-within {
transform: translateY(-4px);
box-shadow: var(--sombra-alta);
}
.hero-portrait {
width: 3rem;
height: 3rem;
border-radius: 50%;
/* Degradado discreto entre dos tonos del acento. */
background: linear-gradient(135deg, var(--acento), var(--acento-fuerte));
color: var(--blanco);
font-weight: 700;
display: grid;
place-items: center;
margin-bottom: 0.75rem;
}
.hero-name {
font-size: 1.05rem;
margin: 0;
}
.hero-role {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--tinta-suave);
margin: 0.15rem 0 1rem;
}
.hero-stats {
display: flex;
gap: 1.25rem;
margin: 0;
padding-top: 0.75rem;
border-top: 1px solid var(--linea);
}
.stat dt {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--tinta-suave);
margin: 0;
}
.stat dd {
font-size: 1.05rem;
font-weight: 600;
margin: 0.15rem 0 0;
}
.site-footer {
padding: 1.25rem var(--sp);
border-top: 1px solid var(--linea);
color: var(--tinta-suave);
font-size: 0.85rem;
}
/* Para quien pidió menos movimiento: se queda la sombra, sin el desplazamiento. */
@media (prefers-reduced-motion: reduce) {
.hero-card {
transition: box-shadow 0.2s ease;
}
.hero-card:hover,
.hero-card:focus-within {
transform: none;
}
} Por qué es mejor que el anterior
- Integra lo último del nivel: cabecera `sticky` que se queda arriba al hacer scroll, por encima de las cartas (`z-index`).
- El acabado del bonus, con mesura: sombra sutil y elevación al hover Y al foco de teclado (`:focus-within`), suavizada con `transition`.
- Respeta `prefers-reduced-motion` y aguanta impecable a 375px. Es la misma página que el «ok», pero construida con el criterio de todo el nivel.