Simples SPA com javascript e bootstrap 5

Tutorial: SPA Moderna com Vanilla JS e ES Modules

Estrutura do Projeto

Conceitos Aplicados

  1. ES Modules nativos — cada arquivo usa export/import; o HTML carrega apenas <script type="module" src="app.js"> e o browser resolve a árvore de dependências.

  2. Roteamento por hash — a URL #home#about#contact controla qual página é exibida. O app.js ouve o evento hashchange e renderiza o template correspondente.

  3. Separação de responsabilidades — cada página é uma função que retorna HTML (template literal). Lógica específica (ex: bindContactForm) fica junto da página que a utiliza.

  4. CSS externo com custom properties — variáveis como --spa-primary e --spa-gradient centralizam cores e facilitam customização.

  5. Dark mode nativo — usa data-bs-theme do Bootstrap 5.3, com persistência via localStorage e detecção da preferência do sistema (prefers-color-scheme).

  6. Transições de página — classe .page-exit aplica fade+slide antes de trocar o conteúdo; animações .fade-up escalonadas nos elementos internos.

  7. Zero dependências de build — funciona direto no browser, sem bundler, transpiler ou framework. Apenas Bootstrap via C

app.js

import { homePage } from './pages/home.js';
import { aboutPage } from './pages/about.js';
import { contactPage, bindContactForm } from './pages/contact.js';
import { notFoundPage } from './pages/404.js';
import { showToast } from './utils/toast.js';

// ??? Route Registry ?????????????????????????????????????????
const routes = {
 home: homePage,
 about: aboutPage,
 contact: contactPage,
 404: notFoundPage
};

// Hooks executados após renderizar cada página
const afterRender = {
 contact: bindContactForm
};

// ??? Router ??????????????????????????????????????????????????
const contentDiv = document.getElementById('app-content');
let currentPage = null;

function navigateTo(page) {
 if (page === currentPage) return;
 currentPage = page;

 // Animate out
 contentDiv.classList.add('page-exit');

 setTimeout(() => {
 const renderer = routes[page] || routes['404'];
 contentDiv.innerHTML = renderer();
 contentDiv.classList.remove('page-exit');
 window.scrollTo({ top: 0, behavior: 'smooth' });

 // Update active nav link
 document.querySelectorAll('.nav-link[data-page]').forEach(link => {
 link.classList.toggle('active', link.dataset.page === page);
 });

 // Run page-specific hooks
 if (afterRender[page]) afterRender[page]();

 // Bind internal SPA links inside page content
 bindInternalLinks(contentDiv);
 }, 250);
}

function bindInternalLinks(root) {
 root.querySelectorAll('a[data-page]').forEach(link => {
 link.addEventListener('click', (e) => {
 e.preventDefault();
 window.location.hash = link.dataset.page;
 });
 });
}

// ??? Theme Toggle ???????????????????????????????????????????
const themeBtn = document.getElementById('themeToggle');
const htmlEl = document.documentElement;

function applyTheme(theme) {
 htmlEl.setAttribute('data-bs-theme', theme);
 themeBtn.innerHTML = theme === 'dark'
 ? '<i class="bi bi-sun-fill"></i>'
 : '<i class="bi bi-moon-stars-fill"></i>';
 localStorage.setItem('spa-theme', theme);
}

const savedTheme = localStorage.getItem('spa-theme')
 || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
applyTheme(savedTheme);

themeBtn.addEventListener('click', () => {
 applyTheme(htmlEl.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark');
});

// ??? Hash Routing ???????????????????????????????????????????
function handleRoute() {
 const hash = window.location.hash.replace('#', '') || 'home';
 navigateTo(hash);
}

window.addEventListener('hashchange', handleRoute);

document.querySelectorAll('.nav-link[data-page]').forEach(link => {
 link.addEventListener('click', (e) => {
 e.preventDefault();
 window.location.hash = link.dataset.page;
 const navCollapse = document.getElementById('navbarNav');
 const bsCollapse = bootstrap.Collapse.getInstance(navCollapse);
 if (bsCollapse) bsCollapse.hide();
 });
});

// Initial load
handleRoute();

index.html

<!DOCTYPE html>
<html lang="pt-BR" data-bs-theme="light">

<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>SPA Moderna</title>
 <!-- Bootstrap 5.3 CSS -->
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
 <!-- Bootstrap Icons -->
 <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
 <!-- Google Fonts -->
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 <!-- App CSS -->
 <link rel="stylesheet" href="style.css">
</head>

<body>
 <!-- Navbar -->
 <nav class="navbar navbar-expand-lg sticky-top">
 <div class="container">
 <a class="navbar-brand" href="#home">
 <i class="bi bi-hexagon-fill me-1"></i> SPA
 </a>
 <div class="d-flex align-items-center gap-2 order-lg-last">
 <button class="btn-theme" id="themeToggle" aria-label="Alternar tema">
 <i class="bi bi-moon-stars-fill"></i>
 </button>
 <button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse"
 data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false"
 aria-label="Abrir menu">
 <span class="navbar-toggler-icon"></span>
 </button>
 </div>
 <div class="collapse navbar-collapse" id="navbarNav">
 <ul class="navbar-nav ms-auto gap-1">
 <li class="nav-item">
 <a class="nav-link active" href="#home" data-page="home">
 <i class="bi bi-house-door"></i> Home
 </a>
 </li>
 <li class="nav-item">
 <a class="nav-link" href="#about" data-page="about">
 <i class="bi bi-info-circle"></i> Sobre
 </a>
 </li>
 <li class="nav-item">
 <a class="nav-link" href="#contact" data-page="contact">
 <i class="bi bi-envelope"></i> Contato
 </a>
 </li>
 </ul>
 </div>
 </div>
 </nav>

 <!-- Content -->
 <main class="container py-4 py-lg-5">
 <div id="app-content"></div>
 </main>

 <!-- Footer -->
 <footer class="py-4">
 <div class="container">
 <div class="row align-items-center g-3">
 <div class="col-md-6 text-center text-md-start">
 <span class="text-secondary small">&copy; 2026 SPA Moderna. Todos os direitos reservados.</span>
 </div>
 <div class="col-md-6 text-center text-md-end">
 <a href="#" class="me-3"><i class="bi bi-github fs-5"></i></a>
 <a href="#" class="me-3"><i class="bi bi-twitter-x fs-5"></i></a>
 <a href="#"><i class="bi bi-linkedin fs-5"></i></a>
 </div>
 </div>
 </div>
 </footer>

 <!-- Toast container -->
 <div class="toast-container" id="toastContainer"></div>

 <!-- Bootstrap 5.3 Bundle -->
 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
 <!-- App (ES Module) -->
 <script type="module" src="app.js"></script>
</body>

</html>

/pages/404.js

export const notFoundPage = () => `
 <div class="text-center py-5 fade-up">
 <div class="display-1 fw-bold" style="background:var(--spa-gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">404</div>
 <h3 class="fw-bold mt-2 mb-3">Página não encontrada</h3>
 <p class="text-secondary mb-4">A página que você procura não existe ou foi movida.</p>
 <a href="#home" data-page="home" class="btn btn-primary">
 <i class="bi bi-house me-2"></i>Voltar ao início
 </a>
 </div>
`;

/pages/about.js

export const aboutPage = () => `
 <div class="row g-5 align-items-center mb-5">
 <div class="col-lg-6 fade-up">
 <span class="badge rounded-pill text-bg-primary bg-opacity-10 text-primary mb-3 px-3 py-2" 
 style="background: rgba(99,102,241,.1) !important; color: var(--spa-primary) !important">
 <i class="bi bi-stars me-1"></i> Sobre nós
 </span>
 <h2 class="fw-bold mb-3" style="letter-spacing:-0.03em">
 Criamos experiências digitais excepcionais
 </h2>
 <p class="text-secondary mb-4">
 Somos apaixonados por tecnologia e design. Nossa missão é transformar ideias em 
 aplicações web modernas, performáticas e acessíveis para todos.
 </p>
 <a href="#contact" data-page="contact" class="btn btn-primary">
 Fale conosco <i class="bi bi-arrow-right ms-1"></i>
 </a>
 </div>
 <div class="col-lg-6 fade-up" style="animation-delay:0.1s">
 <div class="p-4">
 <h5 class="fw-semibold mb-4"><i class="bi bi-clock-history me-2 text-primary"></i>Nossa Trajetória</h5>
 ${[
 { year: '2020', text: 'Início do projeto com foco em soluções web modernas.' },
 { year: '2022', text: 'Expansão da equipe e adoção de metodologias ágeis.' },
 { year: '2024', text: 'Migração para arquitetura baseada em SPA e componentização.' },
 { year: '2026', text: 'Lançamento da versão totalmente modernizada.' }
 ].map(t => `
 <div class="timeline-item">
 <h6>${t.year}</h6>
 <p class="text-secondary small mb-0">${t.text}</p>
 </div>
 `).join('')}
 </div>
 </div>
 </div>

 <h4 class="fw-bold text-center mb-4 fade-up">Nossa Equipe</h4>
 <div class="row g-4 mb-3">
 ${[
 { initials: 'AS', name: 'Ana Silva', role: 'CEO & Design', icon: 'bi-palette2' },
 { initials: 'MR', name: 'Marcos Reis', role: 'Full-stack Dev', icon: 'bi-code-slash' },
 { initials: 'JF', name: 'Julia Fontes', role: 'UX Research', icon: 'bi-person-hearts' }
 ].map((m, i) => `
 <div class="col-md-4 fade-up" style="animation-delay:${i * 0.1}s">
 <div class="team-card">
 <div class="team-avatar">${m.initials}</div>
 <h6 class="fw-semibold mb-1">${m.name}</h6>
 <span class="text-secondary small"><i class="bi ${m.icon} me-1"></i>${m.role}</span>
 </div>
 </div>
 `).join('')}
 </div>
`;

/pages/contact.js

import { showToast } from '../utils/toast.js';

export const contactPage = () => `
 <div class="row g-5">
 <div class="col-lg-5 fade-up">
 <span class="badge rounded-pill text-bg-primary bg-opacity-10 text-primary mb-3 px-3 py-2"
 style="background: rgba(99,102,241,.1) !important; color: var(--spa-primary) !important">
 <i class="bi bi-chat-dots me-1"></i> Contato
 </span>
 <h2 class="fw-bold mb-3" style="letter-spacing:-0.03em">Vamos conversar?</h2>
 <p class="text-secondary mb-4">
 Tem uma ideia, projeto ou dúvida? Preencha o formulário ou use um dos canais abaixo. 
 Respondemos em até 24 horas.
 </p>
 <div class="d-flex flex-column gap-3">
 ${[
 { icon: 'bi-envelope-at', label: 'E-mail', value: 'contato@exemplo.com' },
 { icon: 'bi-telephone', label: 'Telefone', value: '+55 (11) 99999-0000' },
 { icon: 'bi-geo-alt', label: 'Localização', value: 'São Paulo, SP — Brasil' }
 ].map(c => `
 <div class="contact-info-card">
 <div class="icon-wrapper"><i class="bi ${c.icon}"></i></div>
 <div>
 <div class="small text-secondary">${c.label}</div>
 <div class="fw-medium">${c.value}</div>
 </div>
 </div>
 `).join('')}
 </div>
 </div>
 <div class="col-lg-7 fade-up" style="animation-delay:0.1s">
 <div class="p-4 p-lg-5 rounded-4" style="border:1px solid rgba(0,0,0,0.06); background: var(--bs-body-bg)">
 <form id="contactForm" novalidate>
 <div class="row g-3">
 <div class="col-sm-6">
 <label class="form-label small fw-medium">Nome</label>
 <input type="text" class="form-control" placeholder="Seu nome" required>
 </div>
 <div class="col-sm-6">
 <label class="form-label small fw-medium">E-mail</label>
 <input type="email" class="form-control" placeholder="seu@email.com" required>
 </div>
 <div class="col-12">
 <label class="form-label small fw-medium">Assunto</label>
 <select class="form-select">
 <option selected disabled>Selecione um assunto</option>
 <option>Projeto novo</option>
 <option>Consultoria</option>
 <option>Parceria</option>
 <option>Outro</option>
 </select>
 </div>
 <div class="col-12">
 <label class="form-label small fw-medium">Mensagem</label>
 <textarea class="form-control" rows="4" placeholder="Descreva como podemos ajudar…" required></textarea>
 </div>
 <div class="col-12">
 <button type="submit" class="btn btn-primary w-100">
 <i class="bi bi-send me-2"></i>Enviar mensagem
 </button>
 </div>
 </div>
 </form>
 </div>
 </div>
 </div>
`;

export function bindContactForm() {
 const form = document.getElementById('contactForm');
 if (!form) return;

 form.addEventListener('submit', (e) => {
 e.preventDefault();
 if (!form.checkValidity()) {
 form.classList.add('was-validated');
 return;
 }
 showToast('Mensagem enviada!', 'Obrigado pelo contato. Responderemos em breve.', 'bi-check-circle-fill text-success');
 form.reset();
 form.classList.remove('was-validated');
 });
}

/pages/home.js

export const homePage = () => `
 <section class="hero text-center mb-5 fade-up">
 <h1 class="mb-3">Construa algo<br>incrível hoje</h1>
 <p class="mx-auto mb-4" style="max-width:540px">
 Uma Single Page Application moderna, rápida e elegante — feita com Bootstrap 5.3, ícones, 
 dark mode e transições suaves.
 </p>
 <a href="#about" data-page="about" class="btn btn-outline-light btn-lg">
 Saiba mais <i class="bi bi-arrow-right ms-1"></i>
 </a>
 </section>

 <div class="row g-4 mb-5">
 ${[
 { icon: 'bi-lightning-charge', title: 'Ultra Rápido', desc: 'Carregamento instantâneo sem reloads de página. Transições suaves entre seções.' },
 { icon: 'bi-palette', title: 'Design Moderno', desc: 'Interface clean com gradientes, glassmorphism e tema escuro automático.' },
 { icon: 'bi-phone', title: 'Responsivo', desc: 'Layout adaptável para qualquer dispositivo — desktop, tablet ou mobile.' },
 { icon: 'bi-shield-check', title: 'Boas Práticas', desc: 'Código semântico, acessível e seguindo padrões modernos da web.' },
 { icon: 'bi-gear', title: 'Fácil de Customizar', desc: 'Custom properties CSS, arquitetura simples e extensível.' },
 { icon: 'bi-moon-stars', title: 'Dark Mode', desc: 'Alternância de tema com um clique, respeitando a preferência do sistema.' }
 ].map((f, i) => `
 <div class="col-md-6 col-lg-4 fade-up" style="animation-delay:${i * 0.08}s">
 <div class="feature-card">
 <div class="feature-icon"><i class="bi ${f.icon}"></i></div>
 <h5>${f.title}</h5>
 <p class="mb-0">${f.desc}</p>
 </div>
 </div>
 `).join('')}
 </div>

 <div class="row g-4 text-center mb-3">
 ${[
 { value: '99.9%', label: 'Uptime' },
 { value: '< 50ms', label: 'Latência' },
 { value: '100', label: 'Lighthouse Score' },
 { value: '0', label: 'Dependências externas' }
 ].map((s, i) => `
 <div class="col-6 col-lg-3 fade-up" style="animation-delay:${(i + 6) * 0.08}s">
 <div class="stat-value">${s.value}</div>
 <div class="stat-label">${s.label}</div>
 </div>
 `).join('')}
 </div>
`;

utils/toast.js

export function showToast(title, body, iconClass = 'bi-info-circle-fill') {
 const container = document.getElementById('toastContainer');
 const id = 'toast-' + Date.now();
 const html = `
 <div id="${id}" class="toast align-items-center border-0 shadow-lg" role="alert" 
 aria-live="assertive" aria-atomic="true">
 <div class="toast-header">
 <i class="bi ${iconClass} me-2"></i>
 <strong class="me-auto">${title}</strong>
 <button type="button" class="btn-close btn-close-sm" data-bs-dismiss="toast"></button>
 </div>
 <div class="toast-body">${body}</div>
 </div>`;
 container.insertAdjacentHTML('beforeend', html);
 const toastEl = document.getElementById(id);
 const toast = new bootstrap.Toast(toastEl, { delay: 4000 });
 toast.show();
 toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
}

style.css

:root {
 --spa-primary: #6366f1 ;
 --spa-primary-hover: #4f46e5 ;
 --spa-accent: #06b6d4 ;
 --spa-gradient: linear-gradient(135deg, #6366f1 , #06b6d4 );
 --spa-transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 --spa-radius: 1rem;
 --spa-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
 --spa-shadow-lg: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}

* {
 font-family: 'Inter', system-ui, -apple-system, sans-serif;
}

body {
 min-height: 100vh;
 display: flex;
 flex-direction: column;
 overflow-x: hidden;
}

/* Navbar */
.navbar {
 backdrop-filter: blur(12px);
 -webkit-backdrop-filter: blur(12px);
 background: rgba(255, 255, 255, 0.8) !important;
 border-bottom: 1px solid rgba(0, 0, 0, 0.06);
 padding: 0.75rem 0;
 transition: var(--spa-transition);
}

[data-bs-theme="dark"] .navbar {
 background: rgba(30, 30, 40, 0.85) !important;
 border-bottom-color: rgba(255, 255, 255, 0.06);
}

.navbar-brand {
 font-weight: 700;
 font-size: 1.35rem;
 background: var(--spa-gradient);
 -webkit-background-clip: text;
 -webkit-text-fill-color: transparent;
 background-clip: text;
 letter-spacing: -0.025em;
}

.nav-link {
 font-weight: 500;
 font-size: 0.925rem;
 padding: 0.5rem 1rem !important;
 border-radius: 0.5rem;
 transition: var(--spa-transition);
 position: relative;
 color: var(--bs-body-color) !important;
}

.nav-link:hover,
.nav-link.active {
 color: var(--spa-primary) !important;
 background: rgba(99, 102, 241, 0.08);
}

.nav-link.active::after {
 content: '';
 position: absolute;
 bottom: 0;
 left: 50%;
 transform: translateX(-50%);
 width: 1.25rem;
 height: 2px;
 background: var(--spa-gradient);
 border-radius: 2px;
}

.nav-link i {
 margin-right: 0.35rem;
 font-size: 1rem;
}

/* Theme toggle */
.btn-theme {
 border: none;
 background: transparent;
 font-size: 1.2rem;
 padding: 0.4rem 0.6rem;
 border-radius: 0.5rem;
 transition: var(--spa-transition);
 color: var(--bs-body-color);
 cursor: pointer;
}

.btn-theme:hover {
 background: rgba(99, 102, 241, 0.1);
 color: var(--spa-primary);
}

/* Page transitions */
#app-content {
 flex: 1;
 opacity: 1;
 transform: translateY(0);
 transition: opacity 0.25s ease, transform 0.25s ease;
}

#app-content.page-exit {
 opacity: 0;
 transform: translateY(12px);
}

/* Hero Section */
.hero {
 background: var(--spa-gradient);
 border-radius: var(--spa-radius);
 padding: 4rem 2rem;
 color: #fff ;
 position: relative;
 overflow: hidden;
}

.hero::before {
 content: '';
 position: absolute;
 inset: 0;
 background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.06'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}

.hero h1 {
 font-weight: 700;
 font-size: 2.75rem;
 letter-spacing: -0.035em;
 position: relative;
}

.hero p {
 font-size: 1.15rem;
 opacity: 0.9;
 position: relative;
}

.hero .btn {
 position: relative;
}

/* Cards */
.feature-card {
 border: 1px solid rgba(0, 0, 0, 0.06);
 border-radius: var(--spa-radius);
 padding: 2rem;
 transition: var(--spa-transition);
 background: var(--bs-body-bg);
 height: 100%;
}

[data-bs-theme="dark"] .feature-card {
 border-color: rgba(255, 255, 255, 0.08);
}

.feature-card:hover {
 transform: translateY(-4px);
 box-shadow: var(--spa-shadow-lg);
 border-color: rgba(99, 102, 241, 0.25);
}

.feature-icon {
 width: 3rem;
 height: 3rem;
 display: flex;
 align-items: center;
 justify-content: center;
 border-radius: 0.75rem;
 background: rgba(99, 102, 241, 0.1);
 color: var(--spa-primary);
 font-size: 1.3rem;
 margin-bottom: 1rem;
}

.feature-card h5 {
 font-weight: 600;
 letter-spacing: -0.01em;
}

.feature-card p {
 color: var(--bs-secondary-color);
 font-size: 0.925rem;
 line-height: 1.6;
}

/* Stats */
.stat-value {
 font-size: 2.25rem;
 font-weight: 700;
 background: var(--spa-gradient);
 -webkit-background-clip: text;
 -webkit-text-fill-color: transparent;
 background-clip: text;
}

.stat-label {
 font-size: 0.85rem;
 color: var(--bs-secondary-color);
 font-weight: 500;
 text-transform: uppercase;
 letter-spacing: 0.05em;
}

/* About page */
.timeline-item {
 position: relative;
 padding-left: 2rem;
 padding-bottom: 2rem;
 border-left: 2px solid rgba(99, 102, 241, 0.2);
}

.timeline-item:last-child {
 padding-bottom: 0;
}

.timeline-item::before {
 content: '';
 position: absolute;
 left: -6px;
 top: 4px;
 width: 10px;
 height: 10px;
 background: var(--spa-primary);
 border-radius: 50%;
}

.timeline-item h6 {
 font-weight: 600;
 color: var(--spa-primary);
}

/* Team */
.team-card {
 text-align: center;
 padding: 2rem 1.5rem;
 border: 1px solid rgba(0, 0, 0, 0.06);
 border-radius: var(--spa-radius);
 transition: var(--spa-transition);
}

[data-bs-theme="dark"] .team-card {
 border-color: rgba(255, 255, 255, 0.08);
}

.team-card:hover {
 box-shadow: var(--spa-shadow-lg);
 transform: translateY(-4px);
}

.team-avatar {
 width: 80px;
 height: 80px;
 border-radius: 50%;
 background: var(--spa-gradient);
 display: flex;
 align-items: center;
 justify-content: center;
 margin: 0 auto 1rem;
 font-size: 2rem;
 color: #fff ;
 font-weight: 600;
}

/* Contact */
.contact-info-card {
 display: flex;
 align-items: center;
 gap: 1rem;
 padding: 1.25rem;
 border: 1px solid rgba(0, 0, 0, 0.06);
 border-radius: var(--spa-radius);
 transition: var(--spa-transition);
}

[data-bs-theme="dark"] .contact-info-card {
 border-color: rgba(255, 255, 255, 0.08);
}

.contact-info-card:hover {
 border-color: rgba(99, 102, 241, 0.3);
 box-shadow: var(--spa-shadow);
}

.contact-info-card .icon-wrapper {
 width: 3rem;
 height: 3rem;
 display: flex;
 align-items: center;
 justify-content: center;
 border-radius: 0.75rem;
 background: rgba(99, 102, 241, 0.1);
 color: var(--spa-primary);
 font-size: 1.2rem;
 flex-shrink: 0;
}

.form-control,
.form-select {
 border-radius: 0.75rem;
 padding: 0.75rem 1rem;
 border: 1px solid rgba(0, 0, 0, 0.1);
 transition: var(--spa-transition);
}

.form-control:focus,
.form-select:focus {
 border-color: var(--spa-primary);
 box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}

.btn-primary {
 background: var(--spa-gradient);
 border: none;
 border-radius: 0.75rem;
 padding: 0.75rem 2rem;
 font-weight: 600;
 transition: var(--spa-transition);
}

.btn-primary:hover {
 transform: translateY(-1px);
 box-shadow: 0 8px 16px rgba(99, 102, 241, 0.3);
}

.btn-outline-light {
 border-radius: 0.75rem;
 padding: 0.65rem 1.75rem;
 font-weight: 600;
 border-width: 2px;
}

/* Footer */
footer {
 border-top: 1px solid rgba(0, 0, 0, 0.06);
 margin-top: auto;
}

[data-bs-theme="dark"] footer {
 border-top-color: rgba(255, 255, 255, 0.06);
}

footer a {
 color: var(--bs-secondary-color);
 text-decoration: none;
 transition: var(--spa-transition);
}

footer a:hover {
 color: var(--spa-primary);
}

/* Animate elements on page load */
.fade-up {
 animation: fadeUp 0.5s ease forwards;
 opacity: 0;
}

@keyframes fadeUp {
 from {
 opacity: 0;
 transform: translateY(20px);
 }
 to {
 opacity: 1;
 transform: translateY(0);
 }
}

.fade-up:nth-child(2) { animation-delay: 0.1s; }
.fade-up:nth-child(3) { animation-delay: 0.2s; }
.fade-up:nth-child(4) { animation-delay: 0.3s; }

/* Toast */
.toast-container {
 position: fixed;
 bottom: 1.5rem;
 right: 1.5rem;
 z-index: 9999;
}

/* Scrollbar */
::-webkit-scrollbar {
 width: 8px;
}

::-webkit-scrollbar-track {
 background: transparent;
}

::-webkit-scrollbar-thumb {
 background: rgba(99, 102, 241, 0.3);
 border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
 background: rgba(99, 102, 241, 0.5);
}