Como criar um simples SPA com javascript e bootstrap 5

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);
}