Saltar al contenido

#Bootstrap 5.3.8 paso a paso: construye el sitio de tu club de videojuegos

Tutorial guiado de autoestudio · 3 horas y 30 minutos · Nivel principiante

🧪 Versión 2 (revisada y verificada). Corrige cuatro problemas reales que tenía la v1 y que se comprobaron abriendo el sitio en un navegador:

Problema Qué hace ahora
El sitio se desplazaba de lado a lado en celular La portada usa g-4 g-lg-5 y el CSS parte los textos largos con overflow-wrap
El modo oscuro ignoraba la preferencia del sistema Arranca con el tema del dispositivo (prefers-color-scheme) y recuerda el que elijas
La página parpadeaba al cargar Un script en el <head> aplica el tema antes de pintar
Al enviar el formulario el foco no se movía El foco salta al primer campo con error y al mensaje de éxito

Además: la galería enseña a usar imágenes reales con alt, y se aclara cuándo usar Sass.

¡Bienvenido/a! En las próximas 3 horas y media vas a construir PixelCrew, la página web de un club de videojuegos de colegio. Vas a empezar con un archivo casi vacío y terminar con un sitio completo, responsive, con modo oscuro y modo claro, formulario con validación, carrusel, acordeón y ventana emergente.

Y lo más importante: al final no solo tendrás un sitio terminado. Tendrás las herramientas para crear tus propios proyectos con Bootstrap sin necesitar este tutorial.


#Tabla de contenidos

  1. Antes de empezar
  2. Módulo 1: Preparación del entorno (15 min)
  3. Módulo 2: Contenedores, grid y breakpoints (30 min)
  4. Módulo 3: Tipografía, color y utilidades (20 min)
  5. Módulo 4: Navbar y navegación responsive (25 min)
  6. Módulo 5: Tarjetas, botones, badges y alertas (25 min)
  7. Módulo 6: Formularios y validación (25 min)
  8. Módulo 7: Componentes interactivos con JavaScript (25 min)
  9. Módulo 8: Personalización, modo oscuro y Sass (20 min)
  10. Módulo 9: Integración final y buenas prácticas (15 min)
  11. Módulo 10: Tu reto final y siguientes pasos (10 min)
  12. Código completo del proyecto
  13. Hoja de referencia rápida
  14. Retos adicionales y recursos oficiales

#Antes de empezar

#Qué vas a construir

Una página con estas secciones, todas hechas con Bootstrap 5.3.8:

Sección Lo que practicas
Barra de navegación Navbar responsive con menú hamburguesa
Hero (portada) Tipografía, utilidades, color
Estadísticas y galería Sistema de grid y breakpoints
"Qué hacemos" Tarjetas, botones, badges, alertas
Comunidad Carrusel
Preguntas frecuentes Acordeón
Inscripción Formularios con validación
Reglas del club Modal (ventana emergente)
Todo el sitio Modo claro/oscuro y personalización

#Prerrequisitos

  • Saber lo básico de HTML (etiquetas, atributos, enlaces) y de CSS (selectores, colores, márgenes).
  • Un editor de código. Recomendado: Visual Studio Code con la extensión Live Server (te recarga la página sola cada vez que guardas).
  • Un navegador moderno (Chrome, Edge o Firefox).
  • Conexión a internet mientras practicas (Bootstrap se carga desde un CDN).
  • No necesitas instalar nada más, ni saber JavaScript, ni conocer Bootstrap.

#Cómo usar este tutorial

  1. Hazlo en orden. Cada módulo agrega una pieza al mismo sitio, así que los módulos dependen unos de otros.
  2. Escribe o pega el código tú mismo/a y míralo funcionar. No basta con leer.
  3. Cada módulo tiene el mismo formato: objetivo, concepto, pasos, resultado esperado, errores comunes, mini-ejercicio y punto de control.
  4. Intenta el mini-ejercicio antes de abrir la pista y la solución. Equivocarse ahí es parte del aprendizaje.
  5. Usa el punto de control al final de cada módulo. Si no puedes marcar todo, repasa antes de seguir.
  6. Si algo no se ve como debería, revisa la sección de errores comunes y las Herramientas del navegador (tecla F12).

Íconos que verás: 💡 tip · ⚠️ ojo con esto · ✅ punto de control.

#Plan de tiempo

Módulo Tema Tiempo
1 Preparación del entorno 15 min
2 Contenedores, grid y breakpoints 30 min
3 Tipografía, color y utilidades 20 min
4 Navbar y navegación responsive 25 min
5 Tarjetas, botones, badges y alertas 25 min
6 Formularios y validación 25 min
7 Componentes interactivos (JS) 25 min
8 Personalización, modo oscuro y Sass 20 min
9 Integración final y buenas prácticas 15 min
10 Reto final: planificación y primer paso 10 min
Total 210 min (3 h 30 min)

💡 Los tiempos son estimaciones para una persona que escribe a ritmo normal. Si vas más rápido, aprovecha para hacer los retos adicionales; si vas más lento, está bien: lo importante es entender, no terminar primero.

#Suposiciones que hice

Para no dejar decisiones abiertas, asumí lo siguiente:

  • Tema del proyecto: un club de videojuegos de colegio (PixelCrew). Los nombres, cifras y testimonios son ficticios.
  • Imágenes: para que no dependas de descargar nada, la galería empieza con emojis sobre bloques de color. En el módulo 2 verás cómo cambiarlos por fotos reales con su alt (usaremos fotos de ejemplo de internet; en tu club, pon las tuyas).
  • Modo claro u oscuro: el sitio arranca con el tema que ya tiene tu dispositivo (prefers-color-scheme) y recuerda el que elijas después.
  • Bootstrap por CDN, con la versión exacta 5.3.8 y sus hashes de integridad. Sass se presenta como lectura opcional: no hace falta para nada de este tutorial.
  • El formulario no envía datos a ningún servidor: es solo práctica de interfaz y validación.
  • Todo el contenido está en español y usa solo clases de Bootstrap 5 (nada de jQuery ni de clases de Bootstrap 4).

#Módulo 1: Preparación del entorno

⏱ 15 minutos

🎯 Objetivo: crear la estructura de archivos, cargar Bootstrap 5.3.8 y comprobar que funciona.

#Concepto

Bootstrap es un framework de front-end: una colección de CSS y JavaScript ya escrita que te da tres cosas:

  • Un sistema de rejilla (grid) para organizar la página en filas y columnas.
  • Componentes listos para usar: barras de navegación, tarjetas, botones, formularios, modales...
  • Utilidades: clases pequeñas que hacen una sola cosa (mt-3 = margen arriba, text-center = texto centrado).

Es mobile-first: el diseño base piensa en celulares y luego se adapta a pantallas más grandes.

Hay dos formas de usarlo. Usaremos la primera:

Forma Cómo Cuándo
CDN Enlazas el CSS y el JS desde internet Aprender, prototipos, sitios sencillos
npm + Sass Lo instalas y lo compilas Proyectos grandes con personalización profunda

Bootstrap tiene dos archivos: el CSS (los estilos) y el bundle JS, que incluye todo el JavaScript de los componentes y a Popper (necesario para menús desplegables). Por eso usamos bootstrap.bundle.min.js y no el JS suelto.

#Pasos

1. Crea esta estructura de carpetas y archivos (puedes hacerlo desde VS Code):

Texto
pixelcrew/
├── index.html
├── css/
│   └── style.css
└── js/
    └── main.js

Deja style.css y main.js vacíos por ahora.

2. Pega esta plantilla en index.html. Es el esqueleto de todo el proyecto. Fíjate en los comentarios <!-- HERO -->, <!-- NAVBAR -->, etc.: son marcadores donde pegarás código en los módulos siguientes.

HTML
<!DOCTYPE html>
<html lang="es" data-bs-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="PixelCrew: el club de videojuegos de tu colegio. Torneos, noches de juego y talleres.">
  <title>PixelCrew | Club de videojuegos</title>

  <!-- Aplica el tema ANTES de que se vea la página (evita el parpadeo).
       Va primero en el <head> a propósito; en un sitio real se le llama
       "anti-flash" y casi siempre se escribe en una sola línea. -->
  <script>
    (function () {
      try {
        var guardado = localStorage.getItem('pc-tema');
        var oscuroDelSistema = window.matchMedia('(prefers-color-scheme: dark)').matches;
        document.documentElement.setAttribute('data-bs-theme', guardado || (oscuroDelSistema ? 'dark' : 'light'));
      } catch (error) {
        document.documentElement.setAttribute('data-bs-theme', 'light');
      }
    })();
  </script>

  <!-- Bootstrap 5.3.8 (CSS) -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">

  <!-- Tu CSS (siempre DESPUÉS de Bootstrap) -->
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <!-- NAVBAR -->

  <main id="contenido">
    <!-- PRUEBA -->
    <!-- HERO -->
    <!-- ESTADÍSTICAS -->
    <!-- QUÉ HACEMOS -->
    <!-- GALERÍA -->
    <!-- COMUNIDAD -->
    <!-- PREGUNTAS -->
    <!-- ÚNETE -->
  </main>

  <!-- FOOTER -->

  <!-- MODAL -->

  <!-- Bootstrap 5.3.8 (JS con Popper incluido) -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

  <!-- Tu JavaScript (después de Bootstrap) -->
  <script src="js/main.js"></script>
</body>
</html>

Observa lo importante:

  • <html lang="es" data-bs-theme="light">: idioma español y el tema inicial.
  • El script del <head> aplica el tema guardado (o el de tu dispositivo) antes de que se pinte nada. Sin él, el navegador muestra un tema claro y luego salta a oscuro: eso es el parpadeo.
  • <meta name="viewport" ...>: sin esta línea, Bootstrap no se adapta a celulares.
  • El CSS de Bootstrap va antes que tu style.css, para que tus estilos puedan sobrescribirlo.
  • Los scripts van al final del <body>.
  • Los atributos integrity y crossorigin verifican que el archivo del CDN no fue alterado. No los modifiques.

3. Agrega un bloque de prueba. Pega esto justo debajo del comentario <!-- PRUEBA -->:

HTML
<div class="container py-5">
  <h1 class="display-4">¡Hola, Bootstrap 5.3.8!</h1>
  <p class="lead">Si ves esto con estilo, todo está funcionando.</p>
  <button type="button" class="btn btn-primary">Mi primer botón</button>
</div>

4. Ábrelo en el navegador. Con Live Server: clic derecho en index.html → Open with Live Server. Sin Live Server: doble clic en el archivo.

5. Comprueba la versión. Abre las herramientas del navegador (F12), ve a la pestaña Console y escribe:

JavaScript
bootstrap.Tooltip.VERSION

Debe responder "5.3.8".

#Resultado esperado

Una página oscura con un título grande, un párrafo destacado y un botón azul con esquinas redondeadas. Si ves letras blancas simples sobre fondo blanco y un botón gris, Bootstrap no cargó.

#Errores comunes

Síntoma Causa probable Solución
La página se ve sin estilos Sin internet, o error al copiar la URL del CDN Revisa la conexión y compara los enlaces con los de la plantilla
La consola dice que bloqueó el recurso por integrity Se alteró el hash al copiar Vuelve a copiar la línea completa, sin espacios extra
El botón se ve bien pero los componentes no responden Falta el <script> del bundle Revisa que esté antes de </body>
En el celular todo se ve diminuto Falta la etiqueta viewport Agrégala dentro de <head>
bootstrap is not defined en la consola El script no cargó Revisa la URL y la pestaña Network (F12)
La página parpadea blanco al cargar Borraste el script del <head> o lo moviste después del CSS Ponlo de nuevo, lo más arriba posible en el <head>
El modo oscuro no aparece aunque tu sistema esté en oscuro Quitaste matchMedia('(prefers-color-scheme: dark)') Ese matchMedia es lo que hace que el sitio respete tu tema del sistema

#Mini-ejercicio

  1. Cambia data-bs-theme="light" por data-bs-theme="dark" en la etiqueta <html> y borra por un momento el script del <head>: verás el sitio oscuro, pero también el parpadeo al recargar. Después vuelve a poner el script.
  2. En el sistema operativo, cambia tu tema a oscuro y abre el sitio: ¿qué modo aparece? (Pista: prefers-color-scheme).
  3. Cambia el botón de btn-primary a btn-success, btn-danger y btn-warning.
💡 Pista Solo cambian palabras dentro del atributo `class`. Bootstrap tiene una clase para cada color: `btn-primary`, `btn-secondary`, `btn-success`, `btn-danger`, `btn-warning`, `btn-info`, `btn-light`, `btn-dark`.

#✅ Punto de control

  • [ ] Tengo la estructura pixelcrew/ con sus tres archivos.
  • [ ] Mi página muestra el título y el botón con estilo de Bootstrap.
  • [ ] bootstrap.Tooltip.VERSION responde "5.3.8" en la consola.
  • [ ] Sé cambiar entre modo claro y oscuro con data-bs-theme.
  • [ ] Entiendo para qué sirve el script del <head> y sé por qué va antes que el CSS.

#Módulo 2: Contenedores, grid y breakpoints

⏱ 30 minutos

🎯 Objetivo: organizar contenido en filas y columnas que se reacomodan según el tamaño de pantalla.

#Concepto

El grid de Bootstrap tiene tres piezas que siempre van anidadas en este orden:

Texto
.container   →   .row   →   .col-*
(centra y        (fila,      (columna: cada fila se divide
 limita el        agrupa      en 12 partes)
 ancho)           columnas)

Cada fila tiene 12 unidades. Una col-6 ocupa la mitad, una col-4 un tercio, una col-3 un cuarto. Si las columnas suman más de 12, las sobrantes saltan a la línea de abajo.

#Breakpoints: el corazón de lo responsive

Un breakpoint es el ancho de pantalla a partir del cual cambia el diseño. Bootstrap tiene seis:

Prefijo Se activa desde Dispositivo típico
(ninguno) 0 px Celular vertical
sm 576 px Celular horizontal
md 768 px Tablet
lg 992 px Laptop
xl 1200 px Escritorio
xxl 1400 px Pantalla grande

La regla clave es mobile-first: una clase sin prefijo aplica a todas las pantallas, y una clase con prefijo aplica desde ese tamaño hacia arriba. Ejemplo:

HTML
<div class="col-12 col-md-6 col-lg-4">

Se lee así: "ocupa las 12 columnas (todo el ancho) en celular; 6 (la mitad) desde tablet; 4 (un tercio) desde laptop".

Otras herramientas del grid que usaremos:

  • g-3: gutter (espacio entre columnas) de tamaño 3.
  • row-cols-2: en vez de dar un ancho a cada columna, dices cuántas columnas caben por fila.
  • container (ancho fijo por breakpoint) vs container-fluid (siempre 100 % del ancho).

#Pasos

1. Laboratorio de grid (temporal). Reemplaza el bloque de prueba del módulo anterior por este (bórralo todo y pega):

HTML
<div class="container py-4">
  <div class="row g-3">
    <div class="col-12 col-md-6 col-lg-4">
      <div class="p-3 bg-primary-subtle border rounded text-center">A</div>
    </div>
    <div class="col-12 col-md-6 col-lg-4">
      <div class="p-3 bg-success-subtle border rounded text-center">B</div>
    </div>
    <div class="col-12 col-md-6 col-lg-4">
      <div class="p-3 bg-danger-subtle border rounded text-center">C</div>
    </div>
  </div>
</div>

2. Experimenta. Abre las herramientas del navegador (F12) y activa el modo responsive (el ícono de celular y tablet). Cambia el ancho y observa:

  • Menos de 768 px → las tres cajas apiladas, una por fila.
  • Entre 768 y 991 px → dos por fila (la C baja).
  • 992 px o más → tres en una fila.

💡 Entre 768 y 991 px, A y B (col-md-6 cada una) suman las 12 unidades de la fila, por eso C no cabe y salta a la línea de abajo.

3. Borra el laboratorio. Ya aprendiste lo esencial. Elimina todo el bloque que pegaste en el paso 1.

4. Construye la franja de estadísticas. Pega justo debajo de <!-- ESTADÍSTICAS -->:

HTML
<section class="py-4 border-top border-bottom bg-body-tertiary" aria-label="El club en números">
  <div class="container">
    <div class="row g-3 text-center">
      <div class="col-6 col-md-3">
        <p class="display-6 fw-bold text-primary-emphasis mb-0">120+</p>
        <p class="text-body-secondary mb-0">miembros</p>
      </div>
      <div class="col-6 col-md-3">
        <p class="display-6 fw-bold text-primary-emphasis mb-0">12</p>
        <p class="text-body-secondary mb-0">torneos al año</p>
      </div>
      <div class="col-6 col-md-3">
        <p class="display-6 fw-bold text-primary-emphasis mb-0">3</p>
        <p class="text-body-secondary mb-0">talleres semanales</p>
      </div>
      <div class="col-6 col-md-3">
        <p class="display-6 fw-bold text-primary-emphasis mb-0">1</p>
        <p class="text-body-secondary mb-0">sala gamer</p>
      </div>
    </div>
  </div>
</section>

Aquí hay varias ideas nuevas:

  • col-6 col-md-3: dos por fila en celular, cuatro por fila desde tablet.
  • bg-body-tertiary: un fondo que se adapta solo al modo claro u oscuro.
  • border-top border-bottom: bordes arriba y abajo.
  • display-6: un texto grande y llamativo (más sobre esto en el módulo 3).

5. Construye la galería. Pega justo debajo de <!-- GALERÍA -->:

HTML
<section id="galeria" class="py-5">
  <div class="container">
    <h2 class="text-center mb-2">Momentos del club</h2>
    <p class="text-center text-body-secondary mb-4">Un vistazo a lo que pasa en las reuniones. (Aquí irían tus fotos reales.)</p>

    <div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
      <div class="col">
        <div class="ratio ratio-1x1">
          <div class="rounded-4 bg-primary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un trofeo">🏆</div>
        </div>
      </div>
      <div class="col">
        <div class="ratio ratio-1x1">
          <div class="rounded-4 bg-success d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un joystick">🕹️</div>
        </div>
      </div>
      <div class="col">
        <div class="ratio ratio-1x1">
          <div class="rounded-4 bg-danger d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de unos audífonos">🎧</div>
        </div>
      </div>
      <div class="col">
        <div class="ratio ratio-1x1">
          <div class="rounded-4 bg-warning d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un computador">💻</div>
        </div>
      </div>
      <div class="col">
        <div class="ratio ratio-1x1">
          <div class="rounded-4 bg-info d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un dado">🎲</div>
        </div>
      </div>
      <div class="col">
        <div class="ratio ratio-1x1">
          <div class="rounded-4 bg-secondary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un apretón de manos">🤝</div>
        </div>
      </div>
    </div>
  </div>
</section>

Mira la clase row row-cols-2 row-cols-md-3 row-cols-lg-6: ya no le damos ancho a cada columna, sino que decimos cuántas caben: 2 en celular, 3 en tablet, 6 en laptop. Además:

  • ratio ratio-1x1 crea un cuadrado perfecto sin importar el ancho.
  • role="img" y aria-label describen el emoji para lectores de pantalla (accesibilidad).

6. (Opcional) Cambia dos cuadrados por fotos reales. Los emojis son cómodo para practicar, pero un sitio de verdad lleva fotos. Reemplaza dos de los <div class="col"> por estos (usamos fotos de ejemplo de internet; tú pon las tuyas):

HTML
<div class="col">
  <div class="ratio ratio-1x1">
    <img src="https://picsum.photos/seed/pixelcrew-torneo/400/400"
         class="rounded-4 object-fit-cover w-100 h-100"
         alt="Jugadores alrededor de una mesa con consolas en un torneo del club"
         width="400" height="400" loading="lazy" decoding="async">
  </div>
</div>

Qué aprendiste ahí, y son cosas que se usan todo el tiempo:

Atributo o clase Para qué
alt Describe la imagen para quien no la ve. Si es solo decorativa, se deja vacío: alt=""
width y height El navegador reserva el espacio antes de descargar la foto: la página no "salta" al cargar
loading="lazy" No descarga la foto hasta que está cerca de la pantalla. Acelera páginas largas
object-fit-cover Rellena el cuadrado sin deformar la foto
decoding="async" El navegador la decodifica sin bloquear el dibujado

⚠️ alt no es un adorno. Escribir alt="foto 1" no sirve de nada: quien usa un lector de pantalla solo oirá ese texto inútil. Describe lo que la imagen muestra.

#Resultado esperado

Una franja con 4 cifras y una galería de 6 cuadrados de colores. Al achicar la ventana, la franja pasa de 4 columnas a 2 + 2, y la galería de 6 a 3 + 3 y luego a 2 + 2 + 2.

#Errores comunes

Síntoma Causa Solución
Las columnas se pegan a los bordes de la pantalla Falta el .container Envuelve la fila en un .container
Las columnas no se alinean bien Pusiste .col sin .row Las columnas siempre van dentro de una .row
Una columna salta de línea sin razón Las columnas suman más de 12 Revisa la suma de los números col-*
col-xs-6 o col-sm-offset-2 no funcionan Son de Bootstrap 3 En Bootstrap 5 es col-6 y offset-sm-2
No ves cambios al achicar la ventana Te falta la etiqueta viewport o estás mirando otro breakpoint Usa el modo responsive de F12 y revisa los anchos de la tabla
La página se desplaza a los lados en celular Usaste un g-5 (gutter grande) dentro de un .container Una fila con gutter se come la mitad del gutter en margen negativo a cada lado. Si eso supera el relleno del contenedor, la fila se sale de la pantalla. Solución: g-4 g-lg-5

#Mini-ejercicio

Modifica la galería para que en pantallas lg se vean 4 cuadrados por fila (en vez de 6) y agrega 2 cuadrados más (con otro emoji y otro color) para completar 2 filas parejas de 4.

💡 Pista Solo necesitas cambiar un número en `row-cols-lg-6` y duplicar dos bloques `
`. Para los colores usa `bg-dark`, `bg-light` o los que ya usaste con otro emoji.
✅ Solución Cambia `row-cols-lg-6` por `row-cols-lg-4` y agrega, por ejemplo:
HTML
<div class="col">
  <div class="ratio ratio-1x1">
    <div class="rounded-4 bg-dark d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de una estrella">⭐</div>
  </div>
</div>
Repite con otro emoji para tener 8 cuadrados en total.

💡 Puedes dejar la galería como la dejaste: el resto del tutorial no depende de cuántos cuadrados tenga.

#✅ Punto de control

  • [ ] Puedo explicar la jerarquía container → row → col.
  • [ ] Entiendo que col-12 col-md-6 col-lg-4 significa "mobile-first".
  • [ ] Mi franja de estadísticas pasa de 4 a 2 columnas en celular.
  • [ ] Mi galería cambia el número de cuadrados por fila según el ancho.
  • [ ] Sé usar el modo responsive de las herramientas del navegador.
  • [ ] Sé por qué un g-5 dentro de un .container puede hacer scroll horizontal, y sé usar g-4 g-lg-5.

#Módulo 3: Tipografía, color y utilidades

⏱ 20 minutos

🎯 Objetivo: dar estilo a tu contenido usando clases utilitarias de Bootstrap, y construir la portada (hero) del sitio.

#Concepto

Las utilidades son clases que hacen una sola cosa. Combinándolas puedes diseñar casi todo sin escribir CSS. Las más importantes:

Familia Ejemplos Qué hacen
Tipografía display-1…display-6, lead, fs-1…fs-6, fw-bold, fw-semibold, text-uppercase, small Tamaño, peso y estilo del texto
Alineación text-center, text-start, text-end Alineación horizontal
Color de texto text-primary, text-body-secondary, text-white Color del texto
Color de fondo bg-primary, bg-success-subtle, bg-body-tertiary Color de fondo
Espaciado m-3, p-4, mt-2, px-5, py-3, ms-auto, gap-3 Márgenes, relleno y separación
Display y flex d-flex, d-none, d-lg-block, flex-column, justify-content-between, align-items-center Mostrar, ocultar y alinear
Bordes y sombras border, rounded-4, rounded-pill, shadow, shadow-lg Aspecto de las cajas

#La escala de espaciado

Los números del 0 al 5 siguen esta escala:

Número 0 1 2 3 4 5
Tamaño 0 0,25 rem 0,5 rem 1 rem 1,5 rem 3 rem

Y las letras indican dónde aplica:

  • m = margen (afuera), p = padding (adentro).
  • t arriba, b abajo, s inicio (izquierda), e final (derecha), x izquierda y derecha, y arriba y abajo.
  • Sin letra = los cuatro lados.

Así, mt-4 es "margen arriba tamaño 4" y px-5 es "relleno horizontal tamaño 5". Y todas aceptan breakpoints: py-3 py-lg-5.

#Colores adaptables

Fíjate en que usamos text-body-secondary y bg-body-tertiary en vez de colores fijos como text-muted o bg-light. Los primeros cambian solos entre modo claro y oscuro, y son los recomendados desde Bootstrap 5.3.

💡 Regla práctica: usa utilidades para el 90 % del estilo y escribe CSS propio solo para lo que Bootstrap no trae (como un degradado).

#Pasos

1. Agrega el HTML del hero. Pega justo debajo de <!-- HERO -->:

HTML
<section id="inicio" class="hero py-5">
  <div class="container py-lg-5">
    <div class="row align-items-center g-4 g-lg-5">

      <div class="col-lg-7">
        <p class="text-uppercase fw-semibold text-primary-emphasis small mb-2">Club de videojuegos del colegio</p>
        <h1 class="display-3 fw-bold mb-3">Juega, crea y compite con <span class="text-gradient">PixelCrew</span></h1>
        <p class="lead text-body-secondary mb-4">
          Somos el club donde los videojuegos se juegan, se estudian y hasta se programan.
          Si tienes entre 14 y 18 años, hay un lugar para ti en el equipo.
        </p>
        <div class="d-flex flex-column flex-sm-row gap-3">
          <a href="#unete" class="btn btn-primary btn-lg px-4">Únete al club</a>
          <a href="#juegos" class="btn btn-outline-secondary btn-lg px-4">Descubre qué hacemos</a>
        </div>
      </div>

      <div class="col-lg-5 d-none d-lg-block">
        <div class="hero-card rounded-4 shadow-lg p-5 text-center text-white" aria-hidden="true">
          <div class="display-1">🎮</div>
          <p class="fs-4 fw-semibold mb-0">Nivel 1: ¡empieza hoy!</p>
        </div>
      </div>

    </div>
  </div>
</section>

Detalles para fijarte:

  • py-5 y py-lg-5 en el contenedor: relleno vertical, más grande en pantallas grandes.
  • display-3 fw-bold: el título enorme y en negrita. lead hace un párrafo destacado.
  • d-flex flex-column flex-sm-row gap-3: los botones se apilan en celular y se ponen uno al lado del otro desde sm.
  • d-none d-lg-block: la tarjeta decorativa solo aparece desde pantallas grandes.
  • aria-hidden="true": es decoración, así que los lectores de pantalla la saltan.
  • <h1>: solo hay uno por página.
  • g-4 g-lg-5 en la fila: en celular usamos un gutter mediano y lo subimos en pantallas grandes. ¿Por qué? Porque una fila con g-5 tiene un margen negativo de 24 px a cada lado, más ancho que el relleno del contenedor, y eso hace que la página se desplace horizontalmente en el celular. Es un error muy común; pruébalo: cambia a g-5 a secas, achica la ventana y verás cómo aparece la barra de desplazamiento horizontal.

2. Agrega CSS propio. Bootstrap no trae degradados, así que esta es una buena excusa para escribir un poco de CSS. Pega esto en css/style.css:

CSS
/* Un texto muy largo (un correo, una URL) puede empujar la página y crear
   scroll horizontal en celular. `overflow-wrap: anywhere` lo parte.
   Ojo: `anywhere`, no `break-word`, porque es el único que además reduce el
   ancho mínimo de las celdas de un grid. */
body {
  overflow-wrap: anywhere;
}

/* ===== 3. Hero ===== */
.hero {
  background:
    radial-gradient(circle at 15% 20%, rgba(124, 77, 255, 0.25), transparent 45%),
    radial-gradient(circle at 85% 80%, rgba(13, 139, 217, 0.18), transparent 45%);
}

.text-gradient {
  background: linear-gradient(90deg, #7c4dff, #0d8bd9);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero-card {
  background: linear-gradient(135deg, #7c4dff, #0d8bd9);
}

La clase text-gradient pinta el texto con un degradado recortando el fondo con la forma de las letras.

#Resultado esperado

Una portada con título gigante (la palabra "PixelCrew" con degradado de morado a azul), un texto descriptivo en gris, dos botones y, en pantallas anchas, una tarjeta decorativa a la derecha. En celular la tarjeta desaparece y los botones ocupan todo el ancho.

#Errores comunes

⚠️ Clases que ya no existen o están obsoletas (las verás en tutoriales viejos):

Si ves… Usa…
ml-3, mr-3 ms-3, me-3
pl-2, pr-2 ps-2, pe-2
text-left, text-right text-start, text-end
float-left, float-right float-start, float-end
font-weight-bold fw-bold
text-muted text-body-secondary
badge-primary text-bg-primary
data-toggle, data-target data-bs-toggle, data-bs-target

Otros problemas:

  • El título no tiene degradado: revisa que style.css esté enlazado y que escribiste -webkit-background-clip y background-clip.
  • Tu CSS no pisa a Bootstrap: confirma que style.css se carga después del CSS de Bootstrap.
  • La página se desplaza en celular: casi siempre es un g-5 dentro de un .container o un texto largo sin partir. Usa g-4 g-lg-5 y agrega overflow-wrap: anywhere al body.

#Mini-ejercicio

En celular, centra el título y el párrafo del hero. Desde pantallas lg, deben volver a alinearse a la izquierda.

💡 Pista Es una clase de alineación con y sin prefijo de breakpoint. Va en la columna que contiene el texto (`col-lg-7`).
✅ Solución Agrega `text-center text-lg-start` a la columna: `
`.

#✅ Punto de control

  • [ ] Mi hero muestra el título con degradado y dos botones.
  • [ ] Entiendo qué significa mt-4, px-5 y py-lg-5.
  • [ ] Sé usar d-none d-lg-block para ocultar elementos según el tamaño de pantalla.
  • [ ] Sé cuáles clases de Bootstrap 4 ya no debo usar.

#Módulo 4: Navbar y navegación responsive

⏱ 25 minutos

🎯 Objetivo: crear una barra de navegación que se colapsa en un menú hamburguesa en pantallas pequeñas y que permanece fija arriba.

#Concepto

La navbar es un componente completo de Bootstrap. Se compone de:

  • .navbar: el contenedor principal.
  • .navbar-expand-lg: define desde qué breakpoint el menú se muestra expandido. Por debajo de lg, se colapsa.
  • .navbar-brand: el logo o nombre del sitio.
  • .navbar-toggler: el botón hamburguesa.
  • .navbar-collapse: la parte que se oculta y se muestra.
  • .navbar-nav, .nav-item, .nav-link: la lista de enlaces.

El botón hamburguesa funciona sin que escribas JavaScript: Bootstrap lee atributos data-bs-* en el HTML. Aquí, data-bs-toggle="collapse" y data-bs-target="#menuPrincipal" significan "al hacer clic, muestra u oculta el elemento con id menuPrincipal".

#Accesibilidad en la navbar

  • aria-label en el <nav> y en el botón dice a los lectores de pantalla qué son.
  • aria-expanded indica si el menú está abierto (Bootstrap lo actualiza solo).
  • Un enlace "Saltar al contenido" permite a quien navega con teclado evitar repetir todo el menú.

#Pasos

1. Pega la navbar justo debajo de <!-- NAVBAR -->:

HTML
<a class="visually-hidden-focusable" href="#contenido">Saltar al contenido</a>

<header class="sticky-top">
  <nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom" aria-label="Navegación principal">
    <div class="container">
      <a class="navbar-brand fw-bold" href="#inicio">🎮 PixelCrew</a>

      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuPrincipal" aria-controls="menuPrincipal" aria-expanded="false" aria-label="Abrir o cerrar el menú">
        <span class="navbar-toggler-icon"></span>
      </button>

      <div class="collapse navbar-collapse" id="menuPrincipal">
        <ul class="navbar-nav ms-auto mb-2 mb-lg-0 align-items-lg-center">
          <li class="nav-item"><a class="nav-link" href="#juegos">Qué hacemos</a></li>
          <li class="nav-item"><a class="nav-link" href="#galeria">Galería</a></li>
          <li class="nav-item"><a class="nav-link" href="#testimonios">Comunidad</a></li>
          <li class="nav-item"><a class="nav-link" href="#faq">Preguntas</a></li>
          <li class="nav-item ms-lg-3 my-2 my-lg-0"><a class="btn btn-primary btn-sm" href="#unete">Únete</a></li>
        </ul>
      </div>
    </div>
  </nav>
</header>

2. Revisa los detalles:

  • sticky-top: la barra se queda fija arriba al hacer scroll.
  • bg-body-tertiary border-bottom: un fondo suave que se adapta al modo claro/oscuro.
  • ms-auto: empuja los enlaces hacia la derecha (el margen izquierdo es "automático").
  • align-items-lg-center: alinea verticalmente solo desde lg.
  • visually-hidden-focusable: el enlace de saltar está oculto hasta que lo enfocas con la tecla Tab.

3. Pruébalo:

  • Estira y encoge la ventana: pasado lg (992 px), el menú se oculta tras el botón hamburguesa.
  • Haz clic en la hamburguesa: el menú se despliega.
  • Los enlaces del menú todavía no te llevan a ningún lado: las secciones a las que apuntan las vas a crear en los próximos módulos.
  • Presiona Tab apenas cargue la página: debe aparecer "Saltar al contenido".

#Resultado esperado

Una barra superior con el nombre del club a la izquierda y los enlaces a la derecha. En celular, solo se ve el nombre y el botón de tres rayitas.

#Errores comunes

Síntoma Causa Solución
El botón hamburguesa no hace nada Falta el bundle JS, o el id no coincide con data-bs-target Revisa que data-bs-target="#menuPrincipal" y id="menuPrincipal" sean idénticos
Los enlaces quedan a la izquierda Falta ms-auto Agrégalo al <ul class="navbar-nav">
El menú nunca se colapsa Falta navbar-expand-lg o collapse Revisa ambos nombres de clase
La navbar tapa el contenido al navegar a una sección Es normal con sticky-top Lo corregiremos con una línea de CSS en el módulo 8
El ícono hamburguesa no aparece Falta <span class="navbar-toggler-icon"> Agrégalo dentro del botón

#Mini-ejercicio

Haz que el menú se expanda desde pantallas medianas (md) en lugar de lg.

💡 Pista Cambia el breakpoint en una clase de la navbar. Hay otras clases con `lg` dentro de la navbar que también deberías ajustar para que todo sea coherente.
✅ Solución Cambia `navbar-expand-lg` por `navbar-expand-md`, y en el `
    ` usa `mb-md-0` y `align-items-md-center`. También ajusta `ms-lg-3` y `my-lg-0` del botón "Únete" a `ms-md-3` y `my-md-0`.

#✅ Punto de control

  • [ ] La navbar se ve completa en pantalla ancha y se colapsa en pantalla angosta.
  • [ ] El botón hamburguesa abre y cierra el menú.
  • [ ] La barra se queda fija arriba al hacer scroll.
  • [ ] Entiendo cómo data-bs-toggle y data-bs-target conectan un botón con un elemento.
  • [ ] Probé el enlace "Saltar al contenido" con la tecla Tab.

#Módulo 5: Tarjetas, botones, badges y alertas

⏱ 25 minutos

🎯 Objetivo: usar los componentes más comunes de Bootstrap para presentar información de forma atractiva.

#Concepto

Un componente es un bloque de interfaz listo para usar. Se arma con una clase base y variantes:

  • Botones: base .btn + variante de color (.btn-primary) o contorno (.btn-outline-primary) + tamaño (.btn-lg, .btn-sm).
  • Badges: etiquetas pequeñas, con .badge text-bg-warning (el fondo y el texto con buen contraste ya vienen resueltos).
  • Alertas: mensajes contextuales: .alert alert-success, alert-danger, alert-warning, alert-info. Pueden cerrarse con un botón.
  • Tarjetas: .card > .card-body > .card-title, .card-text. También .card-header y .card-footer.

Los colores contextuales significan algo, y es bueno respetarlo:

Color Significado
primary Acción principal
secondary Acción secundaria
success Todo salió bien
danger Error o peligro
warning Atención
info Información neutral

Una tarjeta que ocupa toda la fila no es útil por sí sola; lo común es ponerlas dentro de un grid, como aprendiste en el módulo 2: row row-cols-1 row-cols-md-3 g-4. Y para que todas midan lo mismo de alto, cada tarjeta lleva h-100.

#Pasos

1. Agrega la sección "Qué hacemos". Pega justo debajo de <!-- QUÉ HACEMOS -->:

HTML
<section id="juegos" class="py-5">
  <div class="container">

    <div class="alert alert-info alert-dismissible fade show" role="alert">
      <strong>¡Inscripciones abiertas!</strong> Hay cupos disponibles hasta el 31 de octubre.
      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar aviso"></button>
    </div>

    <div class="row justify-content-center my-5">
      <div class="col-lg-7 text-center">
        <h2 class="mb-2">Más que jugar</h2>
        <p class="text-body-secondary mb-0">Tres formas de formar parte del club. Elige la tuya (o prueba las tres).</p>
      </div>
    </div>

    <div class="row row-cols-1 row-cols-md-3 g-4">

      <div class="col">
        <div class="card h-100 card-hover shadow-sm">
          <div class="card-body">
            <span class="badge text-bg-warning mb-3">Cada viernes</span>
            <h3 class="card-title h5">🏆 Torneos</h3>
            <p class="card-text text-body-secondary">Compite en equipo o en solitario en juegos para todos los niveles. Hay premios, diplomas y mucha rivalidad sana.</p>
          </div>
          <div class="card-footer bg-transparent border-0 pb-3">
            <a href="#unete" class="btn btn-outline-primary w-100">Quiero competir</a>
          </div>
        </div>
      </div>

      <div class="col">
        <div class="card h-100 card-hover shadow-sm">
          <div class="card-body">
            <span class="badge text-bg-success mb-3">Cada miércoles</span>
            <h3 class="card-title h5">🕹️ Noches de juego</h3>
            <p class="card-text text-body-secondary">Juegos cooperativos y de mesa, música y snacks. Ideal para conocer gente nueva sin presión.</p>
          </div>
          <div class="card-footer bg-transparent border-0 pb-3">
            <a href="#unete" class="btn btn-outline-primary w-100">Me apunto</a>
          </div>
        </div>
      </div>

      <div class="col">
        <div class="card h-100 card-hover shadow-sm">
          <div class="card-body">
            <span class="badge text-bg-info mb-3">Cada martes</span>
            <h3 class="card-title h5">💻 Taller de creación</h3>
            <p class="card-text text-body-secondary">Aprende a diseñar niveles y personajes, y a programar tu primer videojuego desde cero.</p>
          </div>
          <div class="card-footer bg-transparent border-0 pb-3">
            <a href="#unete" class="btn btn-outline-primary w-100">Quiero crear</a>
          </div>
        </div>
      </div>

    </div>
  </div>
</section>

Qué mirar:

  • La alerta al inicio tiene alert-dismissible fade show y un botón btn-close con data-bs-dismiss="alert": se cierra con un clic, sin que escribas JS.
  • Las tres tarjetas usan h-100 para igualar alturas y card-footer bg-transparent border-0 para un pie limpio.
  • Cada badge usa un color distinto según el día.
  • Los títulos son <h3 class="h5">: semánticamente son un h3 (porque están debajo de un h2), pero se ven con tamaño h5. Es un truco muy útil.
  • Los botones w-100 ocupan todo el ancho de su tarjeta.

2. Agrega el efecto "levitar" al pasar el mouse. Pega al final de css/style.css:

CSS
/* ===== 4. Tarjetas ===== */
.card-hover {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.card-hover:hover {
  transform: translateY(-6px);
  box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.25) !important;
}

@media (prefers-reduced-motion: reduce) {
  .card-hover { transition: none; }
  .card-hover:hover { transform: none; }
}

El bloque prefers-reduced-motion desactiva la animación para quienes tienen activada la opción de "reducir movimiento" en su dispositivo. Es una buena práctica de accesibilidad.

#Resultado esperado

Una alerta azul cerrable, un título centrado y tres tarjetas del mismo alto, con una etiqueta de color, un título con emoji, una descripción y un botón. Al pasar el mouse, cada tarjeta sube un poco. En celular, las tarjetas se apilan.

#Errores comunes

Síntoma Causa Solución
Las tarjetas tienen alturas distintas Falta h-100 en .card Agrégalo a cada .card
La alerta no se puede cerrar Falta el bundle JS o data-bs-dismiss="alert" Revisa ambos
El texto del badge no se lee Usaste bg-warning sin color de texto Usa text-bg-warning que ya incluye ambos
Las tarjetas ocupan todo el ancho Olvidaste el row row-cols-* Envuélvelas en un grid
btn btn-default no se ve No existe en Bootstrap 5 Usa btn-secondary o btn-outline-secondary

#Mini-ejercicio

Agrega una cuarta tarjeta llamada "📚 Zona de estudio" con un badge text-bg-danger que diga "Antes de exámenes". Haz que en pantallas lg se vean las 4 en una fila, y en md, 2 por fila.

💡 Pista Duplica una `
...
` completa, cambia los textos y ajusta `row-cols-md-3` para que además tenga una regla para `lg`.
✅ Solución Cambia la fila a `row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4` y agrega la cuarta tarjeta copiando la estructura de una existente (badge `text-bg-danger`, título "📚 Zona de estudio" y su texto).

#✅ Punto de control

  • [ ] Veo tres tarjetas del mismo alto dentro de un grid.
  • [ ] La alerta se cierra con su botón.
  • [ ] Entiendo para qué sirven h-100, text-bg-* y btn-close.
  • [ ] Sé elegir el color contextual correcto según el mensaje.
  • [ ] Las tarjetas se apilan en celular.

#Módulo 6: Formularios y validación

⏱ 25 minutos

🎯 Objetivo: construir un formulario de inscripción accesible y validarlo con las herramientas del navegador y Bootstrap.

#Concepto

Bootstrap da estilo a los formularios con pocas clases:

Clase Para qué sirve
.form-control Campos de texto, correo, número, textarea
.form-select Listas desplegables
.form-check + .form-check-input + .form-check-label Casillas y botones de opción
.form-label Etiqueta sobre un campo
.form-floating Etiqueta que "flota" dentro del campo
.input-group + .input-group-text Un campo con un texto o ícono pegado (como el @)

#Cómo funciona la validación

La validación tiene tres piezas:

  1. Atributos HTML5 que definen las reglas: required, minlength, min, max, type="email", pattern. El navegador ya sabe evaluarlos.
  2. JavaScript que pregunta al navegador si el formulario es válido con formulario.checkValidity().
  3. La clase .was-validated en el <form>. Al agregarla, Bootstrap pinta de verde los campos válidos y de rojo los inválidos, y muestra los mensajes .invalid-feedback.

También agregamos novalidate al <form> para apagar los globitos feos del navegador y mostrar los mensajes de Bootstrap en su lugar.

#Accesibilidad en formularios

Todo campo necesita una <label> conectada con el id del campo (for="nombre" ↔ id="nombre"). Sin eso, los lectores de pantalla no saben qué se pide, y hacer clic en la etiqueta no enfoca el campo. En el campo @ que no tiene etiqueta visible usamos aria-label.

#Pasos

1. Agrega el formulario. Pega justo debajo de <!-- ÚNETE -->:

HTML
<section id="unete" class="py-5">
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-lg-7">

        <h2 class="text-center mb-2">Únete a PixelCrew</h2>
        <p class="text-center text-body-secondary mb-4">Completa el formulario y te contamos cuándo es la primera reunión.</p>

        <div id="exito" class="alert alert-success d-none" role="status" tabindex="-1">
          🎉 ¡Listo! Recibimos tu inscripción. Nos vemos en el siguiente nivel.
        </div>

        <form id="formUnete" class="row g-3" novalidate>

          <div class="col-md-6">
            <div class="form-floating">
              <input type="text" class="form-control" id="nombre" placeholder="Tu nombre" required minlength="2" autocomplete="given-name">
              <label for="nombre">Nombre</label>
              <div class="invalid-feedback">Escribe tu nombre (mínimo 2 letras).</div>
            </div>
          </div>

          <div class="col-md-6">
            <div class="form-floating">
              <input type="email" class="form-control" id="correo" placeholder="tucorreo@ejemplo.com" required autocomplete="email">
              <label for="correo">Correo electrónico</label>
              <div class="invalid-feedback">Escribe un correo válido, por ejemplo nombre@correo.com.</div>
            </div>
          </div>

          <div class="col-md-4">
            <div class="form-floating">
              <input type="number" class="form-control" id="edad" placeholder="16" required min="13" max="18">
              <label for="edad">Edad</label>
              <div class="invalid-feedback">El club es para edades entre 13 y 18 años.</div>
            </div>
          </div>

          <div class="col-md-8">
            <div class="form-floating">
              <select class="form-select" id="interes" required>
                <option value="" selected disabled>Elige una opción…</option>
                <option>Torneos</option>
                <option>Noches de juego</option>
                <option>Taller de creación</option>
                <option>Todo lo anterior</option>
              </select>
              <label for="interes">¿Qué te interesa más?</label>
              <div class="invalid-feedback">Elige una opción de la lista.</div>
            </div>
          </div>

          <div class="col-12">
            <div class="input-group">
              <span class="input-group-text" id="arroba">@</span>
              <input type="text" class="form-control" id="usuario" placeholder="Tu usuario gamer (opcional)" aria-label="Tu usuario gamer (opcional)" aria-describedby="arroba">
            </div>
          </div>

          <div class="col-12">
            <label for="mensaje" class="form-label">Cuéntanos tu juego favorito (opcional)</label>
            <textarea class="form-control" id="mensaje" rows="3"></textarea>
          </div>

          <div class="col-12">
            <div class="form-check">
              <input class="form-check-input" type="checkbox" id="reglas" required>
              <label class="form-check-label" for="reglas">Acepto las reglas del club</label>
              <div class="invalid-feedback">Debes aceptar las reglas para inscribirte.</div>
            </div>
          </div>

          <div class="col-12 d-grid">
            <button class="btn btn-primary btn-lg" type="submit">Enviar inscripción</button>
          </div>

        </form>
      </div>
    </div>
  </div>
</section>

Qué observar:

  • row g-3 + col-md-6: ¡el grid también sirve para formularios! Dos campos por fila en pantallas medianas, uno por fila en celular.
  • El <select> tiene una primera opción con value="" y disabled selected: es el "texto guía". Como está vacía y required, obliga a elegir una opción real.
  • edad usa min="13" max="18": el navegador rechaza valores fuera de rango.
  • d-none en la alerta de éxito: está oculta hasta que se necesite.
  • d-grid en el botón: lo estira a todo el ancho.

2. Agrega el JavaScript. Pega en js/main.js:

JavaScript
// ===== Validación del formulario de inscripción =====
const formulario = document.getElementById('formUnete');
const mensajeExito = document.getElementById('exito');

formulario.addEventListener('submit', (evento) => {
  evento.preventDefault(); // este sitio de práctica no envía datos a ningún servidor

  if (formulario.checkValidity()) {
    formulario.reset();
    formulario.classList.remove('was-validated');
    mensajeExito.classList.remove('d-none');
    mensajeExito.focus();               // el foco va al mensaje: quien navega con
                                         // teclado o lector de pantalla se entera
  } else {
    mensajeExito.classList.add('d-none');
    formulario.classList.add('was-validated');

    // Nadie se da cuenta de un error si no lo ve. Movemos el foco al primer
    // campo inválido: el navegador lo resalta y además lo deja preparado
    // para escribir de nuevo.
    const primerError = formulario.querySelector(':invalid');
    if (primerError) primerError.focus();
  }
});

Léelo con calma. No es mucho código:

  • getElementById busca los elementos por su id.
  • addEventListener('submit', ...) ejecuta una función cuando se intenta enviar el formulario.
  • evento.preventDefault() evita que la página se recargue.
  • formulario.checkValidity() le pregunta al navegador si todos los campos cumplen las reglas.
  • formulario.querySelector(':invalid') devuelve el primer campo que no las cumple.
  • Si es válido: limpia el formulario, muestra el mensaje de éxito y le da el foco.
  • Si no: activa was-validated y lleva el foco al primer campo con error.

#Por qué importa lo del foco

Marcar los campos en rojo no sirve de nada si la persona sigue mirando el botón. Al mandar el foco al primer error, quien usa teclado o lector de pantalla escucha "Correo electrónico, inválido" y puede corregirlo sin buscarlo con el mouse.

Para que mensajeExito.focus() funcione, el mensaje necesita poder recibir foco, y eso se logra con tabindex="-1". Es el valor especial que significa: "puede recibir el foco por código, pero no aparece en la secuencia del Tab".

3. Pruébalo así:

  1. Presiona Enviar inscripción con todo vacío → campos en rojo con sus mensajes.
  2. Escribe un correo mal escrito (ana@) → sigue en rojo.
  3. Escribe edad 25 → rojo; con 16 → verde.
  4. Completa todo correctamente y marca las reglas → aparece el mensaje de éxito.

⚠️ Sobre datos personales: este formulario no guarda ni envía nada. En un sitio real que recoja datos de menores de edad se necesita permiso de madres, padres o tutores y un manejo cuidadoso de la información. Nunca publiques datos personales de otras personas.

#Resultado esperado

Un formulario centrado con campos de etiqueta flotante. Al enviarlo vacío, todo lo obligatorio se marca en rojo con su mensaje explicativo. Al completarlo bien, el formulario se limpia y aparece una alerta verde.

#Errores comunes

Síntoma Causa Solución
Aparecen globitos del navegador en vez de los mensajes de Bootstrap Falta novalidate en el <form> Agrégalo
Nada se pone rojo ni verde No se agregó was-validated o main.js no está enlazado Revisa la consola (F12) por errores y confirma que el script esté al final del <body>
El mensaje de error nunca aparece El .invalid-feedback no está después del campo, como hermano Colócalo justo después del <input> o del <label>
Los campos flotantes se ven mal Falta placeholder en el input Los form-floating requieren un placeholder (aunque sea con una palabra)
Cannot read properties of null en la consola El id en JS no coincide con el HTML Compara formUnete y exito en ambos
Usas form-group o form-row Son de Bootstrap 4 En Bootstrap 5: mb-3 o row g-3
Los campos se ponen rojos pero el foco sigue en el botón Falta la línea que mueve el foco al primer :invalid Agrega el querySelector(':invalid') con su focus()
mensajeExito.focus() no hace nada El mensaje no puede recibir foco Agrega tabindex="-1" al div del mensaje
querySelector(':invalid') devuelve null El formulario es válido Solo llámalo dentro del else

#Mini-ejercicio

Haz que el campo usuario gamer (opcional) solo acepte letras, números y guion bajo, de 3 a 16 caracteres. Si escriben algo distinto, debe mostrar un mensaje de error. Si lo dejan vacío, debe seguir siendo válido.

💡 Pista Existe un atributo HTML5 llamado `pattern` que recibe una expresión regular. Para que el mensaje de error se vea bien dentro de un `.input-group`, agrega la clase `has-validation` al grupo y pon el `.invalid-feedback` **dentro** del grupo.
✅ Solución
HTML
<div class="input-group has-validation">
  <span class="input-group-text" id="arroba">@</span>
  <input type="text" class="form-control" id="usuario"
         placeholder="Tu usuario gamer (opcional)"
         aria-label="Tu usuario gamer (opcional)" aria-describedby="arroba"
         pattern="[A-Za-z0-9_]{3,16}">
  <div class="invalid-feedback">Usa de 3 a 16 letras, números o guion bajo.</div>
</div>
Un campo vacío sin `required` es válido aunque tenga `pattern`.

#✅ Punto de control

  • [ ] Mi formulario tiene todos sus campos con etiqueta asociada.
  • [ ] Al enviarlo vacío se ven los errores en rojo con mensajes.
  • [ ] Al llenarlo bien aparece el mensaje de éxito y se limpia.
  • [ ] Entiendo la función de required, novalidate, checkValidity() y was-validated.
  • [ ] Sé por qué hay que mover el foco al primer error y al mensaje de éxito.
  • [ ] Sé que no debo pedir ni publicar datos personales sin cuidado.

#Módulo 7: Componentes interactivos con JavaScript

⏱ 25 minutos

🎯 Objetivo: usar el JavaScript de Bootstrap para crear un acordeón, un carrusel y una ventana modal.

#Concepto

Bootstrap trae su propio JavaScript para los componentes interactivos. Puedes usarlo de dos maneras:

Forma Cómo Cuándo
Atributos data-bs-* Solo HTML, sin escribir JS La mayoría de los casos (lo que usaremos)
API de JavaScript bootstrap.Modal.getOrCreateInstance(...) Cuando necesitas abrir o cerrar con tu propia lógica

Los atributos más importantes:

Atributo Significado
data-bs-toggle="modal" / "collapse" Qué tipo de componente activa este botón
data-bs-target="#id" A qué elemento afecta
data-bs-dismiss="modal" / "alert" Cierra el componente
data-bs-slide="prev" / "next" Mueve el carrusel
data-bs-parent="#id" En un acordeón, hace que solo se abra uno a la vez

La regla de oro: los id deben coincidir exactamente entre el botón y el elemento al que apunta, y no pueden repetirse en la página.

#Pasos

1. Acordeón de preguntas frecuentes. Pega debajo de <!-- PREGUNTAS -->:

HTML
<section id="faq" class="py-5">
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-lg-8">
        <h2 class="text-center mb-4">Preguntas frecuentes</h2>

        <div class="accordion" id="acordeonFaq">
          <div class="accordion-item">
            <h3 class="accordion-header">
              <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1" aria-expanded="true" aria-controls="faq1">
                ¿Necesito experiencia para entrar?
              </button>
            </h3>
            <div id="faq1" class="accordion-collapse collapse show" data-bs-parent="#acordeonFaq">
              <div class="accordion-body">Para nada. Hay miembros que juegan desde hace años y otros que nunca han tenido una consola. Te ayudamos a empezar.</div>
            </div>
          </div>
          <div class="accordion-item">
            <h3 class="accordion-header">
              <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2" aria-expanded="false" aria-controls="faq2">
                ¿Cuánto cuesta?
              </button>
            </h3>
            <div id="faq2" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
              <div class="accordion-body">Unirte es gratis. Solo pedimos que cuides el material y participes en las actividades.</div>
            </div>
          </div>
          <div class="accordion-item">
            <h3 class="accordion-header">
              <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3" aria-expanded="false" aria-controls="faq3">
                ¿Cuándo nos reunimos?
              </button>
            </h3>
            <div id="faq3" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
              <div class="accordion-body">Los martes, miércoles y viernes después de clases. Te enviamos el calendario completo cuando te inscribas.</div>
            </div>
          </div>
          <div class="accordion-item">
            <h3 class="accordion-header">
              <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq4" aria-expanded="false" aria-controls="faq4">
                ¿Necesito mi propio equipo?
              </button>
            </h3>
            <div id="faq4" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
              <div class="accordion-body">No. El club tiene consolas, computadores y mandos para todos. Si quieres traer el tuyo, ¡bienvenido!</div>
            </div>
          </div>
        </div>

      </div>
    </div>
  </div>
</section>

Fíjate en el patrón: cada botón apunta (data-bs-target="#faq1") a su panel (id="faq1"). El primer ítem tiene show (abierto) y los demás collapsed en el botón.

2. Carrusel de testimonios. Pega debajo de <!-- COMUNIDAD -->:

HTML
<section id="testimonios" class="py-5 bg-body-tertiary">
  <div class="container">
    <h2 class="text-center mb-4">Lo que dice la comunidad</h2>

    <div class="row justify-content-center">
      <div class="col-lg-8">

        <div id="carruselComunidad" class="carousel slide" aria-label="Testimonios de miembros del club">

          <div class="carousel-indicators">
            <button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Testimonio 1"></button>
            <button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="1" aria-label="Testimonio 2"></button>
            <button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="2" aria-label="Testimonio 3"></button>
          </div>

          <div class="carousel-inner rounded-4 shadow">
            <div class="carousel-item active">
              <figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
                <blockquote class="fs-4 mb-3">“Entré por los torneos y salí sabiendo hacer mi primer videojuego.”</blockquote>
                <figcaption class="fw-semibold">Valentina, 16 años</figcaption>
              </figure>
            </div>
            <div class="carousel-item">
              <figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
                <blockquote class="fs-4 mb-3">“Aquí conocí a mis mejores amigos. Jugar en equipo es otro nivel.”</blockquote>
                <figcaption class="fw-semibold">Santiago, 17 años</figcaption>
              </figure>
            </div>
            <div class="carousel-item">
              <figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
                <blockquote class="fs-4 mb-3">“Pensé que era solo jugar, pero también aprendí a organizar eventos.”</blockquote>
                <figcaption class="fw-semibold">Camila, 15 años</figcaption>
              </figure>
            </div>
          </div>

          <button class="carousel-control-prev" type="button" data-bs-target="#carruselComunidad" data-bs-slide="prev">
            <span class="carousel-control-prev-icon" aria-hidden="true"></span>
            <span class="visually-hidden">Anterior</span>
          </button>
          <button class="carousel-control-next" type="button" data-bs-target="#carruselComunidad" data-bs-slide="next">
            <span class="carousel-control-next-icon" aria-hidden="true"></span>
            <span class="visually-hidden">Siguiente</span>
          </button>

        </div>

        <p class="text-center text-body-secondary small mt-3 mb-0">Los nombres de este ejemplo son ficticios.</p>
      </div>
    </div>
  </div>
</section>

El carrusel tiene tres partes: los indicadores (puntitos de abajo), las diapositivas (carousel-item; una debe llevar active) y los controles (flechas). Usamos botones con texto oculto (visually-hidden) para que las flechas sean comprensibles con lectores de pantalla. No lo configuramos con avance automático a propósito: el movimiento solo se activa por decisión de quien usa la página.

Agrega estilo a las diapositivas. Pega al final de css/style.css:

CSS
/* ===== 5. Carrusel de la comunidad ===== */
.testimonio {
  min-height: 280px;
  background: linear-gradient(135deg, #4b2bb5, #0b6fa8);
}

/* En modo oscuro Bootstrap pone controles negros en los carruseles.
   Como nuestras diapositivas son oscuras en ambos modos, los forzamos a blanco. */
.carousel {
  --bs-carousel-indicator-active-bg: #fff;
  --bs-carousel-caption-color: #fff;
  --bs-carousel-control-icon-filter: none;
}

3. Modal de reglas. Primero pega el modal justo debajo de <!-- MODAL -->:

HTML
<div class="modal fade" id="modalReglas" tabindex="-1" aria-labelledby="tituloReglas" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
    <div class="modal-content">

      <div class="modal-header">
        <h2 class="modal-title fs-5" id="tituloReglas">📜 Reglas de PixelCrew</h2>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
      </div>

      <div class="modal-body">
        <ol class="mb-0">
          <li class="mb-2"><strong>Respeto ante todo.</strong> Nada de insultos ni burlas, ni dentro ni fuera del juego.</li>
          <li class="mb-2"><strong>Juego limpio.</strong> Sin trampas, hacks ni cuentas prestadas en los torneos.</li>
          <li class="mb-2"><strong>Equilibrio.</strong> Las tareas del colegio van primero; el club puede esperar.</li>
          <li class="mb-2"><strong>Cuidamos el material.</strong> Consolas, mandos y equipos son de todos.</li>
          <li class="mb-2"><strong>Contenido apto.</strong> Los juegos deben ser adecuados para la edad de todos.</li>
          <li><strong>Si algo te incomoda</strong>, avísale a una persona responsable del club. Siempre te vamos a escuchar.</li>
        </ol>
      </div>

      <div class="modal-footer">
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Entendido</button>
      </div>

    </div>
  </div>
</div>

Luego conecta un botón con él. En el formulario del módulo anterior, reemplaza esta línea:

HTML
<label class="form-check-label" for="reglas">Acepto las reglas del club</label>

por esta, que convierte "reglas del club" en un botón que abre el modal:

HTML
<label class="form-check-label" for="reglas">
                Acepto las
                <button type="button" class="btn btn-link p-0 align-baseline" data-bs-toggle="modal" data-bs-target="#modalReglas">reglas del club</button>
              </label>

Puntos importantes del modal:

  • fade da una animación suave al aparecer.
  • modal-dialog-centered lo centra verticalmente y modal-dialog-scrollable permite desplazar el contenido si es largo.
  • aria-labelledby="tituloReglas" conecta el modal con su título para lectores de pantalla.
  • Bootstrap se encarga solo de atrapar el foco dentro del modal, de cerrarlo con la tecla Esc y de devolver el foco al botón que lo abrió cuando se cierra.
  • tabindex="-1" en el modal no es un detalle: le permite a Bootstrap poner el foco ahí al abrirlo. Si lo quitas, el foco se pierde y la persona que navega con teclado se queda sin saber dónde está.
  • El modal se coloca al final del <body>, fuera de cualquier sección, para evitar problemas de posición.

4. Prueba la API de JavaScript. Abre la consola (F12) y escribe:

JavaScript
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('modalReglas'));
modal.show();

Se abre el modal desde código. Ciérralo con modal.hide(). Cuando una acción (por ejemplo, un temporizador) debe abrir un componente, usarás esta forma.

#Resultado esperado

  • Acordeón: al abrir una pregunta, la anterior se cierra.
  • Carrusel: puedes pasar de testimonio con las flechas o los puntitos.
  • Modal: al hacer clic en "reglas del club" aparece una ventana centrada con el fondo oscurecido; se cierra con ✕, con "Entendido", con Esc o haciendo clic afuera.

#Errores comunes

Síntoma Causa Solución
El carrusel no muestra nada Ninguna diapositiva tiene active Pon active en la primera carousel-item
Los puntitos no cambian de diapositiva data-bs-slide-to no coincide con el número de diapositivas Cuéntalas: empiezan en 0
El modal no abre El data-bs-target no coincide con el id Compara #modalReglas con id="modalReglas"
El fondo del modal queda gris y bloqueado Un error de JS dejó el modal a medias Recarga y revisa la consola
Se abren varias preguntas a la vez Falta data-bs-parent Agrégalo a cada accordion-collapse
Dos acordeones se afectan entre sí Repetiste id Cada id debe ser único

#Mini-ejercicio

  1. Haz que el acordeón permita tener varias preguntas abiertas a la vez.
  2. Haz que el carrusel cambie de diapositiva con un efecto de desvanecimiento en vez de deslizamiento.
💡 Pista Para el 1, piensa qué atributo es el que "cierra a las demás". Para el 2, busca en la documentación oficial de Carousel el modificador `carousel-fade`.
✅ Solución 1. Quita el atributo `data-bs-parent="#acordeonFaq"` de cada `accordion-collapse`. 2. Cambia `class="carousel slide"` por `class="carousel slide carousel-fade"`.

#✅ Punto de control

  • [ ] El acordeón abre una pregunta a la vez.
  • [ ] El carrusel cambia con las flechas y con los puntitos.
  • [ ] El modal abre desde el enlace de las reglas y se cierra de varias maneras.
  • [ ] Entiendo qué relación hay entre data-bs-target e id.
  • [ ] Abrí el modal desde la consola con bootstrap.Modal.

#Módulo 8: Personalización, modo oscuro y Sass

⏱ 20 minutos

🎯 Objetivo: cambiar la apariencia de Bootstrap con tus colores, activar un botón de modo claro/oscuro y entender cuándo conviene usar Sass.

#Concepto

Hasta ahora tu sitio se ve "como Bootstrap". Para que se vea tuyo hay tres niveles de personalización, de menor a mayor esfuerzo:

Nivel Cómo Cuándo
1. Utilidades Clases como bg-primary, rounded-4 Siempre
2. Variables CSS Sobrescribes --bs-* en tu style.css Cambios de color, bordes y fuentes
3. Sass Compilas Bootstrap con tus valores Proyectos grandes con tema propio

#Variables CSS

Desde la versión 5.3, Bootstrap guarda sus colores, bordes y fuentes en variables CSS (--bs-primary, --bs-body-bg, --bs-border-radius...). Al cambiar una variable, todo lo que la usa cambia. Puedes verlas en las herramientas del navegador: selecciona la etiqueta <html> en la pestaña Elements y mira el panel Styles.

Hay dos tipos:

  • Globales (en :root): afectan a todo el sitio. Ejemplo: --bs-border-radius.
  • Locales de un componente: viven dentro de cada componente. Ejemplo: un botón usa --bs-btn-bg. Para cambiar solo los botones primarios, sobrescribes esa variable en .btn-primary.

#Modo claro y oscuro

Bootstrap 5.3 trae dos temas integrados. Se elige con el atributo data-bs-theme (light o dark) en <html> o en cualquier elemento: el tema se aplica a ese elemento y a todo lo que contiene. Por eso para alternar el modo solo hay que cambiar un atributo con JavaScript, y recordarlo con localStorage.

Respetar el tema del sistema

Hay una cosa que un sitio bien hecho sí o sí hace: adoptar el tema que ya eligió la persona en su dispositivo. Se lee con matchMedia:

JavaScript
window.matchMedia('(prefers-color-scheme: dark)').matches  // true si el sistema está en oscuro

Ese dark es el tema del sistema. También existe (prefers-color-scheme: light), (prefers-reduced-motion: reduce) (que ya usaste en el módulo 5) y otras como (prefers-contrast: more). Todas son la manera correcta de "preguntarle" al dispositivo.

La regla de un sitio bien construido es esta:

Primero respetas el tema del sistema. Después, si la persona elige otro con tu botón, su elección manda (y se recuerda).

El parpadeo: por qué el script va en el <head>

Un problema típico: si el tema lo aplica JavaScript al final de la página, el navegador primero pinta el sitio en claro y un instante después lo cambia a oscuro. Se ve un parpadeo Diez unpleasantísimo.

La solución es la que pusimos en el módulo 1: un <script> diminuto en el <head>, antes del CSS, que aplica el tema antes de que se pinte el primer píxel. Ese script debe ser cortísimo y no depender de nada: por eso va dentro de una función immediately ejecutada ((function () { ... })()) y protegido con try/catch, para que un navegador en modo privado (donde localStorage puede estar bloqueado) no rompa la página.

#Pasos

1. Agrega el botón de modo. En la navbar, pega después del <li> del botón "Únete" (dentro del mismo <ul>):

HTML
<li class="nav-item ms-lg-2 mb-2 mb-lg-0">
  <button id="themeToggle" class="btn btn-outline-secondary btn-sm" type="button" aria-label="Cambiar entre modo claro y modo oscuro">
    <span id="themeIcon" aria-hidden="true">☀️</span>
  </button>
</li>

2. Agrega el JavaScript al final de js/main.js:

JavaScript
// ===== Modo claro / oscuro =====
const raiz = document.documentElement;
const botonTema = document.getElementById('themeToggle');
const iconoTema = document.getElementById('themeIcon');
const CLAVE = 'pc-tema'; // el nombre con el que guardamos la preferencia

// El tema que ya tiene el dispositivo. Se consulta, no se supone.
const consultaSistema = window.matchMedia('(prefers-color-scheme: dark)');

// Leer y guardar puede fallar en modo privado: por eso van protegidos.
function leerTemaGuardado() {
  try {
    return localStorage.getItem(CLAVE);
  } catch (error) {
    return null;
  }
}

function aplicarTema(tema, recordar) {
  raiz.setAttribute('data-bs-theme', tema);
  iconoTema.textContent = tema === 'dark' ? '☀️' : '🌙'; // el ícono muestra a qué modo cambiarás

  // Solo guardamos cuando la ELECCIÓN es de la persona. Si guardáramos el
  // tema del sistema, después no sabríamos si ella eligió o si fue el default.
  if (recordar) {
    try {
      localStorage.setItem(CLAVE, tema);
    } catch (error) {
      /* en modo privado no se guarda, pero el sitio funciona igual */
    }
  }
}

// Al cargar: primero la elección de la persona; si no hay, la del sistema.
aplicarTema(leerTemaGuardado() || (consultaSistema.matches ? 'dark' : 'light'), false);

botonTema.addEventListener('click', () => {
  const actual = raiz.getAttribute('data-bs-theme');
  aplicarTema(actual === 'dark' ? 'light' : 'dark', true);
});

// Si la persona NO ha elegido nada y cambia el tema del sistema, la seguimos.
consultaSistema.addEventListener('change', (evento) => {
  if (!leerTemaGuardado()) {
    aplicarTema(evento.matches ? 'dark' : 'light', false);
  }
});

Qué hace, en orden:

  1. Consulta el tema del dispositivo con matchMedia('(prefers-color-scheme: dark)').
  2. Aplica el guardado; si no hay, el del sistema.
  3. Cambia el emoji del botón.
  4. Guarda la preferencia (solo si la elección fue suya), protegida con try/catch.
  5. Escucha los cambios del tema del sistema, pero solo si la persona no ha elegido nada.

⚠️ El detalle que hace que la última parte funcione es el parámetro recordar. Si al cargar guardáramos el tema del sistema, leerTemaGuardado() devolvería siempre un valor y el sitio dejaría de seguir al sistema después del primer clic. Por eso el valor por defecto se aplica con recordar = false.

💡 El try/catch parece exagerado, y sin embargo es lo correcto: en modo incógnito, localStorage puede lanzar un error. Sin esa protección, una sola línea detiene todo el archivo de JavaScript, y con ella se rompen también el formulario y el menú.

3. Personaliza con variables CSS. Pega al inicio de css/style.css (antes de lo que ya escribiste):

CSS
/* ===== 1. Variables: personalizamos Bootstrap sin tocar sus archivos ===== */
:root {
  --pc-accent: #7c4dff;

  /* Variables de Bootstrap que sobrescribimos */
  --bs-primary: #7c4dff;
  --bs-primary-rgb: 124, 77, 255;
  --bs-primary-text-emphasis: #4a22b8;
  --bs-primary-bg-subtle: #e9e0ff;
  --bs-primary-border-subtle: #cbb8ff;
  --bs-border-radius: 0.75rem;
}

/* Este bloque va DESPUÉS de :root para ganar en modo oscuro */
[data-bs-theme="dark"] {
  --bs-body-bg: #0e0f1f;
  --bs-body-bg-rgb: 14, 15, 31;
  --bs-tertiary-bg: #161833;
  --bs-tertiary-bg-rgb: 22, 24, 51;
  --bs-primary-text-emphasis: #b8a0ff;
  --bs-primary-bg-subtle: #241a4d;
  --bs-primary-border-subtle: #4a2fa8;
}

/* Los componentes de Bootstrap guardan sus colores en variables LOCALES.
   Las cambiamos para que los botones también usen nuestro morado. */
.btn-primary {
  --bs-btn-bg: var(--pc-accent);
  --bs-btn-border-color: var(--pc-accent);
  --bs-btn-hover-bg: #6a3de8;
  --bs-btn-hover-border-color: #6a3de8;
  --bs-btn-active-bg: #5a30d0;
  --bs-btn-active-border-color: #5a30d0;
  --bs-btn-disabled-bg: var(--pc-accent);
  --bs-btn-disabled-border-color: var(--pc-accent);
  --bs-btn-focus-shadow-rgb: 124, 77, 255;
}

.btn-outline-primary {
  --bs-btn-color: var(--bs-primary-text-emphasis);
  --bs-btn-border-color: var(--pc-accent);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--pc-accent);
  --bs-btn-hover-border-color: var(--pc-accent);
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #5a30d0;
  --bs-btn-active-border-color: #5a30d0;
  --bs-btn-focus-shadow-rgb: 124, 77, 255;
}

.card {
  --bs-card-bg: var(--bs-tertiary-bg);
}
CSS
/* ===== 2. Ajustes generales ===== */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* Evita que la navbar fija tape los títulos al navegar por el menú */
section[id] { scroll-margin-top: 4.5rem; }

Observa los dos patrones:

  • Variables globales (--bs-primary, --bs-body-bg...) que definen la paleta, una para el modo claro (:root) y otra para el oscuro ([data-bs-theme="dark"]).
  • Variables locales de componentes (.btn-primary, .btn-outline-primary) para que los botones también adopten el morado.

⚠️ Cambiar --bs-primary no cambia el color de .btn-primary, porque los botones tienen su propia copia del color. Por eso fue necesario sobrescribir también --bs-btn-bg y sus variantes. Si tuvieras que hacer esto con muchos componentes, ahí es donde conviene Sass.

También verás que section[id] { scroll-margin-top } resuelve el problema del módulo 4: la navbar fija ya no tapa los títulos al navegar.

4. Introducción a Sass (lectura, opcional para practicar).

Sass es un lenguaje que se compila a CSS. Con Bootstrap te permite cambiar sus variables internas antes de generar el CSS, y entonces toda la paleta (botones, alertas, fondos suaves, bordes...) se recalcula automáticamente.

El flujo es:

Terminal
# 1. Crear un proyecto e instalar Bootstrap y Sass (necesitas Node.js)
npm init -y
npm install bootstrap@5.3.8 sass
SCSS
// scss/custom.scss
// 2. Tus variables ANTES de importar Bootstrap
$primary: #7c4dff;
$border-radius: 0.75rem;

// 3. Importa Bootstrap
@import "bootstrap/scss/bootstrap";
Terminal
# 4. Compila a CSS
npx sass --load-path=node_modules scss/custom.scss css/custom.css

Luego enlazas css/custom.css en lugar del CSS del CDN.

⚠️ Esto es solo lectura, no lo hagas todavía. Si lo intentas hoy vas a ver una montaña de avisos rojos: Bootstrap 5.3 todavía usa @import (la sintaxis vieja) y el compilador de Sass moderno la marca como deprecada. Los avisos no rompen nada, pero desconciertan mucho. Además compilar Sass necesita Node.js instalado, y en un portátil del colegio eso puede ser un problema.

La ruta moderna es @use, pero Bootstrap todavía no la usa por dentro. Por ahora quédate con las variables CSS del paso 3, que no necesitas compilar nada y hacen el 90 % de lo que querrías hacer con Sass. Cuando tengas un proyecto propio y Node.js funcionando, vuelve aquí.

💡 ¿Cuándo usar cada una? Variables CSS para ajustes rápidos y para cambiar de tema en tiempo real. Sass cuando quieres una identidad visual completa y estás en un proyecto con herramientas de desarrollo.

#Resultado esperado

El sitio tiene una paleta morada. En la barra hay un botón con ☀️ (en modo oscuro) o 🌙 (en modo claro) que cambia todo el sitio. Al recargar, se mantiene el modo elegido. Los botones principales son morados y las esquinas son un poco más redondeadas.

#Errores comunes

Síntoma Causa Solución
Mis variables no se aplican style.css se carga antes que Bootstrap Debe ir después en el <head>
El botón de modo no hace nada main.js falló antes Revisa la consola: un error en el formulario detiene todo el archivo
Cannot read properties of null (reading 'addEventListener') El botón no existe en el HTML Confirma que pegaste el <li> con id="themeToggle"
El modo oscuro no recuerda mi elección localStorage bloqueado por el navegador Prueba con otro navegador o con Live Server
Cambié --bs-primary pero los botones siguen azules Los botones usan variables locales Sobrescribe también --bs-btn-bg en .btn-primary
En modo oscuro algunos colores no cambian Pusiste tu bloque oscuro antes que :root [data-bs-theme="dark"] debe ir después de :root
El sitio arranca siempre en oscuro, aunque tu sistema esté en claro Usaste || 'dark' como valor por defecto Deja que el default sea el del sistema: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
El sitio parpadea al cargar El tema se aplica con JavaScript al final de la página Pon el script del <head> (módulo 1), antes del CSS
SecurityError o localStorage is not defined Modo privado con almacenamiento bloqueado Ya está protegido con try/catch; si ves esto, revisa que lo pegaste bien
Cambias el tema del sistema y el sitio no se mueve La persona ya había elegido con el botón Es lo correcto: su elección manda. Borra pc-tema en la consola para volver al del sistema

#Mini-ejercicio

  1. Cambia el radio de las esquinas de todo el sitio (botones, tarjetas, campos) con una sola línea.
  2. Cambia la fuente del sitio por una de sistema como Georgia, serif.
💡 Pista Mira la lista de variables `--bs-*` en el panel Styles. Hay una variable de radio de borde y otra de familia de fuente del cuerpo (`--bs-body-font-family`).
✅ Solución En `:root` de tu `style.css`:
CSS
:root {
  --bs-border-radius: 1.25rem;
  --bs-body-font-family: Georgia, serif;
}
Si ya tenías `--bs-border-radius` definido en ese bloque, solo cambia su valor.

#✅ Punto de control

  • [ ] Mi sitio alterna entre modo claro y oscuro con el botón.
  • [ ] El modo elegido se mantiene al recargar.
  • [ ] Si borro la preferencia guardada, el sitio arranca con el tema de mi dispositivo.
  • [ ] Sé por qué el script del tema va en el <head> y no al final.
  • [ ] Los botones y los colores principales usan mi paleta.
  • [ ] Entiendo la diferencia entre variables globales y variables locales de componente.
  • [ ] Sé para qué sirve Sass y cuándo conviene usarlo.

#Módulo 9: Integración final y buenas prácticas

⏱ 15 minutos

🎯 Objetivo: terminar el sitio con el pie de página y revisarlo como lo haría un profesional: accesibilidad, orden del código y pruebas en distintos tamaños.

#Concepto

Un sitio "terminado" no es el que se ve bien en tu computador, sino el que funciona para todas las personas y en todos los dispositivos. Hay tres frentes de revisión: estructura y accesibilidad, responsive y calidad del código.

#Pasos

1. Agrega el pie de página. Pega debajo de <!-- FOOTER -->:

HTML
<footer class="border-top py-4 mt-5">
  <div class="container d-flex flex-column flex-md-row justify-content-between align-items-center gap-3">
    <p class="mb-0 text-body-secondary small">© 2026 PixelCrew · Hecho con Bootstrap 5.3.8</p>

    <ul class="nav">
      <li class="nav-item"><a class="nav-link px-2" href="#juegos">Qué hacemos</a></li>
      <li class="nav-item"><a class="nav-link px-2" href="#faq">Preguntas</a></li>
      <li class="nav-item"><a class="nav-link px-2" href="#unete">Únete</a></li>
    </ul>

    <a href="#inicio" class="btn btn-outline-secondary btn-sm">Volver arriba ↑</a>
  </div>
</footer>

Detalles: flex-column flex-md-row apila los elementos en celular y los pone en línea desde tablet, y justify-content-between los reparte a los extremos. mt-5 separa el pie del contenido.

2. Haz la revisión de accesibilidad. Recorre esta lista con tu sitio abierto:

Qué revisar Cómo comprobarlo
Un solo <h1> y títulos en orden (h1 → h2 → h3) Busca <h1 con Ctrl + F en el código
Landmarks: <header>, <nav>, <main>, <footer> Revisa que estén presentes y que no falten
Teclado: todo se puede usar solo con Tab, Enter y Esc Deja el mouse quieto y recorre el sitio
Foco visible: al tabular se ve dónde estás Bootstrap lo trae; no lo quites con outline: none
Textos alternativos: cada foto con alt="descripción real", y los emojis decorativos con role="img" + aria-label Las fotos que agregaste en el módulo 2 y los seis emojis de la galería
Foco al enviar un formulario: el foco va al primer error o al mensaje de éxito Envía el formulario vacío y presiona Tab: el foco debe estar en el campo marcado
Anclas: ningún enlace apunta a un id inexistente Haz clic en cada enlace del menú y en "Volver arriba"
Sin desbordes: nada obliga a desplazar la página de lado a lado A 375 px no debe aparecer barra horizontal
Etiquetas en formularios: todo campo tiene su <label> Haz clic en una etiqueta: debe enfocar su campo
Contraste de colores: texto legible sobre su fondo Usa WebAIM Contrast Checker
Movimiento: nada se mueve solo; se respeta prefers-reduced-motion Ya lo hicimos en el CSS
Enlace "Saltar al contenido" Presiona Tab al cargar

3. Prueba el diseño responsive. En las herramientas del navegador, comprueba estos anchos: 375 px (celular), 768 px (tablet), 1280 px (laptop). En cada uno confirma que: no hay scroll horizontal, nada se corta, los botones se pueden tocar con facilidad y los textos se leen sin hacer zoom.

4. Haz una auditoría automática. En Chrome o Edge: F12 → pestaña Lighthouse → marca Accessibility y Best Practices → Analyze page load. Lee las sugerencias. No necesitas 100 puntos, pero entender cada aviso te enseña mucho.

5. Valida tu HTML pegándolo en el validador del W3C. Corrige lo que marque como error.

#Buenas prácticas para tus propios proyectos

  • Usa utilidades primero, y CSS propio solo cuando no exista una utilidad.
  • No modifiques los archivos de Bootstrap. Siempre sobrescribe en tu propio CSS.
  • Evita !important. Si lo necesitas mucho, tu CSS está mal ordenado o es muy débil.
  • Mantén el orden: primero el CSS de Bootstrap, luego el tuyo; primero el JS de Bootstrap, luego el tuyo.
  • Usa HTML semántico (<section>, <nav>, <footer>) y no solo <div>.
  • Piensa primero en el celular y luego agrega md, lg, etc.
  • Nombra tus clases propias con sentido (.hero-card, .text-gradient).
  • Un id solo una vez por página.
  • Consulta la documentación de cada componente: cada una trae el HTML mínimo y las variantes.

#Resultado esperado

El sitio completo: navbar, hero, estadísticas, tarjetas, galería, carrusel, acordeón, formulario, modal y pie de página, con modo claro y oscuro. Sin errores en la consola.

#Errores comunes

Síntoma Causa Solución
Hay scroll horizontal en celular Casi siempre hay dos causas: un g-5 dentro de un .container, o un texto largo sin partir Con F12 selecciona el elemento culpable: si es una .row, cambia a g-4 g-lg-5; si es un texto (correo, URL), agrega overflow-wrap: anywhere al body
La barra horizontal aparece solo en celular Estás viendo el gutter grande en pantallas chicas g-4 en móvil y sube a g-5 con g-lg-5
El sitio parpadea al abrir El tema se aplica al final del JS Mueve el script del tema al <head> (módulo 1)
El pie queda a mitad de pantalla en páginas cortas Poco contenido Es normal en este sitio; para pegarlo abajo puedes usar d-flex flex-column min-vh-100 en el <body>
Lighthouse reporta bajo contraste Texto gris sobre fondo oscuro Usa text-body-secondary o aclara el color
Los id están duplicados Copiaste bloques sin cambiarlos Cámbialos y actualiza sus referencias

#Mini-ejercicio

Ejecuta Lighthouse en modo claro y en modo oscuro. Escribe el número de accesibilidad de cada uno y elige un aviso para corregir.

💡 Pista Cambia de modo con el botón de la navbar y vuelve a analizar. Para entender un aviso, haz clic sobre él: Lighthouse te muestra el elemento y un enlace con la explicación.

#✅ Punto de control

  • [ ] El pie de página está agregado y se ve bien en celular y escritorio.
  • [ ] Recorrí todo el sitio usando solo el teclado.
  • [ ] Probé el sitio en 375, 768 y 1280 px sin scroll horizontal.
  • [ ] Ejecuté Lighthouse y leí sus sugerencias.
  • [ ] La consola del navegador no muestra errores.
  • [ ] En el formulario, el foco salta al primer error y luego al mensaje de éxito.
  • [ ] Borré la preferencia de tema guardada y el sitio arrancó con el tema de mi sistema.

#Módulo 10: Tu reto final y siguientes pasos

⏱ 10 minutos de planificación y primer paso. El reto completo te tomará entre 30 y 60 minutos adicionales a tu ritmo.

🎯 Objetivo: pasar de "seguir instrucciones" a "tomar decisiones". Aquí no hay código para copiar.

#Reto 1: amplía PixelCrew

Agrega una nueva sección "Calendario de torneos" a tu sitio, sin copiar código del tutorial. Debe cumplir:

  • [ ] Estar entre las secciones "Galería" y "Comunidad", con su enlace en la navbar.
  • [ ] Usar al menos 3 componentes que no hayas usado en el sitio. Opciones: tabla (table, table-striped) dentro de table-responsive, pestañas (nav-tabs con tab-content), lista de grupo (list-group), barra de progreso (progress), tooltip o toast.
  • [ ] Verse bien en 375, 768 y 1280 px.
  • [ ] Funcionar en modo claro y oscuro.
  • [ ] Respetar las reglas de accesibilidad del módulo 9.

💡 Busca cada componente en la documentación oficial: copia su HTML mínimo y adáptalo. Leer la documentación y adaptar ejemplos es exactamente cómo trabajan los profesionales.

#Reto 2: tu propio proyecto

Elige algo que te importe y que quieras construir. Ideas:

Idea Secciones sugeridas
Portafolio personal Presentación, proyectos (tarjetas), contacto
Sitio de tu banda o equipo Integrantes, próximos eventos, galería
Página de un evento del colegio Programa, ubicación, inscripción
Mini tienda de un emprendimiento Productos (tarjetas), preguntas, contacto
Fan site de un hobby (anime, deportes, música) Noticias, galería, comunidad

Plan de 10 minutos (haz esto ahora):

  1. Dibuja en papel cómo se vería en celular: bloques de arriba hacia abajo.
  2. Lista las secciones y al lado escribe qué componente de Bootstrap usarías para cada una (usa la tabla de abajo).
  3. Crea el esqueleto: copia la plantilla del módulo 1, cambia el título y deja los marcadores.
  4. Construye la primera sección y pruébala en el celular (modo responsive) antes de seguir.

¿Qué componente uso para...?

Necesito... Uso...
Menú de navegación navbar
Un bloque de portada container + utilidades (como el hero)
Mostrar productos, personas o proyectos card dentro de un grid
Mostrar varias fotos en fila row-cols-* + ratio o card
Preguntas y respuestas accordion
Información que aparece sobre la página modal u offcanvas
Mensajes de aviso alert o toast
Datos ordenados table dentro de table-responsive
Cambiar entre contenidos en el mismo espacio nav-tabs o nav-pills
Recoger datos del usuario form + validación

#Siguientes pasos

Cuando te sientas cómodo/a con Bootstrap, estas son las rutas naturales para crecer:

  1. Íconos: Bootstrap Icons (más de 2000 íconos gratis).
  2. JavaScript: aprende lo básico del lenguaje para dar más vida a tus sitios (el módulo 6 y el 8 te dieron una probada).
  3. Git y GitHub: guarda tu código y publica tu sitio gratis con GitHub Pages.
  4. Herramientas modernas: npm, Sass y un empaquetador como Vite.
  5. Otras ideas de diseño: estudia las plantillas de ejemplo de Bootstrap para ver cómo se combinan los componentes.

#Código completo del proyecto

Compara tu trabajo con la versión final. Si algo falla, busca la diferencia con tu archivo.

#index.html

HTML
<!DOCTYPE html>
<html lang="es" data-bs-theme="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="PixelCrew: el club de videojuegos de tu colegio. Torneos, noches de juego y talleres.">
  <title>PixelCrew | Club de videojuegos</title>

  <!-- Aplica el tema ANTES de que se vea la página (evita el parpadeo).
       Va primero en el <head> a propósito; en un sitio real se le llama
       "anti-flash" y casi siempre se escribe en una sola línea. -->
  <script>
    (function () {
      try {
        var guardado = localStorage.getItem('pc-tema');
        var oscuroDelSistema = window.matchMedia('(prefers-color-scheme: dark)').matches;
        document.documentElement.setAttribute('data-bs-theme', guardado || (oscuroDelSistema ? 'dark' : 'light'));
      } catch (error) {
        document.documentElement.setAttribute('data-bs-theme', 'light');
      }
    })();
  </script>

  <!-- Bootstrap 5.3.8 (CSS) -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">

  <!-- Tu CSS (siempre DESPUÉS de Bootstrap) -->
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <!-- NAVBAR -->
  <a class="visually-hidden-focusable" href="#contenido">Saltar al contenido</a>

  <header>
    <nav class="navbar navbar-expand-lg bg-body-tertiary border-bottom sticky-top" aria-label="Navegación principal">
      <div class="container">
        <a class="navbar-brand fw-bold" href="#inicio">🎮 PixelCrew</a>

        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuPrincipal" aria-controls="menuPrincipal" aria-expanded="false" aria-label="Abrir o cerrar el menú">
          <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="menuPrincipal">
          <ul class="navbar-nav ms-auto mb-2 mb-lg-0 align-items-lg-center">
            <li class="nav-item"><a class="nav-link" href="#juegos">Qué hacemos</a></li>
            <li class="nav-item"><a class="nav-link" href="#galeria">Galería</a></li>
            <li class="nav-item"><a class="nav-link" href="#testimonios">Comunidad</a></li>
            <li class="nav-item"><a class="nav-link" href="#faq">Preguntas</a></li>
            <li class="nav-item ms-lg-3 my-2 my-lg-0"><a class="btn btn-primary btn-sm" href="#unete">Únete</a></li>
            <li class="nav-item ms-lg-2 mb-2 mb-lg-0">
              <button id="themeToggle" class="btn btn-outline-secondary btn-sm" type="button" aria-label="Cambiar entre modo claro y modo oscuro">
                <span id="themeIcon" aria-hidden="true">☀️</span>
              </button>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>

  <main id="contenido">
    <!-- HERO -->
    <section id="inicio" class="hero py-5">
      <div class="container py-lg-5">
        <div class="row align-items-center g-4 g-lg-5">

          <div class="col-lg-7">
            <p class="text-uppercase fw-semibold text-primary-emphasis small mb-2">Club de videojuegos del colegio</p>
            <h1 class="display-3 fw-bold mb-3">Juega, crea y compite con <span class="text-gradient">PixelCrew</span></h1>
            <p class="lead text-body-secondary mb-4">
              Somos el club donde los videojuegos se juegan, se estudian y hasta se programan.
              Si tienes entre 14 y 18 años, hay un lugar para ti en el equipo.
            </p>
            <div class="d-flex flex-column flex-sm-row gap-3">
              <a href="#unete" class="btn btn-primary btn-lg px-4">Únete al club</a>
              <a href="#juegos" class="btn btn-outline-secondary btn-lg px-4">Descubre qué hacemos</a>
            </div>
          </div>

          <div class="col-lg-5 d-none d-lg-block">
            <div class="hero-card rounded-4 shadow-lg p-5 text-center text-white" aria-hidden="true">
              <div class="display-1">🎮</div>
              <p class="fs-4 fw-semibold mb-0">Nivel 1: ¡empieza hoy!</p>
            </div>
          </div>

        </div>
      </div>
    </section>
    <!-- ESTADÍSTICAS -->
    <section class="py-4 border-top border-bottom bg-body-tertiary" aria-label="El club en números">
      <div class="container">
        <div class="row g-3 text-center">
          <div class="col-6 col-md-3">
            <p class="display-6 fw-bold text-primary-emphasis mb-0">120+</p>
            <p class="text-body-secondary mb-0">miembros</p>
          </div>
          <div class="col-6 col-md-3">
            <p class="display-6 fw-bold text-primary-emphasis mb-0">12</p>
            <p class="text-body-secondary mb-0">torneos al año</p>
          </div>
          <div class="col-6 col-md-3">
            <p class="display-6 fw-bold text-primary-emphasis mb-0">3</p>
            <p class="text-body-secondary mb-0">talleres semanales</p>
          </div>
          <div class="col-6 col-md-3">
            <p class="display-6 fw-bold text-primary-emphasis mb-0">1</p>
            <p class="text-body-secondary mb-0">sala gamer</p>
          </div>
        </div>
      </div>
    </section>
    <!-- QUÉ HACEMOS -->
    <section id="juegos" class="py-5">
      <div class="container">

        <div class="alert alert-info alert-dismissible fade show" role="alert">
          <strong>¡Inscripciones abiertas!</strong> Hay cupos disponibles hasta el 31 de octubre.
          <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar aviso"></button>
        </div>

        <div class="row justify-content-center my-5">
          <div class="col-lg-7 text-center">
            <h2 class="mb-2">Más que jugar</h2>
            <p class="text-body-secondary mb-0">Tres formas de formar parte del club. Elige la tuya (o prueba las tres).</p>
          </div>
        </div>

        <div class="row row-cols-1 row-cols-md-3 g-4">

          <div class="col">
            <div class="card h-100 card-hover shadow-sm">
              <div class="card-body">
                <span class="badge text-bg-warning mb-3">Cada viernes</span>
                <h3 class="card-title h5">🏆 Torneos</h3>
                <p class="card-text text-body-secondary">Compite en equipo o en solitario en juegos para todos los niveles. Hay premios, diplomas y mucha rivalidad sana.</p>
              </div>
              <div class="card-footer bg-transparent border-0 pb-3">
                <a href="#unete" class="btn btn-outline-primary w-100">Quiero competir</a>
              </div>
            </div>
          </div>

          <div class="col">
            <div class="card h-100 card-hover shadow-sm">
              <div class="card-body">
                <span class="badge text-bg-success mb-3">Cada miércoles</span>
                <h3 class="card-title h5">🕹️ Noches de juego</h3>
                <p class="card-text text-body-secondary">Juegos cooperativos y de mesa, música y snacks. Ideal para conocer gente nueva sin presión.</p>
              </div>
              <div class="card-footer bg-transparent border-0 pb-3">
                <a href="#unete" class="btn btn-outline-primary w-100">Me apunto</a>
              </div>
            </div>
          </div>

          <div class="col">
            <div class="card h-100 card-hover shadow-sm">
              <div class="card-body">
                <span class="badge text-bg-info mb-3">Cada martes</span>
                <h3 class="card-title h5">💻 Taller de creación</h3>
                <p class="card-text text-body-secondary">Aprende a diseñar niveles y personajes, y a programar tu primer videojuego desde cero.</p>
              </div>
              <div class="card-footer bg-transparent border-0 pb-3">
                <a href="#unete" class="btn btn-outline-primary w-100">Quiero crear</a>
              </div>
            </div>
          </div>

        </div>
      </div>
    </section>
    <!-- GALERÍA -->
    <section id="galeria" class="py-5">
      <div class="container">
        <h2 class="text-center mb-2">Momentos del club</h2>
        <p class="text-center text-body-secondary mb-4">Un vistazo a lo que pasa en las reuniones. (Aquí irían tus fotos reales.)</p>

        <div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
          <div class="col">
            <div class="ratio ratio-1x1">
              <div class="rounded-4 bg-primary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un trofeo">🏆</div>
            </div>
          </div>
          <div class="col">
            <div class="ratio ratio-1x1">
              <div class="rounded-4 bg-success d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un joystick">🕹️</div>
            </div>
          </div>
          <div class="col">
            <div class="ratio ratio-1x1">
              <div class="rounded-4 bg-danger d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de unos audífonos">🎧</div>
            </div>
          </div>
          <div class="col">
            <div class="ratio ratio-1x1">
              <div class="rounded-4 bg-warning d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un computador">💻</div>
            </div>
          </div>
          <div class="col">
            <div class="ratio ratio-1x1">
              <div class="rounded-4 bg-info d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un dado">🎲</div>
            </div>
          </div>
          <div class="col">
            <div class="ratio ratio-1x1">
              <div class="rounded-4 bg-secondary d-flex align-items-center justify-content-center fs-1" role="img" aria-label="Ilustración de un apretón de manos">🤝</div>
            </div>
          </div>
        </div>
      </div>
    </section>
    <!-- COMUNIDAD -->
    <section id="testimonios" class="py-5 bg-body-tertiary">
      <div class="container">
        <h2 class="text-center mb-4">Lo que dice la comunidad</h2>

        <div class="row justify-content-center">
          <div class="col-lg-8">

            <div id="carruselComunidad" class="carousel slide" aria-label="Testimonios de miembros del club">

              <div class="carousel-indicators">
                <button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Testimonio 1"></button>
                <button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="1" aria-label="Testimonio 2"></button>
                <button type="button" data-bs-target="#carruselComunidad" data-bs-slide-to="2" aria-label="Testimonio 3"></button>
              </div>

              <div class="carousel-inner rounded-4 shadow">
                <div class="carousel-item active">
                  <figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
                    <blockquote class="fs-4 mb-3">“Entré por los torneos y salí sabiendo hacer mi primer videojuego.”</blockquote>
                    <figcaption class="fw-semibold">Valentina, 16 años</figcaption>
                  </figure>
                </div>
                <div class="carousel-item">
                  <figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
                    <blockquote class="fs-4 mb-3">“Aquí conocí a mis mejores amigos. Jugar en equipo es otro nivel.”</blockquote>
                    <figcaption class="fw-semibold">Santiago, 17 años</figcaption>
                  </figure>
                </div>
                <div class="carousel-item">
                  <figure class="testimonio text-white text-center px-5 py-5 mb-0 d-flex flex-column justify-content-center">
                    <blockquote class="fs-4 mb-3">“Pensé que era solo jugar, pero también aprendí a organizar eventos.”</blockquote>
                    <figcaption class="fw-semibold">Camila, 15 años</figcaption>
                  </figure>
                </div>
              </div>

              <button class="carousel-control-prev" type="button" data-bs-target="#carruselComunidad" data-bs-slide="prev">
                <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                <span class="visually-hidden">Anterior</span>
              </button>
              <button class="carousel-control-next" type="button" data-bs-target="#carruselComunidad" data-bs-slide="next">
                <span class="carousel-control-next-icon" aria-hidden="true"></span>
                <span class="visually-hidden">Siguiente</span>
              </button>

            </div>

            <p class="text-center text-body-secondary small mt-3 mb-0">Los nombres de este ejemplo son ficticios.</p>
          </div>
        </div>
      </div>
    </section>
    <!-- PREGUNTAS -->
    <section id="faq" class="py-5">
      <div class="container">
        <div class="row justify-content-center">
          <div class="col-lg-8">
            <h2 class="text-center mb-4">Preguntas frecuentes</h2>

            <div class="accordion" id="acordeonFaq">
              <div class="accordion-item">
                <h3 class="accordion-header">
                  <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#faq1" aria-expanded="true" aria-controls="faq1">
                    ¿Necesito experiencia para entrar?
                  </button>
                </h3>
                <div id="faq1" class="accordion-collapse collapse show" data-bs-parent="#acordeonFaq">
                  <div class="accordion-body">Para nada. Hay miembros que juegan desde hace años y otros que nunca han tenido una consola. Te ayudamos a empezar.</div>
                </div>
              </div>
              <div class="accordion-item">
                <h3 class="accordion-header">
                  <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2" aria-expanded="false" aria-controls="faq2">
                    ¿Cuánto cuesta?
                  </button>
                </h3>
                <div id="faq2" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
                  <div class="accordion-body">Unirte es gratis. Solo pedimos que cuides el material y participes en las actividades.</div>
                </div>
              </div>
              <div class="accordion-item">
                <h3 class="accordion-header">
                  <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3" aria-expanded="false" aria-controls="faq3">
                    ¿Cuándo nos reunimos?
                  </button>
                </h3>
                <div id="faq3" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
                  <div class="accordion-body">Los martes, miércoles y viernes después de clases. Te enviamos el calendario completo cuando te inscribas.</div>
                </div>
              </div>
              <div class="accordion-item">
                <h3 class="accordion-header">
                  <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq4" aria-expanded="false" aria-controls="faq4">
                    ¿Necesito mi propio equipo?
                  </button>
                </h3>
                <div id="faq4" class="accordion-collapse collapse" data-bs-parent="#acordeonFaq">
                  <div class="accordion-body">No. El club tiene consolas, computadores y mandos para todos. Si quieres traer el tuyo, ¡bienvenido!</div>
                </div>
              </div>
            </div>

          </div>
        </div>
      </div>
    </section>
    <!-- ÚNETE -->
    <section id="unete" class="py-5">
      <div class="container">
        <div class="row justify-content-center">
          <div class="col-lg-7">

            <h2 class="text-center mb-2">Únete a PixelCrew</h2>
            <p class="text-center text-body-secondary mb-4">Completa el formulario y te contamos cuándo es la primera reunión.</p>

            <div id="exito" class="alert alert-success d-none" role="status" tabindex="-1">
              🎉 ¡Listo! Recibimos tu inscripción. Nos vemos en el siguiente nivel.
            </div>

            <form id="formUnete" class="row g-3" novalidate>

              <div class="col-md-6">
                <div class="form-floating">
                  <input type="text" class="form-control" id="nombre" placeholder="Tu nombre" required minlength="2" autocomplete="given-name">
                  <label for="nombre">Nombre</label>
                  <div class="invalid-feedback">Escribe tu nombre (mínimo 2 letras).</div>
                </div>
              </div>

              <div class="col-md-6">
                <div class="form-floating">
                  <input type="email" class="form-control" id="correo" placeholder="tucorreo@ejemplo.com" required autocomplete="email">
                  <label for="correo">Correo electrónico</label>
                  <div class="invalid-feedback">Escribe un correo válido, por ejemplo nombre@correo.com.</div>
                </div>
              </div>

              <div class="col-md-4">
                <div class="form-floating">
                  <input type="number" class="form-control" id="edad" placeholder="16" required min="13" max="18">
                  <label for="edad">Edad</label>
                  <div class="invalid-feedback">El club es para edades entre 13 y 18 años.</div>
                </div>
              </div>

              <div class="col-md-8">
                <div class="form-floating">
                  <select class="form-select" id="interes" required>
                    <option value="" selected disabled>Elige una opción…</option>
                    <option>Torneos</option>
                    <option>Noches de juego</option>
                    <option>Taller de creación</option>
                    <option>Todo lo anterior</option>
                  </select>
                  <label for="interes">¿Qué te interesa más?</label>
                  <div class="invalid-feedback">Elige una opción de la lista.</div>
                </div>
              </div>

              <div class="col-12">
                <div class="input-group">
                  <span class="input-group-text" id="arroba">@</span>
                  <input type="text" class="form-control" id="usuario" placeholder="Tu usuario gamer (opcional)" aria-label="Tu usuario gamer (opcional)" aria-describedby="arroba">
                </div>
              </div>

              <div class="col-12">
                <label for="mensaje" class="form-label">Cuéntanos tu juego favorito (opcional)</label>
                <textarea class="form-control" id="mensaje" rows="3"></textarea>
              </div>

              <div class="col-12">
                <div class="form-check">
                  <input class="form-check-input" type="checkbox" id="reglas" required>
                  <label class="form-check-label" for="reglas">
                    Acepto las
                    <button type="button" class="btn btn-link p-0 align-baseline" data-bs-toggle="modal" data-bs-target="#modalReglas">reglas del club</button>
                  </label>
                  <div class="invalid-feedback">Debes aceptar las reglas para inscribirte.</div>
                </div>
              </div>

              <div class="col-12 d-grid">
                <button class="btn btn-primary btn-lg" type="submit">Enviar inscripción</button>
              </div>

            </form>
          </div>
        </div>
      </div>
    </section>
  </main>

  <!-- FOOTER -->
  <footer class="border-top py-4 mt-5">
    <div class="container d-flex flex-column flex-md-row justify-content-between align-items-center gap-3">
      <p class="mb-0 text-body-secondary small">© 2026 PixelCrew · Hecho con Bootstrap 5.3.8</p>

      <ul class="nav">
        <li class="nav-item"><a class="nav-link px-2" href="#juegos">Qué hacemos</a></li>
        <li class="nav-item"><a class="nav-link px-2" href="#faq">Preguntas</a></li>
        <li class="nav-item"><a class="nav-link px-2" href="#unete">Únete</a></li>
      </ul>

      <a href="#inicio" class="btn btn-outline-secondary btn-sm">Volver arriba ↑</a>
    </div>
  </footer>

  <!-- MODAL -->
  <div class="modal fade" id="modalReglas" tabindex="-1" aria-labelledby="tituloReglas" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content">

        <div class="modal-header">
          <h2 class="modal-title fs-5" id="tituloReglas">📜 Reglas de PixelCrew</h2>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
        </div>

        <div class="modal-body">
          <ol class="mb-0">
            <li class="mb-2"><strong>Respeto ante todo.</strong> Nada de insultos ni burlas, ni dentro ni fuera del juego.</li>
            <li class="mb-2"><strong>Juego limpio.</strong> Sin trampas, hacks ni cuentas prestadas en los torneos.</li>
            <li class="mb-2"><strong>Equilibrio.</strong> Las tareas del colegio van primero; el club puede esperar.</li>
            <li class="mb-2"><strong>Cuidamos el material.</strong> Consolas, mandos y equipos son de todos.</li>
            <li class="mb-2"><strong>Contenido apto.</strong> Los juegos deben ser adecuados para la edad de todos.</li>
            <li><strong>Si algo te incomoda</strong>, avísale a una persona responsable del club. Siempre te vamos a escuchar.</li>
          </ol>
        </div>

        <div class="modal-footer">
          <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Entendido</button>
        </div>

      </div>
    </div>
  </div>

  <!-- Bootstrap 5.3.8 (JS con Popper incluido) -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

  <!-- Tu JavaScript (después de Bootstrap) -->
  <script src="js/main.js"></script>
</body>
</html>

#css/style.css

CSS
/* ===== 1. Variables: personalizamos Bootstrap sin tocar sus archivos ===== */
:root {
  --pc-accent: #7c4dff;

  /* Variables de Bootstrap que sobrescribimos */
  --bs-primary: #7c4dff;
  --bs-primary-rgb: 124, 77, 255;
  --bs-primary-text-emphasis: #4a22b8;
  --bs-primary-bg-subtle: #e9e0ff;
  --bs-primary-border-subtle: #cbb8ff;
  --bs-border-radius: 0.75rem;
}

/* Este bloque va DESPUÉS de :root para ganar en modo oscuro */
[data-bs-theme="dark"] {
  --bs-body-bg: #0e0f1f;
  --bs-body-bg-rgb: 14, 15, 31;
  --bs-tertiary-bg: #161833;
  --bs-tertiary-bg-rgb: 22, 24, 51;
  --bs-primary-text-emphasis: #b8a0ff;
  --bs-primary-bg-subtle: #241a4d;
  --bs-primary-border-subtle: #4a2fa8;
}

/* Los componentes de Bootstrap guardan sus colores en variables LOCALES.
   Las cambiamos para que los botones también usen nuestro morado. */
.btn-primary {
  --bs-btn-bg: var(--pc-accent);
  --bs-btn-border-color: var(--pc-accent);
  --bs-btn-hover-bg: #6a3de8;
  --bs-btn-hover-border-color: #6a3de8;
  --bs-btn-active-bg: #5a30d0;
  --bs-btn-active-border-color: #5a30d0;
  --bs-btn-disabled-bg: var(--pc-accent);
  --bs-btn-disabled-border-color: var(--pc-accent);
  --bs-btn-focus-shadow-rgb: 124, 77, 255;
}

.btn-outline-primary {
  --bs-btn-color: var(--bs-primary-text-emphasis);
  --bs-btn-border-color: var(--pc-accent);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--pc-accent);
  --bs-btn-hover-border-color: var(--pc-accent);
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #5a30d0;
  --bs-btn-active-border-color: #5a30d0;
  --bs-btn-focus-shadow-rgb: 124, 77, 255;
}

.card {
  --bs-card-bg: var(--bs-tertiary-bg);
}

/* ===== 2. Ajustes generales ===== */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* Evita que la navbar fija tape los títulos al navegar por el menú */
section[id] { scroll-margin-top: 4.5rem; }

/* Un texto muy largo (un correo, una URL) puede empujar la página y crear
   scroll horizontal en celular. `overflow-wrap: anywhere` lo parte.
   Ojo: `anywhere`, no `break-word`, porque es el único que además reduce el
   ancho mínimo de las celdas de un grid. */
body {
  overflow-wrap: anywhere;
}

/* ===== 3. Hero ===== */
.hero {
  background:
    radial-gradient(circle at 15% 20%, rgba(124, 77, 255, 0.25), transparent 45%),
    radial-gradient(circle at 85% 80%, rgba(13, 139, 217, 0.18), transparent 45%);
}

.text-gradient {
  background: linear-gradient(90deg, #7c4dff, #0d8bd9);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero-card {
  background: linear-gradient(135deg, #7c4dff, #0d8bd9);
}

/* ===== 4. Tarjetas ===== */
.card-hover {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.card-hover:hover {
  transform: translateY(-6px);
  box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.25) !important;
}

@media (prefers-reduced-motion: reduce) {
  .card-hover { transition: none; }
  .card-hover:hover { transform: none; }
}

/* ===== 5. Carrusel de la comunidad ===== */
.testimonio {
  min-height: 280px;
  background: linear-gradient(135deg, #4b2bb5, #0b6fa8);
}

/* En modo oscuro Bootstrap pone controles negros en los carruseles.
   Como nuestras diapositivas son oscuras en ambos modos, los forzamos a blanco. */
.carousel {
  --bs-carousel-indicator-active-bg: #fff;
  --bs-carousel-caption-color: #fff;
  --bs-carousel-control-icon-filter: none;
}

#js/main.js

JavaScript
// ===== Validación del formulario de inscripción =====
const formulario = document.getElementById('formUnete');
const mensajeExito = document.getElementById('exito');

formulario.addEventListener('submit', (evento) => {
  evento.preventDefault(); // este sitio de práctica no envía datos a ningún servidor

  if (formulario.checkValidity()) {
    formulario.reset();
    formulario.classList.remove('was-validated');
    mensajeExito.classList.remove('d-none');
    mensajeExito.focus();               // el foco va al mensaje: quien navega con
                                         // teclado o lector de pantalla se entera
  } else {
    mensajeExito.classList.add('d-none');
    formulario.classList.add('was-validated');

    // Nadie se da cuenta de un error si no lo ve. Movemos el foco al primer
    // campo inválido: el navegador lo resalta y además lo deja preparado
    // para escribir de nuevo.
    const primerError = formulario.querySelector(':invalid');
    if (primerError) primerError.focus();
  }
});

// ===== Modo claro / oscuro =====
const raiz = document.documentElement;
const botonTema = document.getElementById('themeToggle');
const iconoTema = document.getElementById('themeIcon');
const CLAVE = 'pc-tema'; // el nombre con el que guardamos la preferencia

// El tema que ya tiene el dispositivo. Se consulta, no se supone.
const consultaSistema = window.matchMedia('(prefers-color-scheme: dark)');

// Leer y guardar puede fallar en modo privado: por eso van protegidos.
function leerTemaGuardado() {
  try {
    return localStorage.getItem(CLAVE);
  } catch (error) {
    return null;
  }
}

function aplicarTema(tema, recordar) {
  raiz.setAttribute('data-bs-theme', tema);
  iconoTema.textContent = tema === 'dark' ? '☀️' : '🌙'; // el ícono muestra a qué modo cambiarás

  // Solo guardamos cuando la ELECCIÓN es de la persona. Si guardáramos el
  // tema del sistema, después no sabríamos si ella eligió o si fue el default.
  if (recordar) {
    try {
      localStorage.setItem(CLAVE, tema);
    } catch (error) {
      /* en modo privado no se guarda, pero el sitio funciona igual */
    }
  }
}

// Al cargar: primero la elección de la persona; si no hay, la del sistema.
aplicarTema(leerTemaGuardado() || (consultaSistema.matches ? 'dark' : 'light'), false);

botonTema.addEventListener('click', () => {
  const actual = raiz.getAttribute('data-bs-theme');
  aplicarTema(actual === 'dark' ? 'light' : 'dark', true);
});

// Si la persona NO ha elegido nada y cambia el tema del sistema, la seguimos.
consultaSistema.addEventListener('change', (evento) => {
  if (!leerTemaGuardado()) {
    aplicarTema(evento.matches ? 'dark' : 'light', false);
  }
});

#Hoja de referencia rápida

#Grid y contenedores

Clase Efecto
container, container-fluid, container-lg Contenedor centrado / a todo el ancho / fluido hasta lg
row Fila
col, col-6, col-md-4, col-lg-auto Columna: igual reparto / 6 de 12 / 4 desde md / según contenido
row-cols-1, row-cols-md-3 Cuántas columnas por fila
g-3, gx-2, gy-4 Separación entre columnas
offset-md-2 Deja espacio vacío a la izquierda

#Espaciado

Clase Efecto
m-*, p-* Margen / relleno (0–5, auto)
mt-*, mb-*, ms-*, me-* Arriba, abajo, inicio, final
mx-*, my-*, px-*, py-* Horizontal / vertical
gap-* Separación entre elementos en flex o grid

#Texto y color

Clase Efecto
display-1…display-6, h1…h6, lead, small Tamaños de texto
fs-1…fs-6 Tamaño de fuente
fw-bold, fw-semibold, fw-normal, fst-italic Peso y estilo
text-start, text-center, text-end Alineación
text-uppercase, text-truncate Transformación del texto
text-primary, text-body-secondary, text-white Color de texto
bg-primary, bg-body-tertiary, bg-success-subtle Color de fondo
text-bg-primary Fondo y texto con buen contraste a la vez

#Display y flexbox

Clase Efecto
d-none, d-block, d-flex, d-lg-none Mostrar y ocultar (con breakpoints)
flex-row, flex-column, flex-wrap Dirección y salto de línea
justify-content-center, -between, -end Alineación en el eje principal
align-items-center, -start, -end Alineación en el eje cruzado
ms-auto, me-auto Empuja hacia un lado

#Bordes, sombras y tamaños

Clase Efecto
border, border-top, border-0 Bordes
rounded, rounded-3, rounded-pill, rounded-circle Esquinas
shadow-sm, shadow, shadow-lg Sombras
w-100, h-100, mw-100, min-vh-100 Anchos y altos
ratio ratio-16x9 Proporción fija
position-relative, sticky-top, fixed-bottom Posición
visually-hidden, visually-hidden-focusable Oculto visualmente pero accesible

#Componentes

Componente Estructura mínima
Botón btn btn-primary, btn-outline-*, btn-lg, btn-sm
Badge badge text-bg-primary
Alerta alert alert-success (+ alert-dismissible)
Tarjeta card > card-body > card-title, card-text
Navbar navbar navbar-expand-lg > navbar-brand, navbar-toggler, navbar-collapse
Formulario form-control, form-select, form-check, form-floating, invalid-feedback
Lista de grupo list-group > list-group-item
Tabla table table-striped dentro de table-responsive
Barra de progreso progress > progress-bar
Modal modal > modal-dialog > modal-content > modal-header/body/footer
Acordeón accordion > accordion-item > accordion-header, accordion-collapse
Carrusel carousel > carousel-inner > carousel-item
Pestañas nav nav-tabs + tab-content > tab-pane

#Atributos de JavaScript

Atributo Uso
data-bs-toggle collapse, modal, tab, dropdown, tooltip, offcanvas
data-bs-target Selector (#id) del elemento afectado
data-bs-dismiss modal, alert, toast, offcanvas
data-bs-slide, data-bs-slide-to Carrusel
data-bs-parent Acordeón de uno a la vez
data-bs-theme light o dark en cualquier elemento

#De Bootstrap 4 a Bootstrap 5

Bootstrap 4 Bootstrap 5
ml-*, mr-*, pl-*, pr-* ms-*, me-*, ps-*, pe-*
text-left, text-right text-start, text-end
float-left, float-right float-start, float-end
font-weight-bold fw-bold
data-toggle, data-target, data-dismiss data-bs-toggle, data-bs-target, data-bs-dismiss
form-group, form-row mb-3, row g-3
badge badge-primary badge text-bg-primary
btn-block d-grid en el contenedor, o w-100
sr-only visually-hidden
text-muted text-body-secondary
jQuery No es necesario: Bootstrap 5 funciona con JavaScript puro

#Retos adicionales y recursos oficiales

#Retos adicionales

Cuando termines, prueba cualquiera de estos (están de menor a mayor dificultad):

  1. Tooltips: agrega un tooltip a los emojis de la galería. (Necesitas inicializarlos con JavaScript: busca "Tooltips" en la documentación.)
  2. Dropdown: convierte "Qué hacemos" en la navbar en un menú desplegable con los tres tipos de actividad.
  3. Toast: al enviar el formulario con éxito, muestra un toast en una esquina en vez de la alerta verde.
  4. Offcanvas: crea un panel lateral con las reglas del club en vez del modal.
  5. Scrollspy: resalta en la navbar la sección que estás viendo. (Busca "Scrollspy".)
  6. Un tercer tema: en lugar de solo claro y oscuro, crea una clase .tema-retro con tus propias variables y alterna entre tres modos.
  7. Sass real: monta el proyecto con npm y Sass, con una paleta propia de 3 colores.
  8. Publica tu sitio: súbelo a GitHub Pages y compártelo con tus amistades.

#Recursos oficiales

Recurso Para qué
Documentación de Bootstrap 5.3 La referencia definitiva, con ejemplos de cada componente
Ejemplos oficiales Plantillas completas para inspirarte
Bootstrap Icons Íconos gratuitos para tus proyectos
MDN Web Docs (español) HTML, CSS y JavaScript explicados con detalle
WCAG: guía rápida de accesibilidad Criterios de accesibilidad web
WebAIM Contrast Checker Comprueba el contraste de colores
Validador HTML del W3C Encuentra errores en tu HTML

#Lo que lograste

Empezaste con una página vacía y hoy sabes: organizar contenido con el grid, dar estilo con utilidades, armar componentes, validar formularios, usar el JavaScript de Bootstrap, personalizar colores y temas, y revisar la accesibilidad de un sitio. Eso es una base real de desarrollo front-end.

La mejor forma de consolidarlo es construir algo que te importe. Empieza hoy con el Reto 2. 🚀