Partilhar: WhatsApp
aulify
UC UC00622 · T. Desenv. Software

Ficha 01 · HTML e CSS

Estrutura semântica, layout, design
Versão · Aluno
Tempo · 60 minutos
Cotação · 100 pontos
Aluno(a)
Turma
Data
Objectivos da ficha

Parte I · HTML

Exercício 1 · Estrutura semântica (15 pts)

Escreve estrutura HTML5 semântica para uma página "Sobre nós" com: - Cabeçalho com logo e navegação. - Conteúdo principal com título e parágrafos. - Sidebar com links relacionados. - Rodapé com informação de contacto.

<!DOCTYPE html>
<html lang="pt-PT">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sobre Nós | Empresa XYZ</title>
  <meta name="description" content="Conhece a nossa empresa, missão e equipa.">
</head>
<body>
  <header>
    <div class="logo">
      <img src="logo.svg" alt="Logo Empresa XYZ">
    </div>
    <nav aria-label="Principal">
      <ul>
        <li><a href="/">Início</a></li>
        <li><a href="/sobre" aria-current="page">Sobre</a></li>
        <li><a href="/servicos">Serviços</a></li>
        <li><a href="/contacto">Contacto</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <article>
      <h1>Sobre a Empresa XYZ</h1>

      <section>
        <h2>A nossa história</h2>
        <p>A Empresa XYZ foi fundada em 2010 com o objectivo de...</p>
        <p>Desde então, ajudámos centenas de clientes a...</p>
      </section>

      <section>
        <h2>Missão e valores</h2>
        <p>A nossa missão é...</p>
        <ul>
          <li>Qualidade em primeiro lugar</li>
          <li>Inovação constante</li>
          <li>Foco no cliente</li>
        </ul>
      </section>

      <section>
        <h2>A nossa equipa</h2>
        <p>Somos uma equipa de profissionais apaixonados...</p>
      </section>
    </article>

    <aside>
      <h3>Ver também</h3>
      <ul>
        <li><a href="/servicos">Os nossos serviços</a></li>
        <li><a href="/portfolio">Portfolio</a></li>
        <li><a href="/blog">Blog</a></li>
      </ul>
    </aside>
  </main>

  <footer>
    <div class="contacto">
      <h2>Contacto</h2>
      <address>
        Empresa XYZ<br>
        Rua das Flores, 12<br>
        1000-000 Lisboa<br>
        <a href="tel:+351200000000">+351 200 000 000</a><br>
        <a href="mailto:info@empresa.pt">info@empresa.pt</a>
      </address>
    </div>

    <nav aria-label="Footer">
      <ul>
        <li><a href="/privacidade">Política de Privacidade</a></li>
        <li><a href="/termos">Termos de Uso</a></li>
      </ul>
    </nav>

    <p>&copy; 2026 Empresa XYZ. Todos os direitos reservados.</p>
  </footer>
</body>
</html>

Características:

  1. <header> contém logo + nav.
  2. <main> é o conteúdo único e principal.
  3. <article> para conteúdo independente (página inteira sobre nós).
  4. <section> para subdivisões temáticas.
  5. <aside> para conteúdo relacionado mas independente.
  6. <footer> para rodapé.
  7. <nav> com aria-label para distinguir nav principal vs footer.
  8. <address> para info de contacto.
  9. alt em imagem.
  10. Hierarquia de headings: h1 → h2 → h3.

Exercício 2 · Formulário (15 pts)

Cria formulário de contacto com validação HTML5: - Nome (obrigatório, mín 3 chars). - Email (obrigatório). - Telefone (opcional, formato português). - Assunto (dropdown). - Mensagem (textarea, mín 10 chars). - Aceitar política privacidade (checkbox). - Botão submit.

<form action="/api/contact" method="POST" class="form-contact">
  <h2>Contacto</h2>

  <!-- Nome -->
  <div class="form-group">
    <label for="nome">Nome *</label>
    <input 
      type="text" 
      id="nome" 
      name="nome" 
      required 
      minlength="3"
      maxlength="100"
      placeholder="Joana Silva"
      autocomplete="name">
  </div>

  <!-- Email -->
  <div class="form-group">
    <label for="email">Email *</label>
    <input 
      type="email" 
      id="email" 
      name="email" 
      required
      placeholder="joana@email.com"
      autocomplete="email">
  </div>

  <!-- Telefone (opcional) -->
  <div class="form-group">
    <label for="telefone">Telefone</label>
    <input 
      type="tel" 
      id="telefone" 
      name="telefone"
      pattern="(\+351)?[0-9]{9}"
      placeholder="912 345 678"
      autocomplete="tel">
    <small>Opcional. Formato: 912345678 ou +351912345678</small>
  </div>

  <!-- Assunto -->
  <div class="form-group">
    <label for="assunto">Assunto *</label>
    <select id="assunto" name="assunto" required>
      <option value="">-- Selecciona --</option>
      <option value="info">Pedido de informação</option>
      <option value="orcamento">Pedido de orçamento</option>
      <option value="suporte">Suporte técnico</option>
      <option value="outro">Outro</option>
    </select>
  </div>

  <!-- Mensagem -->
  <div class="form-group">
    <label for="mensagem">Mensagem *</label>
    <textarea 
      id="mensagem" 
      name="mensagem" 
      required
      minlength="10"
      maxlength="1000"
      rows="6"
      placeholder="Escreve aqui a tua mensagem..."></textarea>
    <small>Mínimo 10 caracteres.</small>
  </div>

  <!-- Newsletter (opcional) -->
  <div class="form-group">
    <input 
      type="checkbox" 
      id="newsletter" 
      name="newsletter">
    <label for="newsletter">
      Quero receber a newsletter mensal
    </label>
  </div>

  <!-- Aceitar política (obrigatório) -->
  <div class="form-group">
    <input 
      type="checkbox" 
      id="privacidade" 
      name="privacidade" 
      required>
    <label for="privacidade">
      Aceito a <a href="/privacidade" target="_blank">política de privacidade</a> *
    </label>
  </div>

  <!-- Submit -->
  <div class="form-actions">
    <button type="submit">Enviar mensagem</button>
    <button type="reset">Limpar</button>
  </div>

  <p class="form-note">* Campos obrigatórios</p>
</form>

Validação HTML5 usada:

Acessibilidade:

CSS sugerido:

.form-contact {
  max-width: 500px;
  margin: 2rem auto;
  padding: 2rem;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 1.25rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.25rem;
  font-weight: 600;
}

.form-group input,
.form-group select,
.form-group textarea {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  font: inherit;
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
  outline: 2px solid #6366f1;
  border-color: #6366f1;
}

/* Checkbox alinhado */
.form-group input[type="checkbox"] {
  width: auto;
  display: inline-block;
  margin-right: 0.5rem;
}

.form-group input[type="checkbox"] + label {
  display: inline;
  font-weight: normal;
}

/* Erro visual (HTML5 validation) */
.form-group input:invalid:not(:focus):not(:placeholder-shown) {
  border-color: #ef4444;
}

button[type="submit"] {
  background: #6366f1;
  color: white;
  padding: 0.75rem 2rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
}

button[type="submit"]:hover {
  background: #4f46e5;
}

JavaScript para tratamento (opcional):

const form = document.querySelector('.form-contact');

form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const formData = new FormData(form);
  const data = Object.fromEntries(formData);

  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (response.ok) {
      alert('Mensagem enviada! Vamos contactá-lo em breve.');
      form.reset();
    } else {
      alert('Erro ao enviar. Tenta novamente.');
    }
  } catch (error) {
    alert('Erro de rede.');
  }
});

Parte II · CSS

Exercício 3 · Box model (10 pts)

Para este HTML:

<div class="caixa">Hello</div>

Aplica CSS: - Width: 200px, height: 100px. - Padding: 20px. - Border: 5px solid black. - Margin: 10px. - Background azul.

a) Qual é a largura total visível? b) Como mudar para que width=200 seja a largura total?

a) Largura total visível:

Por defeito (box-sizing: content-box): - content width: 200px. - + padding (esquerda + direita): 20 + 20 = 40px. - + border (esquerda + direita): 5 + 5 = 10px. - Total visível: 200 + 40 + 10 = 250px.

Margem não conta para largura visível (é espaço externo).

Espaço total ocupado: - 250 + margem (10 + 10) = 270px de espaço horizontal.

b) Para que width=200 seja largura total:

Usar box-sizing: border-box:

.caixa {
  box-sizing: border-box;  /* ← isto */
  width: 200px;
  height: 100px;
  padding: 20px;
  border: 5px solid black;
  margin: 10px;
  background: blue;
}

Com border-box: - width 200px inclui content + padding + border. - Content area = 200 - 40 (padding) - 10 (border) = 150px. - Total visível: 200px (como pedido).

Recomendação universal:

*, *::before, *::after {
  box-sizing: border-box;
}

Aplicar globalmente para comportamento mais intuitivo.

Comparação visual:

content-box (default):
┌────────────────────────────┐  ← total: 250px
│ border (5px)               │
│ ┌────────────────────────┐ │
│ │ padding (20px)         │ │
│ │ ┌────────────────────┐ │ │
│ │ │ content (200px)    │ │ │
│ │ └────────────────────┘ │ │
│ └────────────────────────┘ │
└────────────────────────────┘

border-box:
┌────────────────────────────┐  ← total: 200px (= width)
│ border (5px)               │
│ ┌────────────────────────┐ │
│ │ padding (20px)         │ │
│ │ ┌────────────────────┐ │ │
│ │ │ content (150px)    │ │ │
│ │ └────────────────────┘ │ │
│ └────────────────────────┘ │
└────────────────────────────┘

Exercício 4 · Flexbox (15 pts)

Cria layout com Flexbox: - Container com 3 cards. - Cada card cresce igualmente. - Espaço de 20px entre cards. - Centrados verticalmente. - Em mobile (< 768px), cards empilham verticalmente.

<div class="cards-container">
  <article class="card">
    <h2>Card 1</h2>
    <p>Conteúdo do primeiro card.</p>
  </article>

  <article class="card">
    <h2>Card 2</h2>
    <p>Conteúdo do segundo card.</p>
  </article>

  <article class="card">
    <h2>Card 3</h2>
    <p>Conteúdo do terceiro card.</p>
  </article>
</div>
/* Mobile first (default) */
.cards-container {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px;
}

.card {
  background: white;
  padding: 1.5rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}

.card h2 {
  margin: 0 0 0.5rem 0;
  color: #333;
}

.card p {
  margin: 0;
  color: #666;
}

/* Tablet+ */
@media (min-width: 768px) {
  .cards-container {
    flex-direction: row;
    align-items: stretch;  /* mesma altura */
    max-width: 1200px;
    margin: 0 auto;
  }

  .card {
    flex: 1;  /* cada card cresce igualmente */
  }
}

Resultado:

Mobile (< 768px):

┌────────────────┐
│  Card 1        │
└────────────────┘
       ↓ 20px gap
┌────────────────┐
│  Card 2        │
└────────────────┘
       ↓ 20px gap
┌────────────────┐
│  Card 3        │
└────────────────┘

Tablet+ (>= 768px):

┌─────────┐ ←20px→ ┌─────────┐ ←20px→ ┌─────────┐
│ Card 1  │        │ Card 2  │        │ Card 3  │
└─────────┘        └─────────┘        └─────────┘

Explicação:

Mobile first (regras fora de media query): - flex-direction: column: empilha verticalmente. - gap: 20px: espaço entre items. - Cada card ocupa largura total (default).

Tablet+ (media query): - flex-direction: row: horizontal. - flex: 1 em cada card: crescem igualmente. - Container limitado a 1200px e centrado.

Variações úteis:

Cards com tamanhos diferentes mas iguais:

.card {
  flex: 1 1 0;  /* grow shrink basis */
}

Card com largura mínima (não compactar demais):

.card {
  flex: 1 1 250px;  /* min 250px */
}

Centrar tudo:

.cards-container {
  display: flex;
  justify-content: center;  /* eixo principal */
  align-items: center;       /* eixo cruzado */
}

Justify-content options: - flex-start: ao início. - flex-end: ao fim. - center: ao centro. - space-between: espaço entre items. - space-around: espaço à volta. - space-evenly: espaço igual.

Wrap (várias linhas):

.cards-container {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

.card {
  flex: 1 1 250px;  /* min 250px, wrap se não cabe */
}

Parte III · Layout completo

Exercício 5 · Grid layout (15 pts)

Cria layout "Holy Grail" com CSS Grid: - Header (topo). - Sidebar (esquerda). - Main content (centro). - Aside (direita). - Footer (baixo).

Mobile: tudo empilhado.

<div class="layout">
  <header>Header</header>
  <nav class="sidebar">Sidebar</nav>
  <main>Main Content</main>
  <aside>Aside</aside>
  <footer>Footer</footer>
</div>
/* Mobile first */
.layout {
  display: grid;
  grid-template-areas:
    "header"
    "sidebar"
    "main"
    "aside"
    "footer";
  min-height: 100vh;
  gap: 0;
}

header { grid-area: header; }
.sidebar { grid-area: sidebar; }
main { grid-area: main; }
aside { grid-area: aside; }
footer { grid-area: footer; }

/* Estilos */
header {
  background: #1f2937;
  color: white;
  padding: 1rem;
}

.sidebar {
  background: #f3f4f6;
  padding: 1rem;
}

main {
  background: white;
  padding: 1rem;
}

aside {
  background: #e5e7eb;
  padding: 1rem;
}

footer {
  background: #1f2937;
  color: white;
  padding: 1rem;
  text-align: center;
}

/* Desktop */
@media (min-width: 1024px) {
  .layout {
    grid-template-columns: 200px 1fr 200px;
    grid-template-rows: auto 1fr auto;
    grid-template-areas:
      "header  header  header"
      "sidebar main    aside"
      "footer  footer  footer";
  }
}

Mobile:

┌─────────────┐
│   Header    │
├─────────────┤
│   Sidebar   │
├─────────────┤
│    Main     │
├─────────────┤
│    Aside    │
├─────────────┤
│   Footer    │
└─────────────┘

Desktop:

┌──────────────────────────────────┐
│            Header                 │
├─────────┬───────────────┬────────┤
│ Sidebar │     Main      │ Aside  │
│         │               │        │
│         │               │        │
├─────────┴───────────────┴────────┤
│            Footer                 │
└──────────────────────────────────┘

Vantagens de Grid Areas:

  1. Visual: vê-se imediatamente o layout no CSS.
  2. Manutenção: alterar layout = mudar áreas (não mover HTML).
  3. Responsive: redefine areas em media queries.

Variações:

Sidebar à direita:

grid-template-areas:
  "header  header  header"
  "main    main    sidebar"
  "footer  footer  footer";

Sem aside:

grid-template-columns: 250px 1fr;
grid-template-areas:
  "header  header"
  "sidebar main"
  "footer  footer";

Magazine layout (várias colunas, áreas complexas):

grid-template-columns: repeat(4, 1fr);
grid-template-areas:
  "h h h h"
  "f f f s"
  "a b c s"
  "g g s s";

Combinação Grid + Flexbox: - Grid: layout 2D (página inteira). - Flexbox: layout 1D (dentro de componentes).

/* Grid para página */
.page { display: grid; ... }

/* Flexbox para cards dentro de main */
main {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

Exercício 6 · Responsividade (15 pts)

Página com cards de produtos. Comportamento responsivo: - Mobile (< 600px): 1 coluna. - Tablet (600-1023px): 2 colunas. - Desktop (>= 1024px): 4 colunas.

Sem media queries explícitas, usar Grid auto-fit.

<section class="produtos">
  <article class="produto">...</article>
  <article class="produto">...</article>
  <article class="produto">...</article>
  <article class="produto">...</article>
  <!-- mais cards -->
</section>

Solução com Grid auto-fit (elegante, sem media queries):

.produtos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.produto {
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 1.5rem;
}

Como funciona:

Comportamento automático:

Sem media queries necessárias. Funciona em qualquer largura.

Solução com media queries explícitas (se prefere mais controlo):

.produtos {
  display: grid;
  grid-template-columns: 1fr;  /* mobile: 1 col */
  gap: 20px;
  padding: 20px;
}

@media (min-width: 600px) {
  .produtos {
    grid-template-columns: repeat(2, 1fr);  /* tablet: 2 cols */
  }
}

@media (min-width: 1024px) {
  .produtos {
    grid-template-columns: repeat(4, 1fr);  /* desktop: 4 cols */
    max-width: 1200px;
    margin: 0 auto;
  }
}

Comparação:

Approach Vantagens Desvantagens
auto-fit Funciona em qualquer largura, código mínimo Menos controlo exacto
Media queries Breakpoints precisos Mais código, manutenção

Híbrido (recomendado para sites grandes):

.produtos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
  max-width: 1400px;
  margin: 0 auto;
  padding: 20px;
}

@media (max-width: 768px) {
  .produtos {
    padding: 10px;
    gap: 10px;
  }
}

auto-fit para colunas + media queries para padding/gap ajustes finos.

Bonus — Card de produto completo:

<article class="produto">
  <img src="produto.jpg" alt="Nome do produto" loading="lazy">
  <div class="produto-info">
    <h3>Nome do Produto</h3>
    <p class="produto-desc">Descrição curta do produto.</p>
    <div class="produto-footer">
      <span class="preco">19,99 €</span>
      <button class="btn-comprar">Comprar</button>
    </div>
  </div>
</article>
.produto {
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
  display: flex;
  flex-direction: column;
}

.produto:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}

.produto img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.produto-info {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  flex: 1;
}

.produto-info h3 {
  margin: 0 0 0.5rem 0;
  font-size: 1.1rem;
}

.produto-desc {
  color: #666;
  flex: 1;
  margin-bottom: 1rem;
}

.produto-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.preco {
  font-size: 1.25rem;
  font-weight: 700;
  color: #6366f1;
}

.btn-comprar {
  background: #6366f1;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
}

.btn-comprar:hover {
  background: #4f46e5;
}

Resultado: cards de produto profissionais, responsivos, com hover effects.

Parte IV · Casos práticos

Exercício 7 · Sticky header (10 pts)

Como fazer header que fica fixo no topo ao scroll?

Sticky Header:

<header class="sticky-header">
  <nav>
    <a href="/">Logo</a>
    <ul>
      <li><a href="#home">Home</a></li>
      <li><a href="#sobre">Sobre</a></li>
      <li><a href="#contacto">Contacto</a></li>
    </ul>
  </nav>
</header>

Solução 1 — position: sticky (moderno, recomendado):

.sticky-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  padding: 1rem;
}

Vantagens: - Simples: 1 linha (position: sticky). - Performance: nativo no browser. - Conteúdo abaixo ajusta-se automaticamente.

Solução 2 — position: fixed (mais antigo):

.sticky-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  padding: 1rem;
}

/* Padding no body para compensar */
body {
  padding-top: 70px;  /* altura do header */
}

Diferenças:

Sticky geralmente é melhor (mais flexível).

Adicionar efeito visual no scroll:

.sticky-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: white;
  padding: 1rem;
  transition: all 0.3s;
}

/* Quando faz scroll, adicionar shadow + reduzir altura */
.sticky-header.scrolled {
  padding: 0.5rem 1rem;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
window.addEventListener('scroll', () => {
  const header = document.querySelector('.sticky-header');
  if (window.scrollY > 50) {
    header.classList.add('scrolled');
  } else {
    header.classList.remove('scrolled');
  }
});

Esconder ao fazer scroll para baixo, mostrar ao subir:

let lastScroll = 0;
const header = document.querySelector('.sticky-header');

window.addEventListener('scroll', () => {
  const current = window.scrollY;

  if (current > lastScroll && current > 100) {
    // Scroll para baixo
    header.style.transform = 'translateY(-100%)';
  } else {
    // Scroll para cima
    header.style.transform = 'translateY(0)';
  }

  lastScroll = current;
});
.sticky-header {
  transition: transform 0.3s;
}

Pattern comum em sites modernos (Medium, etc.).

Exercício 8 · Mobile menu (15 pts)

Cria menu hamburger para mobile que abre/fecha com JavaScript.

<header>
  <nav class="navbar">
    <a href="/" class="logo">Logo</a>

    <button 
      class="menu-toggle" 
      aria-label="Abrir menu"
      aria-expanded="false"
      aria-controls="menu">
      <span class="hamburger"></span>
      <span class="hamburger"></span>
      <span class="hamburger"></span>
    </button>

    <ul class="menu" id="menu">
      <li><a href="/">Home</a></li>
      <li><a href="/sobre">Sobre</a></li>
      <li><a href="/servicos">Serviços</a></li>
      <li><a href="/contacto">Contacto</a></li>
    </ul>
  </nav>
</header>
/* Reset básico */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
}

/* Navbar */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 1.5rem;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  position: relative;
}

.logo {
  font-size: 1.5rem;
  font-weight: 700;
  color: #6366f1;
  text-decoration: none;
}

/* Mobile menu (default: hidden) */
.menu {
  display: none;
  list-style: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  flex-direction: column;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  padding: 1rem 0;
}

.menu.open {
  display: flex;
}

.menu li {
  padding: 0;
}

.menu a {
  display: block;
  padding: 0.75rem 1.5rem;
  color: #333;
  text-decoration: none;
  transition: background 0.2s;
}

.menu a:hover {
  background: #f3f4f6;
}

/* Hamburger button */
.menu-toggle {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.hamburger {
  display: block;
  width: 24px;
  height: 3px;
  background: #333;
  border-radius: 2px;
  transition: all 0.3s;
}

/* Quando aberto, transforma em X */
.menu-toggle[aria-expanded="true"] .hamburger:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] .hamburger:nth-child(2) {
  opacity: 0;
}

.menu-toggle[aria-expanded="true"] .hamburger:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* Desktop */
@media (min-width: 768px) {
  .menu-toggle {
    display: none;
  }

  .menu {
    display: flex !important;  /* override mobile */
    position: static;
    flex-direction: row;
    box-shadow: none;
    padding: 0;
    gap: 2rem;
  }

  .menu a {
    padding: 0;
  }
}
const toggle = document.querySelector('.menu-toggle');
const menu = document.querySelector('.menu');

toggle.addEventListener('click', () => {
  const isOpen = menu.classList.toggle('open');

  // Acessibilidade: actualizar aria-expanded
  toggle.setAttribute('aria-expanded', isOpen);
  toggle.setAttribute('aria-label', isOpen ? 'Fechar menu' : 'Abrir menu');
});

// Fechar ao clicar fora
document.addEventListener('click', (e) => {
  if (!e.target.closest('.navbar')) {
    menu.classList.remove('open');
    toggle.setAttribute('aria-expanded', 'false');
    toggle.setAttribute('aria-label', 'Abrir menu');
  }
});

// Fechar com Esc
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && menu.classList.contains('open')) {
    menu.classList.remove('open');
    toggle.setAttribute('aria-expanded', 'false');
    toggle.focus();  // voltar foco ao toggle
  }
});

Características demonstradas:

Mobile (< 768px): - Menu hamburger visível. - Menu escondido por defeito. - Click abre menu (vertical, full width). - Hamburger transforma-se em X. - Fechar: click fora, Esc, ou click no toggle.

Desktop (>= 768px): - Hamburger escondido. - Menu horizontal sempre visível. - Espaçamento entre items.

Acessibilidade:

aria-label no botão. ✅ aria-expanded indica estado. ✅ aria-controls liga ao menu. ✅ Esc para fechar (keyboard). ✅ Click fora para fechar. ✅ Foco retorna ao toggle ao fechar. ✅ Texto descritivo no aria-label muda.

Animação CSS do hamburger: - 3 linhas → X com rotação CSS pura. - Smooth transition. - Sem JavaScript para animação visual.

Extensões:

Slide-in animation:

.menu {
  transform: translateX(100%);
  transition: transform 0.3s ease-in-out;
}

.menu.open {
  transform: translateX(0);
}

Overlay backdrop:

.menu.open::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  z-index: -1;
}

Submenus expansíveis:

<li class="has-submenu">
  <a href="/servicos">Serviços ▾</a>
  <ul class="submenu">
    <li><a href="/servico-1">Serviço 1</a></li>
    <li><a href="/servico-2">Serviço 2</a></li>
  </ul>
</li>
document.querySelectorAll('.has-submenu > a').forEach(link => {
  link.addEventListener('click', (e) => {
    e.preventDefault();
    link.parentElement.classList.toggle('open');
  });
});

Trap focus (acessibilidade avançada): manter Tab dentro do menu aberto.