Files
roi-theme/_planeacion/GAP-CODE-SNIPPETS.md
FrankZamora ea38a12055 [NIVEL 2 AVANCE] Issues #49-#53 - Componentes Principales Verificados
Todos los componentes del NIVEL 2 ya están implementados correctamente:
-  Notification Bar (#49)
-  Navbar (#50)
-  Hero Section (#51)
-  Sidebar (#52)
-  Footer (#53)

Solo se actualizó notification-bar.css para usar variables CSS.

Próximo paso: NIVEL 3 (Refinamientos visuales)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 20:01:07 -06:00

23 KiB

GAP Implementation - Code Snippets Ready to Use

Quick Copy-Paste Reference Fecha: 2025-11-04


Issue #41: TOC Sticky con ScrollSpy

Archivo: assets/css/toc.css

Agregar al final del archivo:

/* ==========================================================================
   TOC STICKY + SCROLLSPY (Issue #41)
   ========================================================================== */

/* Contenedor Sticky */
.apus-toc {
    position: sticky;
    top: 5.5rem; /* Offset debajo del navbar */
    max-height: calc(100vh - 6rem);
    overflow-y: auto;
    z-index: 100;
}

/* Scrollbar Personalizado - Chrome/Safari/Edge */
.apus-toc::-webkit-scrollbar {
    width: 6px;
}

.apus-toc::-webkit-scrollbar-track {
    background: #f1f3f4;
    border-radius: 3px;
}

.apus-toc::-webkit-scrollbar-thumb {
    background: #cbd5e0;
    border-radius: 3px;
    transition: background 0.3s ease;
}

.apus-toc::-webkit-scrollbar-thumb:hover {
    background: #a0aec0;
}

/* Scrollbar Personalizado - Firefox */
.apus-toc {
    scrollbar-width: thin;
    scrollbar-color: #cbd5e0 #f1f3f4;
}

/* Active Link Highlighting */
.apus-toc-link.active {
    color: #1a73e8;
    font-weight: 700;
    background: rgba(26, 115, 232, 0.1);
    border-left: 3px solid #1a73e8;
    padding-left: calc(0.5rem - 3px); /* Compensar border */
}

/* Smooth Transition para Active State */
.apus-toc-link {
    transition: all 0.3s ease;
    padding-left: 0.5rem;
}

/* Mobile: Reducir max-height */
@media (max-width: 768px) {
    .apus-toc {
        position: relative; /* No sticky en mobile */
        max-height: 400px;
        top: auto;
    }
}

Archivo: assets/js/toc.js

Agregar al final del archivo, antes del cierre de función:

/**
 * ScrollSpy para TOC
 * Resalta automáticamente el heading activo basado en scroll position
 * Issue #41
 */
function initTOCScrollSpy() {
    const tocLinks = document.querySelectorAll('.apus-toc-link');

    if (tocLinks.length === 0) {
        return; // No hay TOC en esta página
    }

    // Configurar IntersectionObserver
    const observerOptions = {
        rootMargin: '-20% 0px -35% 0px', // Activa cuando el heading está en el 20%-65% superior de la pantalla
        threshold: 0
    };

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            const id = entry.target.getAttribute('id');
            const tocLink = document.querySelector(`.apus-toc-link[href="#${id}"]`);

            if (entry.isIntersecting && tocLink) {
                // Remover active de todos los links
                tocLinks.forEach(link => link.classList.remove('active'));

                // Agregar active al link actual
                tocLink.classList.add('active');

                // Scroll suave del TOC para mantener link visible
                tocLink.scrollIntoView({
                    behavior: 'smooth',
                    block: 'nearest',
                    inline: 'nearest'
                });
            }
        });
    }, observerOptions);

    // Observar todos los headings que tienen ID
    tocLinks.forEach(link => {
        const href = link.getAttribute('href');
        if (href && href.startsWith('#')) {
            const heading = document.querySelector(href);
            if (heading) {
                observer.observe(heading);
            }
        }
    });

    // Cleanup al salir de la página
    window.addEventListener('beforeunload', () => {
        observer.disconnect();
    });
}

// Inicializar cuando el DOM esté listo
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initTOCScrollSpy);
} else {
    initTOCScrollSpy();
}

Issue #42: Navbar Underline Hover Animado

Archivo: assets/css/header.css

Encontrar la sección de .navbar-nav .nav-link y agregar:

/* ==========================================================================
   NAVBAR UNDERLINE HOVER ANIMADO (Issue #42)
   ========================================================================== */

/* Desktop Navigation - Underline Effect */
.navbar-nav .nav-link {
    position: relative;
    padding-bottom: 0.75rem; /* Espacio para underline */
}

/* Underline Pseudo-elemento */
.navbar-nav .nav-link::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    width: 0;
    height: 2px;
    background-color: #61c7cd; /* Turquesa RDash */
    transition: width 0.3s ease;
}

/* Hover State */
.navbar-nav .nav-link:hover {
    color: #61c7cd;
    background-color: rgba(97, 199, 205, 0.1);
}

.navbar-nav .nav-link:hover::after {
    width: 100%;
}

/* Active/Current Menu Item */
.navbar-nav .nav-link.active,
.navbar-nav .current-menu-item > .nav-link,
.navbar-nav .current_page_item > .nav-link {
    color: #61c7cd;
}

.navbar-nav .nav-link.active::after,
.navbar-nav .current-menu-item > .nav-link::after,
.navbar-nav .current_page_item > .nav-link::after {
    width: 100%;
}

/* Focus State (Accesibilidad) */
.navbar-nav .nav-link:focus {
    color: #61c7cd;
    background-color: rgba(97, 199, 205, 0.1);
    outline: 2px solid #61c7cd;
    outline-offset: 2px;
}

.navbar-nav .nav-link:focus::after {
    width: 100%;
}

/* Responsive: Ocultar underline en mobile menu */
@media (max-width: 767px) {
    .navbar-nav .nav-link::after {
        display: none;
    }
}

Issue #43: Verificar Colores Notification Bar

Archivo: assets/css/notification-bar.css

Reemplazar colores actuales con estos exactos:

/* ==========================================================================
   NOTIFICATION BAR - COLORES EXACTOS RDASH (Issue #43)
   ========================================================================== */

.top-notification-bar {
    background-color: #4C5C6B; /* Gris RDash exacto */
    color: #ffffff;
    padding: 0.75rem 1rem;
    font-size: 0.875rem;
    text-align: center;
    position: relative;
    z-index: 1001;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.top-notification-bar p {
    margin: 0;
    line-height: 1.5;
}

.top-notification-bar a {
    color: #61c7cd; /* Turquesa hover RDash */
    text-decoration: underline;
    font-weight: 600;
    transition: color 0.3s ease;
}

.top-notification-bar a:hover {
    color: #4fb3b9; /* Turquesa hover oscuro */
    text-decoration: none;
}

.top-notification-bar a:focus {
    outline: 2px solid #61c7cd;
    outline-offset: 2px;
}

.notification-close {
    position: absolute;
    right: 1rem;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
    border: none;
    color: rgba(255, 255, 255, 0.8);
    font-size: 1.25rem;
    cursor: pointer;
    padding: 0.25rem 0.5rem;
    transition: color 0.3s ease;
    line-height: 1;
}

.notification-close:hover {
    color: #ffffff;
}

.notification-close:focus {
    outline: 2px solid #61c7cd;
    outline-offset: 2px;
}

Issue #44: Botón "Let's Talk" en Header

Archivo: header.php

Agregar en el navbar, después de los nav items (buscar </ul> del menu):

<!-- Botón "Let's Talk" CTA (Issue #44) -->
<?php if ( has_nav_menu( 'primary' ) ) : ?>
    <div class="ms-auto d-none d-lg-block">
        <a href="<?php echo esc_url( get_permalink( get_page_by_path( 'contacto' ) ) ?: '#contacto' ); ?>"
           class="btn btn-lets-talk"
           aria-label="<?php esc_attr_e( 'Contáctanos', 'apus-theme' ); ?>">
            <?php esc_html_e( "Let's Talk", 'apus-theme' ); ?>
        </a>
    </div>
<?php endif; ?>

Archivo: assets/css/header.css

Agregar al final:

/* ==========================================================================
   BOTÓN "LET'S TALK" CTA (Issue #44)
   ========================================================================== */

.btn-lets-talk {
    background: linear-gradient(135deg, #FF6B35 0%, #FF8C42 100%);
    color: #ffffff !important;
    padding: 0.875rem 1.75rem;
    border-radius: 8px;
    font-weight: 600;
    font-size: 1rem;
    border: none;
    box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
    transition: all 0.3s ease;
    text-decoration: none !important;
    display: inline-block;
    white-space: nowrap;
}

.btn-lets-talk:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4);
    background: linear-gradient(135deg, #FF5722 0%, #FF7043 100%);
    color: #ffffff !important;
    text-decoration: none !important;
}

.btn-lets-talk:active {
    transform: translateY(0);
    box-shadow: 0 2px 8px rgba(255, 107, 53, 0.3);
}

.btn-lets-talk:focus {
    outline: 2px solid #FF8C42;
    outline-offset: 3px;
    box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
}

/* Responsive: Ocultar en tablets y móviles */
@media (max-width: 991px) {
    .btn-lets-talk {
        display: none;
    }
}

/* Opcional: Versión mobile en el menú hamburguesa */
.mobile-menu .btn-lets-talk-mobile {
    display: block;
    width: 100%;
    margin: 1rem 1.5rem;
    width: calc(100% - 3rem);
    text-align: center;
}

Archivo: assets/css/related-posts.css

Reemplazar la sección .related-post-card con:

/* ==========================================================================
   RELATED POST CARD - DISEÑO GRIS CON BORDE LATERAL (Issue #45)
   ========================================================================== */

.related-post-card {
    display: flex;
    flex-direction: column;
    height: 100%;
    background: #f7fafc; /* Gris claro de fondo */
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    position: relative;
    border: 1px solid #e2e8f0;
}

/* Borde lateral izquierdo con gradiente */
.related-post-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 4px;
    height: 100%;
    background: linear-gradient(180deg, #1e3a5f 0%, #1a73e8 100%);
    opacity: 0;
    transition: opacity 0.3s ease;
    z-index: 10;
}

/* Hover State */
.related-post-card:hover {
    background: #ffffff; /* Cambiar a blanco en hover */
    border-color: #1a73e8;
    transform: translateY(-8px); /* Más pronunciado */
    box-shadow: 0 12px 32px rgba(26, 115, 232, 0.15);
}

.related-post-card:hover::before {
    opacity: 1; /* Mostrar borde lateral */
}

/* Ajustar padding del contenido para el borde */
.related-post-content {
    padding: 1.25rem;
    padding-left: calc(1.25rem + 4px); /* Compensar borde */
    flex: 1;
    display: flex;
    flex-direction: column;
}

/* Título con transición de color */
.related-post-title {
    font-size: 1.125rem;
    font-weight: 600;
    color: #212529;
    margin: 0 0 0.75rem 0;
    line-height: 1.4;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    transition: color 0.3s ease;
}

.related-post-card:hover .related-post-title {
    color: #1a73e8;
}

/* Imagen con zoom sutil */
.related-post-thumbnail img {
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.related-post-card:hover .related-post-thumbnail img {
    transform: scale(1.08); /* Zoom más pronunciado */
}

/* Responsive */
@media (max-width: 767.98px) {
    .related-post-card:hover {
        transform: translateY(-4px); /* Menos pronunciado en mobile */
    }
}

/* Reducir movimiento para usuarios que lo prefieren */
@media (prefers-reduced-motion: reduce) {
    .related-post-card,
    .related-post-card::before,
    .related-post-thumbnail img {
        transition: none;
    }

    .related-post-card:hover {
        transform: none;
    }

    .related-post-card:hover .related-post-thumbnail img {
        transform: none;
    }
}

Issue #46: Paginación Profesional

Archivo: style.css

Reemplazar la sección de paginación (líneas 437-479) con:

/* ==========================================================================
   PAGINACIÓN PROFESIONAL (Issue #46)
   ========================================================================== */

.pagination,
.posts-pagination {
    margin-top: var(--spacing-xxl);
    margin-bottom: var(--spacing-xxl);
}

.nav-links {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.nav-links .page-numbers {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 0.5rem 0.75rem;
    background: #ffffff;
    border: 2px solid #e2e8f0; /* Borde más grueso */
    border-radius: 8px; /* Más redondeado */
    color: #4a5568;
    font-weight: 600;
    text-decoration: none;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* Curva de animación mejorada */
}

/* Hover State */
.nav-links .page-numbers:hover {
    background: #1a73e8;
    border-color: #1a73e8;
    color: #ffffff;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(26, 115, 232, 0.3);
    text-decoration: none;
}

/* Current/Active Page */
.nav-links .page-numbers.current {
    background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
    border-color: transparent;
    color: #ffffff;
    box-shadow: 0 4px 12px rgba(30, 58, 95, 0.3);
    cursor: default;
}

/* Dots (ellipsis) */
.nav-links .page-numbers.dots {
    border: none;
    pointer-events: none;
    color: #cbd5e0;
    cursor: default;
}

/* Prev/Next Arrows */
.nav-links .page-numbers.prev,
.nav-links .page-numbers.next {
    font-weight: 700;
}

/* Focus State (Accesibilidad) */
.nav-links .page-numbers:focus {
    outline: 2px solid #1a73e8;
    outline-offset: 2px;
    box-shadow: 0 0 0 0.2rem rgba(26, 115, 232, 0.25);
}

/* Active State */
.nav-links .page-numbers:active {
    transform: translateY(0);
    box-shadow: 0 2px 4px rgba(26, 115, 232, 0.2);
}

/* Reducir movimiento para usuarios que lo prefieren */
@media (prefers-reduced-motion: reduce) {
    .nav-links .page-numbers {
        transition: color 0.3s ease, background-color 0.3s ease;
    }

    .nav-links .page-numbers:hover {
        transform: none;
    }
}

/* Responsive */
@media (max-width: 575px) {
    .nav-links .page-numbers {
        min-width: 40px;
        min-height: 40px;
        padding: 0.375rem 0.625rem;
        font-size: 0.875rem;
    }
}

Issue #47: Modal Contacto Refinamiento

Archivo: assets/css/modal-contact.css

Agregar/Reemplazar estilos del modal:

/* ==========================================================================
   MODAL CONTACTO REFINADO (Issue #47)
   ========================================================================== */

.contact-modal .modal-content {
    border-radius: 16px; /* Más redondeado */
    border: none;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); /* Sombra más profunda */
    overflow: hidden;
}

.contact-modal .modal-header {
    background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
    color: #ffffff;
    border-radius: 16px 16px 0 0;
    padding: 1.5rem 2rem;
    border-bottom: none;
}

.contact-modal .modal-title {
    font-size: 1.5rem;
    font-weight: 700;
    color: #ffffff;
}

/* Botón Close Blanco */
.contact-modal .btn-close {
    filter: brightness(0) invert(1); /* Convertir a blanco */
    opacity: 1;
    transition: opacity 0.3s ease;
}

.contact-modal .btn-close:hover {
    opacity: 0.8;
}

.contact-modal .btn-close:focus {
    outline: 2px solid #ffffff;
    outline-offset: 2px;
    box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.25);
}

/* Body del Modal */
.contact-modal .modal-body {
    padding: 2rem;
}

/* Form Controls */
.contact-modal .form-control,
.contact-modal .form-select {
    border-radius: 8px;
    border: 2px solid #e2e8f0;
    padding: 0.75rem 1rem;
    transition: all 0.3s ease;
}

.contact-modal .form-control:focus,
.contact-modal .form-select:focus {
    border-color: #1a73e8;
    box-shadow: 0 0 0 0.25rem rgba(26, 115, 232, 0.15);
}

/* Labels */
.contact-modal .form-label {
    font-weight: 600;
    color: #2d3748;
    margin-bottom: 0.5rem;
}

/* Botón Submit */
.contact-modal .btn-primary {
    background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
    border: none;
    padding: 0.875rem 2rem;
    font-weight: 600;
    border-radius: 8px;
    transition: all 0.3s ease;
    width: 100%;
}

.contact-modal .btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 20px rgba(30, 58, 95, 0.3);
    background: linear-gradient(135deg, #152e4a 0%, #1e3a5f 100%);
}

.contact-modal .btn-primary:active {
    transform: translateY(0);
}

.contact-modal .btn-primary:focus {
    box-shadow: 0 0 0 0.25rem rgba(30, 58, 95, 0.25);
}

/* Footer del Modal */
.contact-modal .modal-footer {
    padding: 1.5rem 2rem;
    border-top: 1px solid #e2e8f0;
}

/* Responsive */
@media (max-width: 575px) {
    .contact-modal .modal-body {
        padding: 1.5rem;
    }

    .contact-modal .modal-header {
        padding: 1.25rem 1.5rem;
    }

    .contact-modal .modal-title {
        font-size: 1.25rem;
    }
}

Issue #48: Animación Pulse CTA Box (Opcional)

Archivo: assets/css/cta-box-sidebar.css

Agregar al final:

/* ==========================================================================
   ANIMACIÓN PULSE (Issue #48)
   ========================================================================== */

/* Keyframes de la animación */
@keyframes pulse {
    0%, 100% {
        box-shadow: 0 4px 12px rgba(255, 134, 0, 0.3);
        transform: scale(1);
    }
    50% {
        box-shadow: 0 6px 20px rgba(255, 134, 0, 0.5);
        transform: scale(1.02);
    }
}

/* Aplicar animación al CTA box */
.cta-box-sidebar {
    animation: pulse 3s ease-in-out infinite;
    transform-origin: center;
}

/* Detener animación al hacer hover (para no interferir con interacción) */
.cta-box-sidebar:hover {
    animation: none;
}

/* Respetar preferencia de movimiento reducido */
@media (prefers-reduced-motion: reduce) {
    .cta-box-sidebar {
        animation: none;
    }
}

/* Pausar animación cuando el tab no está activo (performance) */
@media (prefers-reduced-motion: no-preference) {
    .cta-box-sidebar {
        animation-play-state: running;
    }
}

/* Pausar si el usuario está inactivo (opcional - requiere JS) */
body.user-inactive .cta-box-sidebar {
    animation-play-state: paused;
}

Issue #49: Hero Section Padding

Archivo: assets/css/hero-section.css

Agregar/Modificar:

/* ==========================================================================
   HERO SECTION - PADDING EXACTO (Issue #49)
   ========================================================================== */

.hero-section {
    background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
    color: #ffffff;
    padding: 3rem 1rem; /* Padding exacto del template */
    text-align: center;
    margin-bottom: 2rem;
}

.hero-content {
    max-width: 900px;
    margin: 0 auto;
}

/* Responsive: Reducir padding en mobile */
@media (max-width: 767px) {
    .hero-section {
        padding: 2rem 1rem;
    }
}

@media (max-width: 575px) {
    .hero-section {
        padding: 1.5rem 0.75rem;
    }
}

Utilidades Adicionales

Variable CSS para Colores RDash

Agregar en style.css en :root:

:root {
    /* Colores RDash */
    --rdash-navy: #0E2337;
    --rdash-blue-dark: #1e3a5f;
    --rdash-blue-light: #2c5282;
    --rdash-turquoise: #61c7cd;
    --rdash-turquoise-dark: #4fb3b9;
    --rdash-orange-start: #FF6B35;
    --rdash-orange-end: #FF8C42;
    --rdash-orange-sidebar-start: #FF8600;
    --rdash-orange-sidebar-end: #FFB800;
    --rdash-gray-notification: #4C5C6B;
    --rdash-gray-card: #f7fafc;
    --rdash-gray-border: #e2e8f0;
}

Mixins SCSS (si usas Sass)

Crear archivo _rdash-mixins.scss:

// Gradiente Hero
@mixin gradient-hero {
    background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
}

// Gradiente Botón "Let's Talk"
@mixin gradient-cta-button {
    background: linear-gradient(135deg, #FF6B35 0%, #FF8C42 100%);
}

// Gradiente CTA Box Sidebar
@mixin gradient-cta-box {
    background: linear-gradient(135deg, #FF8600 0%, #FFB800 100%);
}

// Hover Effect con Transform
@mixin hover-lift($distance: -2px, $shadow-color: rgba(0, 0, 0, 0.15)) {
    transition: all 0.3s ease;

    &:hover {
        transform: translateY($distance);
        box-shadow: 0 6px 20px $shadow-color;
    }
}

// Scrollbar Personalizado
@mixin custom-scrollbar($width: 6px, $thumb-color: #cbd5e0, $track-color: #f1f3f4) {
    &::-webkit-scrollbar {
        width: $width;
    }

    &::-webkit-scrollbar-track {
        background: $track-color;
        border-radius: 3px;
    }

    &::-webkit-scrollbar-thumb {
        background: $thumb-color;
        border-radius: 3px;

        &:hover {
            background: darken($thumb-color, 15%);
        }
    }

    scrollbar-width: thin;
    scrollbar-color: $thumb-color $track-color;
}

Testing Snippets

JavaScript para Verificar ScrollSpy

Agregar temporalmente para debug:

// Debug ScrollSpy
document.querySelectorAll('.apus-toc-link').forEach(link => {
    link.addEventListener('click', (e) => {
        console.log('TOC Link clicked:', link.textContent);
    });
});

// Verificar que IntersectionObserver está funcionando
const headings = document.querySelectorAll('h2[id], h3[id]');
console.log(`Observing ${headings.length} headings for ScrollSpy`);

CSS para Debug Visual

Agregar temporalmente para verificar posiciones:

/* DEBUG: Visualizar áreas sticky */
.apus-toc {
    outline: 2px dashed red !important;
}

/* DEBUG: Visualizar ::after elements */
.navbar-nav .nav-link::after {
    background-color: lime !important;
}

/* DEBUG: Visualizar ::before en related posts */
.related-post-card::before {
    opacity: 1 !important;
    background: red !important;
}

Comandos Git Útiles

Crear Branch para Issue

# Issue #41
git checkout -b issue-41-toc-sticky-scrollspy
git add assets/css/toc.css assets/js/toc.js
git commit -m "Issue #41: Implementar TOC sticky con scrollspy

- Agregar position sticky con top offset
- Implementar IntersectionObserver para active link
- Scrollbar personalizado webkit y firefox
- Respetar prefers-reduced-motion"
git push origin issue-41-toc-sticky-scrollspy

# Issue #42
git checkout -b issue-42-navbar-underline
git add assets/css/header.css
git commit -m "Issue #42: Agregar underline animado en navbar

- ::after pseudo-elemento con width transition
- Hover color turquesa #61c7cd
- Background color hover rgba
- Estados active y current"
git push origin issue-42-navbar-underline

Crear PR desde CLI

gh pr create --title "Issue #41: TOC Sticky con ScrollSpy" --body "Implementa TOC sticky con IntersectionObserver para scrollspy automático.

Cambios:
- Position sticky con top 5.5rem
- IntersectionObserver con rootMargin optimizado
- Scrollbar personalizado
- Active link highlighting
- Responsive: static en mobile

Testing:
- ✅ Chrome, Firefox, Safari, Edge
- ✅ Mobile responsive
- ✅ Keyboard navigation
- ✅ Reduced motion support"

Última actualización: 2025-11-04 Issues cubiertos: #41-#49 Tiempo total estimado: 3.85 días