Apostila de arquivo único — Nível 1 — Introdução ao Desenvolvimento Web. Uma aula por vez; use j/k ou o menu lateral. Voltar ao índice.

Nível 1Unidade 1 · Arquitetura da Web e HTML3 aulas de 50 min + 1 h EAD

Aula 01 — Apresentação, tecnologias e arquitetura da Web

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Descrever como esta trilha funciona: unidades, marcos do projeto, projeto fio-condutor e projeto autoral.
  • Distinguir Internet de World Wide Web e situar os principais marcos da história da Web.
  • Explicar o modelo cliente-servidor e o papel do front-end e do back-end.
  • Descrever, passo a passo, o que acontece entre digitar uma URL e ver a página renderizada.
  • Decompor uma URL em suas seis partes e escrever caminhos relativos e absolutos corretos.
  • Classificar arquiteturas web (camadas, estático × dinâmico, MPA × SPA) e apontar onde o front-end se encaixa.
  • Configurar o ambiente de trabalho (VS Code, Live Server e DevTools) e criar o primeiro documento HTML.

📋 Pré-requisitos

Esta é a primeira aula: não há conteúdo anterior para retomar. Mas há o que trazer:

  • [ ] Um notebook ou computador com Google Chrome ou Firefox atualizado.
  • [ ] Visual Studio Code instalado (https://code.visualstudio.com/). Se ainda não instalou, faremos juntos no Bloco 3.
  • [ ] Extensões do VS Code Live Server e Prettier (instalação na §1.6 desta aula).
  • [ ] Uma pasta dedicada no computador para guardar os arquivos do curso (o Git chega na Aula 15).
  • [ ] Uma ideia, mesmo vaga, de tema para o seu projeto autoral (ver §1.4). Você decide hoje.

Nenhuma experiência prévia com programação é exigida. Se você nunca escreveu uma linha de código, está no lugar certo — a trilha começa do zero.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Como esta trilha funciona (marcos do projeto, projeto fio-condutor e projeto autoral); Internet × Web; linha do tempo
2 50 min Cliente-servidor; o que acontece ao digitar uma URL; anatomia da URL; renderização; camadas, estático × dinâmico, MPA × SPA, APIs e REST
3 50 min Mão na massa: ambiente de trabalho, pasta do projeto, primeiro HTML e exploração do DevTools

1. Como funciona esta trilha

1.1 O que esta trilha cobre

Esta trilha cobre: arquiteturas computacionais para Web; criação de páginas web com HTML, CSS e JavaScript.

Objetivo geral: ao final desta trilha, você projeta e desenvolve o front-end de aplicações web funcionais, acessíveis e responsivas, dominando as três tecnologias fundamentais da plataforma (HTML, CSS e JavaScript) e entendendo a arquitetura em que elas operam.

A palavra-chave é front-end: tudo que roda no navegador do usuário. Você vai entender o sistema completo (servidor, banco de dados, API), mas vai construir a parte que o usuário vê e toca. O back-end é assunto do Nível 2.

1.2 Três unidades, três marcos do projeto

Unidade Foco Aulas Marco do projeto
1 Arquitetura da Web e HTML 01 a 05 Marco 1
2 CSS: estilo, layout e responsividade 06 a 09 Marco 2
3 JavaScript e interatividade 10 a 15 Marco 3

Os três marcos incidem sobre o mesmo site: o Marco 1 constrói a estrutura em HTML, o Marco 2 aplica o design com CSS, o Marco 3 acrescenta o comportamento com JavaScript. Por isso é importante escolher hoje um tema que você aguente desenvolver até o fim da trilha.

1.3 A sequência das aulas

Aula Tema
01 Apresentação da trilha; tecnologias e arquitetura da Web
02 Introdução ao HTML: estrutura, textos, links, tabelas
03 Introdução ao formulário
04 Formulário, mídias e listas
05 Elementos HTML para layout e introdução ao CSS
06 CSS: sintaxe, seletores, classes, atributos e valores — Marco 1 do projeto
07 Formatando o layout de um website e o menu
08 Criando telas responsivas
09 Animações e efeitos em CSS
10 Introdução ao JavaScript — Marco 2 do projeto
11 Variáveis, operações aritméticas e estruturas de controle
12 Estruturas sequenciais, condicionais e de repetição
13 Funções e eventos
14 JavaScript para validação de formulários e consultas dinâmicas
15 Publicando seu website na internet — Marco 3 do projeto

O conteúdo abaixo é o mesmo em qualquer oferta: serve igualmente a quem estuda por conta própria, sem data alguma, e a quem cursa esta trilha em uma turma com professor e calendário próprios — nesse caso, é o professor quem define as datas.

Marco Escopo
1 Site em HTML com os elementos da Unidade 1 (estrutura, textos, links, tabelas, formulários, mídias, listas).
2 O mesmo site estilizado com CSS: layout, menu, responsividade e animações.
3 O site dinâmico e interativo com JavaScript: eventos, validação de formulários e consultas dinâmicas.

Os blocos 📌 Vale gravar espalhados pelas aulas destacam os pontos que mais reaparecem mais adiante — vale mesmo memorizar.

Esta trilha soma aproximadamente 60 h de estudo: cerca de 45 h acompanhando as 15 aulas (teoria e prática guiada, em três blocos de 50 min cada) e 15 h nas atividades assíncronas — uma por aula, feita por conta própria depois do conteúdo principal.

⚠️ Atenção As atividades assíncronas não são extras opcionais. Elas compõem a prática real da trilha e alimentam o Marco do projeto da unidade correspondente. Pule uma e você sente falta dela mais adiante.

1.4 Projeto fio-condutor e projeto autoral

A regra pedagógica central do WebLab é simples:

  1. O projeto fio-condutor é construído ao longo da trilha — o site de um evento acadêmico, a "Semana Acadêmica de Sistemas de Informação". São cinco páginas: início, programação, inscrição, palestrantes e contato. Na Unidade 1 elas nascem em HTML puro; na Unidade 2 ganham CSS; na Unidade 3 ganham JavaScript. Se você está em aula com um professor, é o projeto que ele constrói com a turma, passo a passo, e todo mundo digita junto; se está estudando sozinho, é o que você constrói acompanhando cada Mão na massa.
  2. Cada pessoa desenvolve também um projeto autoral com a mesma arquitetura (cinco páginas, mesma sequência de tecnologias) e um domínio diferente. Os marcos do projeto acompanham o projeto autoral, não o site do evento.

Exemplos de temas que funcionam bem: catálogo de plantas do Pantanal, agenda de quadras esportivas, mural de estágios do curso, brechó de roupas, controle de pescarias no Teles Pires, cardápio de um restaurante, portfólio de um fotógrafo, site de uma ONG de proteção animal. O critério é: você consegue pensar em cinco páginas com conteúdo real para esse tema? Se sim, serve.

💡 Dica Escolha um tema que você goste e sobre o qual tenha conteúdo (textos, dados para tabelas, fotos). O erro mais comum é escolher algo genérico demais ("site de uma empresa") e ficar sem o que escrever na página. Tema concreto gera site melhor.

1.5 Como estudar com este material

Cada aula do WebLab tem quatro camadas de prática, sempre nesta ordem: 💻 Mão na massa (o passo a passo guiado, que você acompanha digitando junto — em aula ou sozinho), 🧪 Laboratório (exercícios em três níveis), 🏆 Desafios (extras, opcionais, com estrelas de dificuldade) e 🏠 Para praticar depois da aula (a tarefa de 1 h ligada ao seu projeto autoral).

Uma rotina que funciona:

  1. Antes de estudar cada aula, leia os objetivos e passe os olhos no conteúdo. Você aproveita muito mais sabendo aonde ela vai chegar.
  2. Ao acompanhar o conteúdo, digite o código junto. Copiar e colar não fixa nada; digitar e errar, sim.
  3. Logo em seguida, faça o Laboratório Nível A. São perguntas curtas que consolidam o vocabulário.
  4. Nos dias seguintes, faça o Nível B e a Atividade assíncrona. São eles que constroem habilidade de verdade.
  5. Se sobrar tempo e vontade, encare o Nível C e os Desafios. São do tamanho de um item de portfólio — o tipo de coisa que impressiona em uma entrevista de emprego.

Programação não se aprende lendo. Se você fechar esta página achando que entendeu tudo e não tiver escrito código, não aprendeu. Abra o editor.

1.6 Ambiente de trabalho

Ferramenta Para quê
Google Chrome ou Firefox Executar as páginas e depurar com o DevTools
Visual Studio Code Editar código
Live Server (extensão do VS Code) Servidor local com recarregamento automático
Prettier (extensão do VS Code) Formatação automática do código
Validador do W3C (https://validator.w3.org/) Verificar se a marcação HTML está correta
Git e GitHub Versionamento e publicação (a partir da Aula 15 e da trilha Deploy)

Instalação, em ordem:

  1. Baixe e instale o VS Code em https://code.visualstudio.com/. Aceite as opções padrão.
  2. Abra o VS Code e pressione Ctrl+Shift+X para abrir a aba de extensões.
  3. Procure Live Server (autor: Ritwick Dey) e clique em Install.
  4. Procure Prettier – Code formatter e clique em Install.
  5. Em File → Preferences → Settings, procure format on save e marque a opção. A partir de agora o código é formatado toda vez que você salva.
  6. Instale o Chrome ou o Firefox, se ainda não tiver. Os dois têm DevTools equivalentes; os exemplos das aulas usam o Chrome.

Git e GitHub ficam para mais tarde: o capítulo 02 da trilha Deploy & Ferramentas ensina do zero, e a Aula 15 publica o site.

2. Internet não é a Web

São coisas diferentes, e confundi-las é o primeiro erro conceitual da área.

A Internet é a infraestrutura física e lógica: cabos, fibras, roteadores, satélites e um conjunto de protocolos (principalmente TCP/IP) que permitem que máquinas em qualquer lugar do planeta troquem pacotes de dados. A Internet existe desde os anos 1970 (ARPANET) e transporta muito mais do que páginas: e-mail (SMTP), transferência de arquivos (FTP), streaming, chamadas de voz (VoIP), jogos online, o próprio DNS.

A World Wide Web é uma aplicação que roda sobre a Internet. Foi proposta por Tim Berners-Lee no CERN em 1989 e é composta por três invenções combinadas:

Invenção Função
URL (Uniform Resource Locator) Um endereço universal para identificar qualquer recurso
HTTP (HyperText Transfer Protocol) Um protocolo para pedir e receber esses recursos
HTML (HyperText Markup Language) Uma linguagem para descrever documentos com links entre si

💡 Dica Analogia: a Internet é o sistema rodoviário (asfalto, placas, regras de trânsito). A Web é o serviço de entregas que usa essas estradas. WhatsApp, e-mail e jogos online são outros serviços que usam as mesmas estradas sem serem a Web.

Repare que as três invenções são exatamente o que você vai estudar: URLs (hoje), HTTP (hoje e no Nível 2) e HTML (a partir da próxima aula). A Web é, no fundo, uma ideia simples: documentos com endereço, que apontam uns para os outros.

Linha do tempo essencial

Ano Marco
1969 ARPANET — primeira rede de comutação de pacotes
1983 TCP/IP se torna o protocolo padrão da ARPANET
1989–1991 Berners-Lee propõe e implementa a Web no CERN; o primeiro site vai ao ar
1993 O navegador Mosaic populariza a Web com imagens
1994 Fundação do W3C (World Wide Web Consortium)
1995 Nasce o JavaScript (Brendan Eich, Netscape, em cerca de 10 dias)
1996 Primeira recomendação do CSS
1999–2005 Ajax e o conceito de "Web 2.0": páginas que atualizam sem recarregar
2014–2015 HTML5 vira recomendação do W3C; ES2015 moderniza o JavaScript
Hoje Padrões vivos (living standards) mantidos por WHATWG, W3C e TC39

🧠 Você sabia? O primeiro site do mundo continua no ar, no endereço original: http://info.cern.ch/hypertext/WWW/TheProject.html. É só HTML, sem uma linha de CSS ou JavaScript — e abre em qualquer navegador moderno em milissegundos. Essa compatibilidade com o passado é uma decisão de projeto da Web: um navegador de hoje precisa continuar exibindo uma página de 1991. Poucas plataformas de software conseguem dizer o mesmo.

3. O modelo cliente-servidor

Praticamente toda a Web funciona sobre um modelo de requisição e resposta entre dois papéis:

  • Cliente: quem pede. Na Web, tipicamente o navegador (Chrome, Firefox, Safari, Edge). Também pode ser um aplicativo de celular, um script, outro servidor.
  • Servidor: quem responde. Um computador executando um software servidor web (Apache, Nginx, Node.js) que fica permanentemente à espera de requisições.
Texto
     CLIENTE                                          SERVIDOR
   ┌───────────┐          1. Requisição HTTP        ┌───────────┐
   │ Navegador │    ────────────────────────────>   │ Servidor  │
   │           │          GET /index.html           │    Web    │
   │           │                                    │           │
   │           │    <────────────────────────────   │           │
   └───────────┘          2. Resposta HTTP          └───────────┘
                          200 OK + HTML

Três características definem esse modelo:

  1. O cliente sempre inicia. O servidor nunca "manda" nada espontaneamente numa conexão HTTP tradicional — ele só responde ao que foi pedido.
  2. HTTP é stateless (sem estado). Cada requisição é independente: o servidor não lembra, por si só, o que aconteceu na requisição anterior. Sessões, logins e carrinhos de compra são construídos por cima disso, com cookies e tokens.
  3. Uma página não é um arquivo, são dezenas. Abrir uma página comum dispara de 30 a 200 requisições: o HTML, cada folha de estilo, cada script, cada imagem, cada fonte.

Front-end e back-end

Front-end Back-end
Onde executa No navegador do usuário No servidor
Tecnologias HTML, CSS, JavaScript Node.js, PHP, Java, Python, C#, Go
Responsabilidade Interface, interação, apresentação Regras de negócio, banco de dados, autenticação
Quem controla O usuário (pode ver e alterar tudo) O dono do sistema

⚠️ Atenção Consequência de segurança, importante desde já: todo código front-end é público. O usuário pode ler seu JavaScript, alterar valores e burlar validações. Validação no cliente é para conforto do usuário; validação no servidor é para segurança. Nunca confie apenas na validação client-side — você vai ver isso na prática na Aula 03 e voltaremos ao tema na Unidade 3.

Nesta trilha trabalhamos exclusivamente com front-end, mas com consciência do sistema completo.

4. O que acontece quando você digita uma URL

Este é um dos assuntos mais cobrados em entrevistas técnicas. Aprenda a sequência.

Passo 1 — Análise da URL. O navegador separa o endereço em partes (§5) e descobre qual protocolo usar e qual servidor contatar.

Passo 2 — Resolução DNS. O nome www.exemplo.com.br não serve para roteamento; a rede trabalha com endereços IP. O navegador consulta o DNS (Domain Name System), um serviço distribuído de diretórios, para traduzir o nome em um IP. Antes de sair para a rede, ele consulta caches, nesta ordem: cache do navegador → cache do sistema operacional → arquivo hosts → servidor DNS do provedor.

Passo 3 — Conexão TCP. Com o IP em mãos, o navegador abre uma conexão TCP com o servidor, normalmente na porta 80 (HTTP) ou 443 (HTTPS), usando o three-way handshake (SYN → SYN-ACK → ACK).

Passo 4 — Handshake TLS (apenas em HTTPS). Cliente e servidor negociam algoritmos de criptografia, o servidor apresenta seu certificado digital e ambos combinam uma chave de sessão. É isso que produz o cadeado na barra de endereço.

Passo 5 — Requisição HTTP. O navegador envia um texto parecido com este:

HTTP
GET /cursos/sistemas HTTP/1.1
Host: www.exemplo.com.br
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/139.0
Accept: text/html,application/xhtml+xml
Accept-Language: pt-BR,pt;q=0.9
Connection: keep-alive

A primeira linha diz o que se quer (GET) e onde (/cursos/sistemas). As demais são cabeçalhos (headers): metadados no formato Nome: valor. Repare que é texto puro, legível — HTTP foi projetado para ser simples.

Passo 6 — Processamento no servidor. O servidor identifica o recurso pedido. Se for um arquivo estático, apenas o lê do disco. Se for dinâmico, executa código (PHP, Node, Java), possivelmente consulta um banco de dados e gera o HTML na hora.

Passo 7 — Resposta HTTP. O servidor devolve outro texto, com uma linha de status, cabeçalhos e, depois de uma linha em branco, o conteúdo:

HTTP
HTTP/1.1 200 OK
Date: Wed, 12 Aug 2030 22:14:05 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 8420
Cache-Control: max-age=3600

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <title>Sistemas de Informação</title>
</head>

Passo 8 — Renderização. O navegador processa a resposta (detalhado na §6) e dispara novas requisições para cada recurso referenciado no HTML: folhas de estilo, scripts, imagens, fontes. Cada uma repete os passos 5 a 7.

🔎 Por baixo do capô Por que tanto cache de DNS? Porque cada consulta que sai para a rede custa dezenas de milissegundos — e uma página faz dezenas de requisições, muitas para domínios diferentes. Sem cache, só a tradução de nomes já deixaria a Web visivelmente lenta. O mesmo raciocínio (guardar o que já foi obtido para não pedir de novo) aparece no cabeçalho Cache-Control da resposta acima, e é o tema do desafio C1 de hoje.

📌 Vale gravar A ordem das etapas (URL → DNS → TCP → TLS → requisição → processamento → resposta → renderização) e o que cada uma faz. Saber que o DNS traduz nome em IP e que HTTP é stateless são perguntas frequentes.

5. Anatomia de uma URL

Texto
https://www.exemplo.com.br:443/cursos/sistemas?campus=sinop&turno=noite#ementa
└─┬─┘   └──────┬─────┘└┬┘└──────┬───────┘└────────┬─────────────┘└──┬──┘
  1            2       3        4                 5                 6
# Elemento Nome Função
1 https Esquema / protocolo Como falar com o servidor
2 www.exemplo.com.br Host / domínio Com quem falar
3 443 Porta Qual serviço na máquina (80 = HTTP, 443 = HTTPS; omitida quando é a padrão)
4 /cursos/sistemas Caminho (path) Qual recurso dentro do servidor
5 ?campus=sinop&turno=noite Query string Parâmetros no formato chave=valor, separados por &
6 #ementa Fragmento / âncora Posição dentro da página. Nunca é enviado ao servidor — é processado só pelo navegador

O fragmento (#) vai ser seu aliado a partir da Aula 02: é ele que faz um link "pular" para uma seção da mesma página. A query string (?) vai aparecer na Aula 03, quando um formulário enviado com GET grava os campos digitados na própria URL.

Caminhos absolutos e relativos

Você vai usar isto em toda aula a partir de agora, em links, imagens, folhas de estilo e scripts:

Notação Significado Exemplo
pagina.html Mesma pasta do arquivo atual contato.html
./pagina.html Idem, de forma explícita ./contato.html
imagens/foto.jpg Subpasta img/logo.png
../pagina.html Uma pasta acima ../index.html
/pagina.html A partir da raiz do site /sobre.html
https://site.com/x Absoluto, outro servidor link externo

O ponto de partida de um caminho relativo é sempre a pasta do arquivo onde o caminho está escrito, não a pasta do projeto. Se paginas/contato.html precisa da imagem img/logo.png que está na raiz, o caminho correto é ../img/logo.png: sobe um nível, entra em img.

⚠️ Atenção Erro clássico: usar C:\Users\Ivan\Documents\site\logo.png no src de uma imagem. Funciona na sua máquina e quebra em todas as outras. Caminhos de sistema de arquivos não existem na Web — o servidor só conhece a pasta do site.

6. Como o navegador renderiza uma página

O navegador não "mostra o HTML". Ele executa um pipeline:

Texto
HTML   ──parsing──>   DOM ┐
                          ├──> Render Tree ──> Layout ──> Paint ──> Composite
CSS   ──parsing──> CSSOM ┘
                            ▲
JavaScript ─────────────────┘ (pode alterar DOM e CSSOM a qualquer momento)
  1. Parsing do HTML → DOM. O navegador lê o HTML caractere a caractere e monta uma árvore de objetos chamada DOM (Document Object Model). Cada tag vira um nó da árvore.
  2. Parsing do CSS → CSSOM. As regras de estilo viram outra árvore, o CSSOM.
  3. Render Tree. DOM + CSSOM são combinados, descartando o que não é visível (por exemplo, o que tem display: none).
  4. Layout (reflow). Cálculo da posição e do tamanho exatos de cada caixa na tela.
  5. Paint. Preenchimento de pixels: cores, textos, bordas, sombras.
  6. Composite. Montagem das camadas na tela.

Cada navegador tem um motor de renderização: Blink (Chrome, Edge, Opera), Gecko (Firefox), WebKit (Safari). Diferenças entre motores são a razão de existirem os padrões — e a razão de os sites às vezes ficarem diferentes em cada navegador.

🔎 Por baixo do capô Por que JavaScript bloqueia o parsing: quando o parser encontra um <script> sem atributos, ele para de montar o DOM, baixa e executa o script, e só então continua. Por isso a boa prática de colocar <script> antes de </body> ou usar o atributo defer. Você vai ver isso de perto na Aula 10, quando o JavaScript entrar em cena.

O DOM é uma das ideias mais importantes do semestre. Guarde desde já: o arquivo .html no disco é texto; o DOM é a árvore em memória que o navegador constrói a partir dele. O DevTools mostra o DOM, não o arquivo — e o JavaScript, na Unidade 3, vai manipular o DOM, não o arquivo.

7. Os três pilares do front-end

Pilar Papel Analogia
HTML Estrutura e significado do conteúdo O esqueleto e os órgãos
CSS Apresentação visual A pele, as roupas, a aparência
JavaScript Comportamento e interatividade Os músculos e o sistema nervoso

A separação entre eles é chamada de separação de responsabilidades (separation of concerns) e é um princípio de engenharia, não capricho: permite trocar o visual sem tocar no conteúdo, reaproveitar estilos entre páginas e manter o código legível por equipes diferentes.

Um mesmo HTML com três CSS distintos vira três sites visualmente diferentes. Esse é o ponto — e é exatamente o que vai acontecer com o site do evento: o HTML que você escreve na Unidade 1 não muda quando o CSS chega na Unidade 2.

Padrões e quem os define

Organização Cuida de
W3C Especificações da Web, com destaque para CSS e acessibilidade
WHATWG HTML e DOM como living standard
TC39 / Ecma International ECMAScript, a especificação da linguagem JavaScript
IETF Protocolos de rede (HTTP, TCP/IP), publicados como RFCs

Nenhuma empresa é dona da Web. Os navegadores implementam especificações públicas, escritas em processo aberto. É por isso que a documentação oficial (MDN, especificações do W3C e da WHATWG) vale mais do que qualquer tutorial — e é para ela que os links de "Para aprofundar" apontam.

8. Arquitetura em camadas

"Camada" (tier ou layer) é uma divisão lógica de responsabilidades. Quanto mais camadas, maior a separação — e maior a complexidade.

Uma camada (monolítica local)

Tudo em um único programa, em uma única máquina. Não é Web — é o modelo de um aplicativo desktop antigo, com interface, regras e dados no mesmo executável.

Duas camadas (cliente-servidor clássico)

Texto
[ Cliente com lógica ]   <──────>   [ Servidor de Banco de Dados ]

O cliente é "gordo" (fat client): contém a interface e as regras de negócio, e conversa diretamente com o banco. Problema: qualquer mudança de regra exige reinstalar o programa em todas as máquinas, e o banco fica exposto na rede.

Três camadas — o padrão da Web

Texto
┌──────────────────┐   HTTP   ┌──────────────────┐   SQL   ┌──────────────┐
│ APRESENTAÇÃO     │ <──────> │    APLICAÇÃO     │ <─────> │    DADOS     │
│ (navegador)      │          │ (servidor web +  │         │   (SGBD)     │
│ HTML/CSS/JS      │          │ regras negócio)  │         │ MySQL,       │
│                  │          │ Node, PHP, Java  │         │ PostgreSQL   │
└──────────────────┘          └──────────────────┘         └──────────────┘
     CLIENTE                        SERVIDOR                  SERVIDOR
Camada Responsabilidade Onde executa
Apresentação Interface, captura de entrada, exibição Navegador (cliente)
Aplicação / Lógica Regras de negócio, validação, autenticação, orquestração Servidor
Dados Armazenamento, integridade, consultas Servidor de banco

Vantagens: cada camada pode ser escalada, atualizada e substituída de forma independente; o banco nunca fica exposto ao cliente; as regras existem em um único lugar.

Esta trilha inteira vive na camada de apresentação. O Nível 2 constrói a camada de aplicação (Node.js + Express) e o Nível 3 integra as três.

N camadas e microsserviços

Sistemas grandes fragmentam ainda mais: camada de cache (Redis), fila de mensagens (RabbitMQ, Kafka), balanceador de carga (Nginx), serviços independentes por domínio de negócio. Ganha-se escalabilidade e autonomia de equipes; paga-se com complexidade operacional e latência de rede. Nada disso é assunto deste nível, mas você vai reconhecer os nomes quando ler uma vaga de emprego.

9. Sites estáticos e dinâmicos

Estático Dinâmico
Como o HTML é obtido Arquivo pronto, lido do disco Gerado a cada requisição por código
Conteúdo Igual para todos Varia por usuário, hora, banco
Velocidade Muito alta Depende do processamento
Custo de hospedagem Baixíssimo (ou grátis) Servidor de aplicação + banco
Segurança Superfície de ataque mínima Exige cuidado (SQL injection, XSS)
Exemplos Portfólio, landing page, documentação Rede social, e-commerce, sistema acadêmico

⚠️ Atenção Confusão frequente: "dinâmico", no sentido de arquitetura, significa HTML gerado no servidor. Uma página estática com muito JavaScript e animações continua sendo estática do ponto de vista arquitetural. O SIGAA é dinâmico. Um portfólio com carrossel animado é estático. O site do evento que vamos construir é estático — e vai ser publicado de graça, por isso mesmo.

10. MPA e SPA

MPA — Multi Page Application. Cada navegação carrega um novo documento HTML completo do servidor. É o modelo tradicional. Vantagens: simples, ótimo para buscadores (SEO), funciona sem JavaScript. Desvantagem: a tela "pisca" a cada clique.

SPA — Single Page Application. O servidor entrega um HTML e um pacote JavaScript. A partir daí, o JavaScript intercepta a navegação, busca apenas os dados (via API, em JSON) e reescreve o DOM. Vantagens: sensação de aplicativo, menos tráfego após a carga inicial. Desvantagens: carga inicial pesada, complexidade, SEO exige trabalho extra, quebra sem JavaScript. Frameworks típicos: React, Vue, Angular, Svelte.

Texto
MPA:   clique ──> servidor ──> HTML completo ──> recarrega a página
SPA:   clique ──> JS intercepta ──> API ──> JSON ──> JS reescreve o DOM

O site do evento é uma MPA: cinco arquivos .html, um por página. É o ponto de partida certo — SPAs são o assunto do Nível 2 (Unidade 2) e do Nível 3 inteiro.

11. APIs, JSON e REST

Uma API (Application Programming Interface) é um contrato pelo qual dois sistemas conversam. Na Web, o estilo dominante é o REST, que usa os próprios verbos do HTTP:

Verbo HTTP Ação Exemplo
GET Ler GET /api/alunos — lista alunos
POST Criar POST /api/alunos — cadastra aluno
PUT / PATCH Atualizar PUT /api/alunos/42
DELETE Remover DELETE /api/alunos/42

Perceba que essas quatro operações são exatamente o CRUD (Create, Read, Update, Delete). Neste nível você vai reconhecer o CRUD nas requisições que o navegador faz; a leitura de dados aparece nas consultas dinâmicas da Aula 14, e a escrita no servidor é assunto do Nível 2.

O formato de troca padrão é o JSON (JavaScript Object Notation) — texto, legível, com pares chave: valor:

JSON
{
  "id": 42,
  "nome": "Maria Silva",
  "curso": "Sistemas de Informação",
  "fase": 2,
  "ativo": true,
  "disciplinas": ["Desenvolvimento Web", "Banco de Dados"]
}

Principais códigos de status HTTP

Faixa Significado Exemplos
1xx Informativo 100 Continue
2xx Sucesso 200 OK, 201 Created, 204 No Content
3xx Redirecionamento 301 Moved Permanently, 304 Not Modified
4xx Erro do cliente 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
5xx Erro do servidor 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

💡 Dica Mnemônico: 4xx é culpa de quem pediu; 5xx é culpa de quem respondeu. Um 404 diz "você pediu algo que não existe"; um 500 diz "eu quebrei ao tentar responder".

🔬 Investigue Abra um site que você usa bastante (rede social, portal de notícias, e-commerce), pressione F12, vá à aba Network e recarregue com Ctrl+F5. Observe: (1) quantas linhas apareceram — cada uma é uma requisição; (2) a coluna Type: document, stylesheet, script, png, font; (3) clique na primeira linha, abra Headers e procure Content-Type e Server na resposta; (4) na barra inferior, leia o total de requisições e o peso transferido. Você acabou de ver o "uma página são dezenas de arquivos" acontecendo — e o cabeçalho Server conta qual software respondeu.

12. Infraestrutura de entrega

  • Servidor web: Apache, Nginx, IIS, Node.js. Recebe requisições e devolve recursos.
  • CDN (Content Delivery Network): rede de servidores espalhados geograficamente que guardam cópias dos arquivos. Um usuário em Sinop recebe o arquivo de um nó em São Paulo, não da Califórnia. Reduz a latência drasticamente. Exemplos: Cloudflare, Akamai.
  • Hospedagem estática: GitHub Pages, Netlify, Vercel, Cloudflare Pages — gratuitas e suficientes para tudo que faremos nesta trilha. O projeto do Marco 3 (Aula 15) é publicado em uma delas.
  • DNS e domínio: registro de domínios .br via Registro.br. Um domínio próprio custa por volta de R$ 40 por ano; a trilha Deploy (capítulo 04) mostra como apontar um para o seu site.

💻 Mão na massa — Pasta do projeto, primeiro HTML e DevTools

Hoje nasce a pasta que você vai usar até o fim da trilha e o primeiro arquivo do site do evento.

Passo 1 — Estrutura de pastas

Crie, no seu computador (em Documentos, por exemplo), a seguinte estrutura. Por enquanto só as pastas e um arquivo:

Texto
introducao-web/
├── site-evento/          ← projeto fio-condutor (construído aula a aula)
│   ├── index.html
│   ├── css/
│   ├── img/
│   └── js/
├── meu-projeto/          ← projeto autoral (tema seu, mesma estrutura)
└── exercicios/
    ├── aula01/
    ├── aula02/
    └── aula03/

⚠️ Atenção Regra permanente: nomes de arquivos e pastas em minúsculas, sem espaços, sem acentos. Use hífen para separar palavras (sobre-nos.html, nunca Sobre Nós.html). Servidores Linux diferenciam maiúsculas de minúsculas — Index.html e index.html são arquivos distintos para eles, e o que funciona no Windows pode quebrar quando você publicar.

Abra o VS Code, vá em File → Open Folder e abra a pasta introducao-web. Sempre abra a pasta raiz, não um arquivo solto: o Live Server e os caminhos relativos dependem disso.

Passo 2 — O primeiro HTML

No painel esquerdo, clique com o botão direito em site-eventoNew Fileindex.html. Digite (não cole):

site-evento/index.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semana Acadêmica de Sistemas de Informação</title>
</head>
<body>
  <h1>Semana Acadêmica de Sistemas de Informação</h1>
  <p>Três dias de palestras, minicursos e oficinas.</p>
  <p>Este site está sendo construído com o WebLab, na trilha <strong>Introdução ao Desenvolvimento Web</strong>.</p>
</body>
</html>

Cada linha será explicada na Aula 02. Por hoje, o essencial: <!DOCTYPE html> diz que é HTML5; <head> guarda informações sobre a página; <body> guarda o que aparece na tela; e <meta charset="UTF-8"> diz ao navegador como ler os acentos.

Passo 3 — Subir o servidor local

Clique com o botão direito no arquivo index.htmlOpen with Live Server (ou clique em Go Live no canto inferior direito). O navegador abre em um endereço parecido com:

Texto
http://127.0.0.1:5500/site-evento/index.html

Leia essa URL com a §5 na cabeça: esquema http, host 127.0.0.1, porta 5500, caminho /site-evento/index.html. Você acabou de subir um servidor web na sua máquina — 127.0.0.1 é o endereço que todo computador usa para se referir a si mesmo (também chamado de localhost). A partir de agora o navegador é o cliente e o Live Server é o servidor, exatamente como no diagrama da §3.

Altere o texto do <p>, salve com Ctrl+S e olhe o navegador: ele recarrega sozinho. É isso que o Live Server faz.

Passo 4 — Experimento obrigatório: o charset

Remova a linha <meta charset="UTF-8">, salve e olhe o navegador. Os acentos viram símbolos estranhos (ç, ã, é). Isso acontece porque o navegador passa a interpretar os bytes com outra tabela de caracteres. Recoloque a linha. Esse é o erro mais comum de iniciante e agora você sabe reconhecê-lo em dois segundos.

Passo 5 — Ferramentas do desenvolvedor (DevTools)

Pressione F12 (ou Ctrl+Shift+I). Você vai usar isto todos os dias do semestre.

Aba Para que serve
Elements Ver e editar o DOM e o CSS aplicado, ao vivo
Console Mensagens de erro e execução de JavaScript
Network Todas as requisições: método, status, tamanho, tempo
Sources Ver arquivos e depurar JavaScript com breakpoints
Application Cookies e localStorage (usaremos na Unidade 3)
Lighthouse Auditoria de desempenho, acessibilidade e SEO

Roteiro de exploração — faça agora, na sua página:

  1. Elements: dê duplo clique no texto do <h1> e altere. Note que muda na tela mas não no arquivo — você está editando o DOM em memória, não o HTML. Recarregue e a alteração some.
  2. Console: digite document.title e pressione Enter. Aparece o texto do seu <title>. Digite document.body.children.length — quantos filhos diretos o <body> tem?
  3. Network: marque Disable cache, recarregue com Ctrl+F5 e observe o status 200 ao lado de index.html. Agora renomeie o arquivo para inicio.html e recarregue: status 404. Renomeie de volta.
  4. Abra em outra aba um site que você visita com frequência e, na aba Network, conte quantas requisições a página faz e qual é o maior arquivo (clique no cabeçalho da coluna Size para ordenar).

Passo 6 — O projeto autoral começa hoje

Copie site-evento/index.html para meu-projeto/index.html e troque o <title>, o <h1> e os parágrafos pelo tema do seu projeto. Abra também com o Live Server. Está feito o primeiro commit mental: a partir de agora, tudo que o site do evento ganha nesta trilha, o seu projeto ganha também.

Como testar

  • O Live Server abre http://127.0.0.1:5500/site-evento/index.html e a página mostra o título e os parágrafos com acentos corretos.
  • Alterar e salvar o arquivo recarrega o navegador sozinho.
  • Na aba Network, index.html aparece com status 200.
  • No Console, document.title devolve "Semana Acadêmica de Sistemas de Informação".
  • meu-projeto/index.html abre com o seu tema.

Resultado esperado: duas páginas simples no ar, servidas pelo Live Server; o DevTools aberto e explorado; a estrutura de pastas pronta para o semestre.

🧪 Laboratório

Nível A — Fixação

A1. Explique, com suas palavras e em no máximo 4 linhas, a diferença entre Internet e World Wide Web.

A2. Classifique cada item como Internet ou Web: (a) protocolo TCP/IP; (b) HTML; (c) e-mail via SMTP; (d) HTTP; (e) cabos de fibra óptica submarinos; (f) URL.

A3. Decomponha a URL abaixo, nomeando cada uma das seis partes:

Texto
https://loja.exemplo.com.br:8443/produtos/notebooks?marca=dell&ordem=preco#avaliacoes

A4. O que significa dizer que o HTTP é stateless? Cite uma consequência prática disso.

A5. Qual a porta padrão do HTTP? E do HTTPS? Por que elas normalmente não aparecem na URL?

A6. Coloque em ordem correta as etapas: (1) handshake TCP; (2) renderização; (3) resolução DNS; (4) resposta HTTP; (5) requisição HTTP; (6) análise da URL.

A7. Associe cada tecnologia ao seu papel: HTML / CSS / JavaScript ↔ comportamento / apresentação / estrutura.

A8. Um arquivo está em site/paginas/contato.html e precisa referenciar site/img/logo.png. Qual o caminho relativo correto?

A9. O que é o DOM? Ele existe no arquivo .html salvo em disco ou apenas em memória?

A10. Cite três organizações que mantêm padrões da Web e diga do que cada uma cuida.

A11. Por que #secao2 na URL não gera requisição ao servidor?

A12. Qual a diferença entre os status HTTP 200 e 404? Cite mais dois códigos e seus significados.

Nível B — Aplicação

B1. Usando a aba Network do DevTools, acesse três sites diferentes (um portal de notícias, um e-commerce e o site da sua universidade ou escola) e registre, para cada um: número de requisições, peso total transferido (KB ou MB), tempo de carregamento (Load, na barra inferior) e qual foi o maior recurso. Escreva um parágrafo comparando os resultados e levantando hipóteses sobre as diferenças.

Resultado esperado: uma tabela com três linhas e quatro medidas cada, mais um parágrafo de análise (por exemplo: "o portal de notícias fez 3× mais requisições por causa de anúncios e rastreadores").

Dica

Marque Disable cache antes de medir, senão a segunda visita vem do cache e distorce a comparação. A barra de resumo na parte inferior da aba Network mostra "N requests | X MB transferred | Load: Y s".

B2. Crie exercicios/aula01/sobre-mim.html contendo: título da página no <title>, um <h1> com seu nome, um parágrafo de apresentação, um parágrafo com seus objetivos nesta trilha e um link para o site de uma universidade ou escola (a sua, se estiver cursando alguma) que abra em nova aba (pesquise o atributo target).

Resultado esperado: a página abre no Live Server com acentos corretos; o link abre o site escolhido em outra aba.

Dica

Parta do index.html que você criou no Mão na massa. O link é algo como <a href="https://www.wikipedia.org" target="_blank">Wikipédia</a>, com o href trocado pelo site escolhido. Na Aula 02 você vai descobrir por que esse link precisa de um atributo extra de segurança.

B3. Desenhe (no papel, no Draw.io ou no Excalidraw) um diagrama do ciclo requisição-resposta incluindo: usuário, navegador, DNS, Internet, servidor web e banco de dados. Indique com setas numeradas a ordem dos eventos.

Resultado esperado: um diagrama com pelo menos 8 setas numeradas, do "usuário digita a URL" ao "navegador renderiza".

Dica

Siga os oito passos da §4. O banco de dados só entra no Passo 6, e só se o site for dinâmico — represente isso.

B4. No Console do DevTools, execute os comandos abaixo em uma página qualquer e registre o resultado de cada um, explicando o que retornaram:

JavaScript
window.location.href
window.location.protocol
window.location.hostname
document.title
navigator.userAgent

Resultado esperado: cinco linhas de saída com a sua explicação; você deve conseguir relacionar protocol e hostname com as partes 1 e 2 da URL da §5.

Dica

window.location é um objeto que representa a URL atual, já decomposta. Experimente também window.location.port e window.location.hash na sua página do Live Server.

B5. Pesquise e escreva um resumo de meia página sobre a diferença entre HTTP/1.1, HTTP/2 e HTTP/3, focando em: multiplexação, cabeçalhos e protocolo de transporte usado.

Resultado esperado: texto de 15 a 25 linhas, com pelo menos uma fonte citada (a MDN serve).

Dica

Comece por "HTTP/2 multiplexação" e "HTTP/3 QUIC" na MDN. Na aba Network, a coluna Protocol (ative-a clicando com o botão direito no cabeçalho das colunas) mostra h2 ou h3 para cada requisição — veja o que os sites que você visita usam.

B6. Abra um site com HTTPS, clique no cadeado e examine o certificado. Registre: quem emitiu, para qual domínio é válido, data de validade e qual o algoritmo de chave. Explique para que serve cada informação.

Resultado esperado: quatro dados anotados e uma explicação de 2 a 3 linhas para cada um.

Dica

No Chrome: cadeado → A conexão é seguraO certificado é válido. O "emissor" é a autoridade certificadora (Let's Encrypt, DigiCert); ela é quem garante ao navegador que o servidor é quem diz ser — é o Passo 4 da §4.

Nível C — Desafio

C1. Investigação de cache. Acesse um mesmo site duas vezes na aba Network: uma com Disable cache marcado e outra desmarcado. Compare o número de requisições e o peso transferido. Identifique quais recursos vieram do cache (procure por (disk cache), (memory cache) ou status 304). Produza um relatório de 1 página explicando o que é cache HTTP, o papel do cabeçalho Cache-Control e por que ele é essencial para a performance da Web.

Dica

Na segunda visita, a coluna Size mostra "(disk cache)" em vez de um tamanho — esses arquivos nem saíram para a rede. O status 304 Not Modified é diferente: a requisição saiu, mas o servidor respondeu "você já tem a versão atual". Clique em um recurso e leia Cache-Control e ETag nos cabeçalhos de resposta.

🏆 Desafios

⭐ Quem entregou esta página?

devtoolshttpinvestigacao

Toda resposta HTTP carrega cabeçalhos que contam quem a produziu — e muitos sites deixam pistas: qual servidor web, se passou por uma CDN, há quanto tempo o arquivo está em cache. Hoje você vira detetive: escolha três sites (o da sua universidade ou escola, um jornal e um e-commerce) e descubra, só pelos cabeçalhos, como cada um é entregue.

Critérios de pronto

  • Para cada site, uma tabela com os valores dos cabeçalhos de resposta do documento HTML principal: Server, Content-Type, Cache-Control e pelo menos um cabeçalho que revele CDN (por exemplo cf-ray, x-served-by, via, x-cache).
  • Uma classificação de cada site em "servido direto" ou "servido via CDN", com a evidência.
  • Uma linha explicando o que o valor de Cache-Control de cada site significa na prática.
Pistas
  1. Aba Network → clique na primeira linha (o documento) → Headers → role até Response Headers.
  2. Cloudflare deixa cf-ray e server: cloudflare; Fastly deixa x-served-by; Akamai costuma deixar x-akamai-* ou server: AkamaiGHost.
  3. Se Server estiver ausente, o site está escondendo o software de propósito — isso também é uma resposta (e uma prática de segurança).
  4. Leia a página da MDN sobre Cache-Control para traduzir max-age, no-cache e public.
⭐⭐

⭐⭐ Linha do tempo da Web, só com HTML

htmlprojeto

A tabela da §2 resume meio século em dez linhas. Ela merece mais: crie uma página HTML com uma linha do tempo de pelo menos 10 marcos da história da Web (1969 até hoje), cada um com ano, título e um parágrafo de 2 a 3 linhas explicando sua importância. Use apenas HTML nesta etapa — o CSS entra a partir da Aula 05 e você vai reaproveitar exatamente esta página para estilizá-la.

Critérios de pronto

  • Arquivo exercicios/aula01/linha-do-tempo.html com a estrutura mínima completa (doctype, lang, charset, viewport, title).
  • Pelo menos 10 marcos, em ordem cronológica, cada um com ano, título e parágrafo.
  • Pelo menos 3 marcos que não estão na tabela da §2 (pesquise: o primeiro navegador gráfico, a criação do Google, o lançamento do iPhone e o efeito na Web móvel, o nascimento do Node.js).
  • Uma fonte citada ao final da página, com link.
Pistas
  1. Você ainda não conhece os elementos de lista e de título — use <h2> para o ano + título e <p> para o texto; na Aula 02 você reescreve com <ol> e <time>.
  2. A Wikipédia em português tem um artigo "História da World Wide Web" com datas confiáveis.
  3. Pense na página como um documento que alguém vai ler de cima a baixo sem nenhum estilo: a ordem e a hierarquia dos títulos precisam contar a história sozinhas.
⭐⭐

⭐⭐ A rota até o servidor

terminaldnsinvestigacao

Escolha um domínio real (o site de uma universidade, um jornal, uma loja). Quantos computadores um pacote atravessa entre a sua máquina e o servidor dele? A resposta está em um comando que existe em todo sistema operacional. No terminal, execute tracert dominio-escolhido (Windows) ou traceroute dominio-escolhido (Linux/macOS). Registre a saída e responda: quantos saltos foram necessários? O que representa cada linha? Qual o tempo total? Relacione o resultado com o conceito de comutação de pacotes.

Critérios de pronto

  • A saída completa do comando, copiada em um bloco de texto.
  • O número de saltos e o tempo aproximado do último salto.
  • Uma explicação, por escrito, do que são as três medidas de tempo em cada linha e por que algumas linhas mostram * * *.
  • Uma comparação com nslookup dominio-escolhido (ou dig): qual IP foi resolvido, e ele bate com o destino do traceroute?
  • Um parágrafo relacionando o resultado com "a Internet é uma rede de comutação de pacotes".
Pistas
  1. No Windows, abra o Prompt de Comando ou o PowerShell; no Linux/macOS, o Terminal. Se traceroute não existir no Linux, instale com sudo apt install traceroute.
  2. Cada linha é um roteador no caminho; as três medidas são três tentativas de ida e volta (RTT). * * * significa que aquele roteador não respondeu ao pacote de teste — não que a rota parou.
  3. Os primeiros saltos são a sua rede local e o seu provedor; os últimos, o data center do destino. Os nomes dos roteadores costumam entregar a cidade (procure siglas como spo, cgb, gru).
  4. Rode duas vezes em horários diferentes e compare os tempos.
⭐⭐⭐

⭐⭐⭐ HTTP à mão

httphttpsterminalinvestigacao

O navegador esconde tudo o que a §4 descreve. Hoje você faz o trabalho dele manualmente: dispara uma requisição HTTP sem navegador, lê a resposta crua e compara protocolos. A ferramenta é o curl, que já vem instalado no Windows 10+, no macOS e na maioria das distribuições Linux. Escolha um site HTTPS (pode ser o mesmo domínio do desafio anterior). Ao final, você vai conseguir explicar para outra pessoa, com evidências, cada etapa do "o que acontece quando digito uma URL".

Critérios de pronto

  • A saída de curl -v https://dominio-escolhido salva em um arquivo, com anotações marcando: a resolução DNS, a conexão TCP, o handshake TLS (algoritmo negociado e emissor do certificado), a requisição enviada (linhas com >) e a resposta recebida (linhas com <).
  • A saída de curl -I https://dominio-escolhido (só cabeçalhos) com uma explicação, em uma linha cada, de pelo menos 5 cabeçalhos de resposta.
  • Uma requisição a um caminho inexistente (curl -I https://dominio-escolhido/nao-existe) e a interpretação do status recebido.
  • Uma comparação entre curl --http1.1 -I e curl --http2 -I no mesmo site: qual versão o servidor aceitou, e como você sabe?
  • Um texto de 10 linhas, escrito como se fosse explicar a outra pessoa, ligando cada evidência ao passo correspondente da §4.
Pistas
  1. curl --version confirma que o comando existe e lista os protocolos suportados (procure HTTP2 e HTTP3 na linha Features).
  2. Em -v, as linhas que começam com * são informações da conexão (DNS, TCP, TLS); > é o que foi enviado; < é o que voltou.
  3. Se o site redirecionar (status 301 ou 302), adicione -L para seguir o redirecionamento e observe as duas respostas.
  4. A primeira linha da resposta (HTTP/2 200 ou HTTP/1.1 200 OK) já denuncia a versão do protocolo negociada.

🐛 Erros comuns

Sintoma Causa Solução
Acentos aparecem como ç, ã, é Falta <meta charset="UTF-8"> ou o arquivo foi salvo em outra codificação Incluir a meta como primeira linha do <head>; no VS Code, conferir "UTF-8" na barra inferior
Link ou imagem funciona no seu computador e quebra no do colega Nome com espaço, acento ou maiúscula, ou caminho de disco (C:\) Nomes em minúsculas, sem acentos, com hífen; caminhos relativos
Navegador exibe o código-fonte como texto puro Arquivo salvo como .txt (às vezes index.html.txt, com a extensão oculta) Conferir a extensão no VS Code; no Windows, ativar "mostrar extensões de arquivo"
Live Server abre uma lista de pastas ou "Cannot GET /" O VS Code foi aberto em um arquivo solto, ou em uma pasta que não contém o index.html File → Open Folder na pasta raiz introducao-web; abrir o Live Server a partir do arquivo
A página não recarrega ao salvar Live Server não está ativo (não aparece "Port: 5500" na barra inferior) Clicar em Go Live; se a porta estiver ocupada, fechar o outro servidor
Edição feita no DevTools sumiu ao recarregar O DevTools altera o DOM em memória, não o arquivo Fazer a alteração no VS Code e salvar
Confundir Internet com Web Erro conceitual: tratar as duas como sinônimos Web é uma aplicação que roda sobre a Internet (§2)
Aba Network vazia ao abrir o DevTools A aba só registra a partir do momento em que é aberta Abrir o DevTools antes e recarregar a página

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulo introdutório. MILETTO, E. M.; BERTAGNOLLI, S. C. Desenvolvimento de software II, capítulo sobre arquiteturas para a Web. Anote duas ideias de cada texto que não apareceram nesta aula.

Parte 2 — Produção (30 min). Produza:

  1. O exercício B2 (sobre-mim.html) e o exercício B5 (resumo sobre HTTP/1.1, 2 e 3).
  2. Uma captura de tela do VS Code com a pasta introducao-web aberta e outra do Live Server exibindo site-evento/index.html, com a URL 127.0.0.1:5500 visível.
  3. O tema do seu projeto autoral, em 3 linhas: o que é, para quem é, e o nome das cinco páginas (a versão "início, programação, inscrição, palestrantes, contato" do seu domínio).

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —, escreva 10 a 15 linhas sobre por que a Web se tornou a plataforma dominante de aplicações — pense em distribuição (não precisa instalar nada), padrões abertos e compatibilidade entre dispositivos. Se puder, compare sua resposta com a de outra pessoa que esteja estudando o mesmo conteúdo.

Critério de pronto: os dois arquivos abrem sem erro de codificação; as duas capturas mostram o ambiente funcionando; o tema tem cinco páginas nomeadas.

Guarde: os arquivos .html e as capturas na pasta exercicios/aula01/ — a partir da Aula 15 tudo isso passa a viver em um repositório Git.

✅ Checkpoint do projeto

Ao fim desta aula, na sua máquina:

  • [ ] Pasta introducao-web/ criada com site-evento/, meu-projeto/ e exercicios/.
  • [ ] site-evento/index.html com a estrutura mínima (doctype, lang="pt-BR", charset, viewport, title, um <h1> e parágrafos).
  • [ ] meu-projeto/index.html com o mesmo esqueleto e o seu tema.
  • [ ] Live Server instalado e abrindo as páginas em http://127.0.0.1:5500/.
  • [ ] Prettier instalado, com format on save ativado.
  • [ ] DevTools explorado: você já editou o DOM na aba Elements, rodou document.title no Console e viu um 200 e um 404 na aba Network.
  • [ ] Tema do projeto autoral escolhido e as cinco páginas nomeadas.

📚 Para aprofundar

Na próxima aula, o HTML entra de verdade: você vai entender cada linha do index.html que criou hoje, aprender os elementos de texto, listas, links e tabelas, e construir a página inicial, a programação e a página de palestrantes do site do evento com HTML semântico e validado no W3C.

Nível 1Unidade 1 · Arquitetura da Web e HTML3 aulas de 50 min + 1 h EAD

Aula 02 — Introdução ao HTML: estrutura, textos, links e tabelas

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Escrever a estrutura mínima de um documento HTML5 válido e explicar a função de cada linha do <head>.
  • Aplicar os elementos de texto (títulos, parágrafos, ênfase, citações, quebras) respeitando a hierarquia e o significado de cada um.
  • Construir listas ordenadas, não ordenadas, de definição e aninhadas.
  • Criar links externos, internos, de âncora, de e-mail, de telefone e de download, com caminhos relativos corretos e atributos de segurança.
  • Marcar tabelas de dados com caption, thead/tbody/tfoot, th scope e mesclagem de células.
  • Usar HTML semântico (header, nav, main, section, article, aside, footer) em vez de div genérica.
  • Validar páginas no validador do W3C e corrigir os erros apontados.

📋 Pré-requisitos

  • [ ] Pasta introducao-web/ da Aula 01 com site-evento/index.html abrindo no Live Server.
  • [ ] VS Code com Live Server e Prettier instalados e format on save ativado.
  • [ ] Navegador com DevTools — você vai usar a aba Elements o tempo todo hoje.
  • [ ] Tema do projeto autoral definido (entregue na atividade assíncrona da Aula 01).

Na aula passada você viu como a Web funciona por fora: cliente e servidor, URL, HTTP, o caminho de uma requisição até a renderização — e criou um index.html mínimo sem entender cada linha. Hoje você entende cada linha, aprende os elementos de texto, listas, links e tabelas, e constrói três das cinco páginas do site do evento com HTML semântico e validado.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Anatomia do elemento, aninhamento, estrutura do documento, elementos de texto
2 50 min Listas, links (caminhos, âncoras, segurança), tabelas
3 50 min HTML semântico, entidades, validação no W3C; Mão na massa: início, programação e palestrantes do site do evento

1. O que é HTML

HTML é uma linguagem de marcação, não de programação: ela não tem variáveis, condicionais ou laços. Sua função é descrever a estrutura e o significado do conteúdo por meio de marcas (tags). Você não diz ao navegador "desenhe um retângulo azul aqui"; você diz "isto é um título", "isto é um parágrafo", "isto é uma lista de três itens" — e o navegador (ou o leitor de tela, ou o buscador) decide o que fazer com essa informação.

Essa é a diferença entre HTML e um editor de texto: no Word você formata a aparência; no HTML você declara o que cada coisa é. A aparência é assunto do CSS, a partir da Aula 05.

Anatomia de um elemento

Texto
<a href="https://github.io" title="Site oficial">GitHub</a>
└┬┘└──────────┬───────────┘ └───────┬───────┘└──┬──┘└─┬─┘
 │            │                     │           │     │
nome      atributo 1            atributo 2   conteúdo tag
da tag                                                de fechamento
└───────────────── tag de abertura ─────────────┘
  • Elemento = tag de abertura + conteúdo + tag de fechamento.
  • Atributo = informação adicional, sempre na tag de abertura, no formato nome="valor". Use sempre aspas duplas.
  • Elementos vazios não têm conteúdo nem fechamento: <br>, <hr>, <img>, <input>, <meta>, <link>.

Os nomes de tags e atributos não diferenciam maiúsculas de minúsculas, mas a convenção universal é tudo em minúsculas. Siga-a.

Aninhamento

Elementos podem conter outros, mas precisam ser fechados na ordem inversa da abertura — como parênteses em matemática:

HTML
<!-- CORRETO -->
<p>Texto com <strong>destaque <em>duplo</em></strong> aqui.</p>

<!-- ERRADO: fechamento cruzado -->
<p>Texto com <strong>destaque <em>duplo</strong></em> aqui.</p>

O aninhamento é o que transforma o HTML em uma árvore — a mesma árvore que você viu na Aula 01 com o nome de DOM. <p> é pai de <strong>, que é pai de <em>. Todo elemento tem exatamente um pai (exceto <html>, a raiz) e pode ter vários filhos.

🧠 Você sabia? O navegador nunca se recusa a exibir um HTML "errado". A especificação da WHATWG descreve, passo a passo, como o parser deve se recuperar de cada tipo de erro: uma tag cruzada é reorganizada, um </li> esquecido é inserido, um <p> aberto dentro de outro <p> fecha o anterior. É por isso que páginas quebradas "funcionam" — e por isso que o resultado às vezes não é o que você escreveu. Aliás, pela especificação, fechar <li>, <p>, <td> e <tr> é opcional; nesta trilha você fecha tudo, sempre, porque código previsível é mais fácil de ler, depurar e manter.

2. Estrutura mínima de um documento

exercicios/aula02/modelo.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Descrição da página em até 160 caracteres.">
  <meta name="author" content="Seu Nome">
  <title>Título que aparece na aba do navegador</title>
  <link rel="stylesheet" href="css/estilo.css">
</head>
<body>
  <!-- Todo conteúdo visível fica aqui -->
  <script src="js/script.js"></script>
</body>
</html>
Linha Função
<!DOCTYPE html> Declara HTML5. Sem ela, o navegador entra em quirks mode e emula bugs de navegadores antigos
lang="pt-BR" Idioma do conteúdo. Essencial para leitores de tela (pronúncia), tradução automática e buscadores
<meta charset="UTF-8"> Codificação de caracteres. Sempre a primeira linha do <head> — você viu na Aula 01 o que acontece sem ela
viewport Faz a página se adaptar a telas de celular. Sem ela, não há responsividade (Aula 08)
description Texto que aparece nos resultados de busca, abaixo do título
<title> Título da aba, dos favoritos e do resultado de busca. Obrigatório
<link rel="stylesheet"> Liga uma folha de estilo externa (Aula 05). Pode omitir enquanto não houver CSS
<script src> Liga um arquivo JavaScript (Aula 10). Fica no fim do <body> para não bloquear a renderização

O <head> é invisível: nada dele aparece na tela. Ele carrega metadados — informações sobre a página para o navegador, os buscadores e as redes sociais. Tudo que o usuário vê fica dentro de <body>.

🔎 Por baixo do capô O quirks mode existe por causa da história: nos anos 1990, cada navegador interpretava CSS de um jeito, e milhões de páginas foram escritas em cima desses bugs. Quando os padrões foram corrigidos, os navegadores precisaram de um jeito de saber se a página era "antiga" (renderiza com os bugs) ou "moderna" (renderiza pelo padrão). O sinal é o <!DOCTYPE>. Sem ele, o navegador assume o pior — e o seu CSS da Unidade 2 vai se comportar de forma estranha sem motivo aparente.

3. Elementos de texto

Títulos

HTML
<h1>Título principal — apenas um por página</h1>
  <h2>Seção</h2>
    <h3>Subseção</h3>
      <h4>Sub-subseção</h4>

⚠️ Atenção Hierarquia de títulos não é escolha de tamanho de fonte — é estrutura de documento. Leitores de tela permitem navegar pulando de título em título, e o Google usa essa hierarquia para entender a página. Nunca pule níveis (h1 direto para h4) e nunca escolha <h3> só porque "ficou menor". Tamanho é assunto do CSS.

Pense nos títulos como o sumário de um livro: <h1> é o título do livro (um só), <h2> são os capítulos, <h3> as seções dentro de cada capítulo. Se você conseguir extrair só os títulos da página e eles formarem um sumário coerente, a hierarquia está certa.

Parágrafos e semântica inline

HTML
<p>Um parágrafo comum de texto.</p>

<p>
  Este é <strong>importante</strong> e este tem <em>ênfase</em>.
  Este é apenas <b>negrito visual</b> e este é <i>itálico visual</i>.
  <mark>Texto realçado</mark>, <small>letra miúda</small>,
  <del>removido</del> e <ins>inserido</ins>.
  Fórmula: H<sub>2</sub>O e potência: x<sup>2</sup>.
  Código: <code>let x = 10;</code>.
  Atalho: <kbd>Ctrl</kbd> + <kbd>S</kbd>.
</p>
Par de tags Diferença
<strong> × <b> strong = importância semântica (o leitor de tela pode enfatizar). b = negrito puramente visual (palavras-chave, nome de produto)
<em> × <i> em = ênfase semântica (muda o sentido da frase). i = itálico visual (termo estrangeiro, nome científico, pensamento)

Na prática: use strong e em; deixe b e i para casos específicos. Uma regra simples: se você leria a palavra em voz alta com outro tom, é em; se a informação é importante mesmo lida em tom neutro, é strong.

📌 Vale gravar A diferença entre <strong>/<b> e <em>/<i> é pergunta clássica. Resposta curta: os dois pares têm a mesma aparência padrão, mas só strong e em carregam significado — e significado é o que leitores de tela e buscadores usam.

Citações e quebras

HTML
<blockquote cite="https://www.w3.org/">
  <p>A força da Web está em sua universalidade.</p>
  <footer><cite>Tim Berners-Lee</cite></footer>
</blockquote>

<p>Endereço:<br>Av. dos Ingás, 3001<br>Sinop — MT</p>

<hr>

<pre>
Texto pré-formatado:
    preserva     espaços
    e quebras de linha.
</pre>
  • <blockquote> é uma citação em bloco; o atributo cite guarda a URL da fonte (não aparece na tela). <cite> marca o título da obra ou autor citado.
  • <q> é a versão inline: <p>Ele disse <q>volto já</q> e sumiu.</p> — o navegador coloca as aspas.
  • <br> quebra a linha dentro de um bloco; <hr> é uma quebra temática (mudança de assunto), não uma "linha decorativa".
  • <pre> preserva espaços e quebras exatamente como digitados — útil para código e diagramas.

⚠️ Atenção <br> serve para quebras dentro de um bloco de conteúdo (endereços, poemas, letras de música). Usar <br><br> para separar parágrafos é erro — use <p>. O espaçamento entre parágrafos é assunto do CSS.

4. Listas

Três tipos, cada um com um significado:

HTML
<!-- Lista não ordenada: a ordem dos itens não importa -->
<ul>
  <li>HTML</li>
  <li>CSS</li>
  <li>JavaScript</li>
</ul>

<!-- Lista ordenada: a ordem importa (passos, ranking) -->
<ol>
  <li>Analisar a URL</li>
  <li>Resolver o DNS</li>
  <li>Abrir conexão TCP</li>
</ol>

<!-- Com atributos: numeração romana, começando em 3, decrescente -->
<ol type="I" start="3" reversed>
  <li>Terceiro item</li>
  <li>Segundo item</li>
  <li>Primeiro item</li>
</ol>

<!-- Lista de definições: pares termo → descrição -->
<dl>
  <dt>HTML</dt>
  <dd>Linguagem de marcação que estrutura o conteúdo.</dd>
  <dt>CSS</dt>
  <dd>Linguagem de estilo que define a apresentação.</dd>
</dl>

<!-- Listas aninhadas: o <ul> filho vai DENTRO do <li> pai -->
<ul>
  <li>Front-end
    <ul>
      <li>HTML</li>
      <li>CSS</li>
    </ul>
  </li>
  <li>Back-end</li>
</ul>

A pergunta para escolher entre <ul> e <ol> é: se eu embaralhar os itens, a informação muda? Passos de uma receita, sim → <ol>. Ingredientes, não → <ul>. A lista de definições (<dl>) serve para glossários, metadados ("Autor: Fulano", "Duração: 2 h") e perguntas frequentes.

Um <ul> ou <ol> só pode ter <li> como filhos diretos. Qualquer outra coisa — um <h3>, um <p>, outra lista — vai dentro de um <li>. O validador do W3C avisa quando você erra isso.

O link é a invenção que dá o "hipertexto" ao HTML. Todos os tipos usam o mesmo elemento, <a>, e mudam só o href:

HTML
<a href="https://www.wikipedia.org">Link externo</a>
<a href="contato.html">Link interno relativo</a>
<a href="/sobre.html">Link a partir da raiz</a>
<a href="#secao3">Âncora interna da própria página</a>
<a href="programacao.html#dia-2">Âncora em outra página</a>
<a href="documentos/edital.pdf" download>Baixar edital</a>
<a href="mailto:contato@exemplo.com.br">Enviar e-mail</a>
<a href="tel:+556635111000">Ligar</a>
<a href="https://exemplo.com" target="_blank" rel="noopener noreferrer">
  Abrir em nova aba
</a>

Os caminhos relativos seguem exatamente a tabela da §5 da Aula 01: contato.html é "na mesma pasta", ../index.html é "uma pasta acima", img/logo.png é "na subpasta img".

⚠️ Atenção Segurança: ao usar target="_blank", sempre inclua rel="noopener noreferrer". Sem isso, a página aberta ganha acesso parcial à sua janela original (window.opener) e pode redirecioná-la — um vetor de phishing conhecido como tabnabbing. Navegadores modernos já aplicam noopener por padrão, mas o atributo explícito garante o comportamento em todos.

Âncoras internas

Para a âncora funcionar, o destino precisa de um id — e id deve ser único na página:

HTML
<a href="#metodologia">Ir para Metodologia</a>

<h2 id="metodologia">Metodologia</h2>
<p>O evento combina palestras e oficinas práticas.</p>

Ao clicar, o navegador rola até o elemento e acrescenta #metodologia à URL — lembre-se da Aula 01: o fragmento nunca é enviado ao servidor. Um link href="#a02-topo" para um id="a02-topo" no cabeçalho dá o clássico "voltar ao topo".

Textos de link ruins: "clique aqui", "saiba mais", "link". Um usuário de leitor de tela pode navegar por uma lista só de links — "clique aqui" ali não significa nada. Buscadores também usam o texto do link para entender o destino. Use texto descritivo: "Baixe o edital em PDF", "Veja a programação completa".

6. Tabelas

Tabelas são para dados tabulares — informação que faz sentido em linhas e colunas, como notas, horários, preços. Jamais para layout: isso quebra em celular, confunde leitores de tela e é assunto do CSS (Aula 07).

HTML
<table>
  <caption>Notas da turma</caption>
  <thead>
    <tr>
      <th scope="col">Aluno</th>
      <th scope="col">A1</th>
      <th scope="col">A2</th>
      <th scope="col">A3</th>
      <th scope="col">Média</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Maria Silva</th>
      <td>8,0</td><td>7,5</td><td>9,0</td><td>8,2</td>
    </tr>
    <tr>
      <th scope="row">João Souza</th>
      <td>6,0</td><td>5,5</td><td>7,0</td><td>6,2</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Média da turma</th>
      <td>7,0</td><td>6,5</td><td>8,0</td><td>7,2</td>
    </tr>
  </tfoot>
</table>
Elemento Função
<caption> Título/legenda da tabela — o leitor de tela anuncia antes de entrar nela
<thead>, <tbody>, <tfoot> Agrupamento semântico de linhas: cabeçalho, corpo, rodapé (totais)
<tr> Linha (table row)
<th> Célula de cabeçalho
<td> Célula de dado
scope="col" / scope="row" Informa a leitores de tela a qual eixo o cabeçalho pertence

Mesclagem de células

HTML
<td colspan="3">Ocupa 3 colunas</td>
<td rowspan="2">Ocupa 2 linhas</td>

colspan estende a célula para a direita; rowspan, para baixo. Cuidado com a contagem: se uma linha tem colspan="3", as linhas seguintes precisam continuar somando o mesmo número de colunas, senão a tabela fica torta e o validador reclama.

Sem CSS, a tabela aparece sem bordas — parece "só texto alinhado". É normal. A Aula 06 coloca as bordas; hoje o que importa é a estrutura estar certa.

7. HTML semântico

Antes do HTML5, tudo era <div>. O resultado era a chamada div soup: código ilegível, inacessível e ruim para buscadores. O HTML5 trouxe elementos que dizem o que a região é:

HTML
<body>
  <header>
    <h1>Nome do site</h1>
    <nav>
      <ul>
        <li><a href="index.html">Início</a></li>
        <li><a href="sobre.html">Sobre</a></li>
        <li><a href="contato.html">Contato</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <article>
      <header>
        <h2>Título do artigo</h2>
        <p>Publicado em <time datetime="2030-10-14">14 de outubro</time></p>
      </header>
      <section>
        <h3>Primeira seção</h3>
        <p>Texto da primeira seção do artigo.</p>
      </section>
      <footer>Autor: Maria Silva</footer>
    </article>

    <aside>
      <h3>Conteúdo relacionado</h3>
      <p>Links, publicidade, biografia do autor.</p>
    </aside>
  </main>

  <footer>
    <p>&copy; Nome do site — Todos os direitos reservados</p>
  </footer>
</body>
Elemento Quando usar
<header> Cabeçalho do site ou de um artigo/seção
<nav> Bloco de navegação principal
<main> Conteúdo principal e único da página. Apenas um por página
<article> Conteúdo autocontido que faria sentido isolado (post, notícia, produto, palestrante)
<section> Agrupamento temático com título próprio
<aside> Conteúdo tangencial (barra lateral, box relacionado)
<footer> Rodapé do site ou de um artigo
<figure> / <figcaption> Ilustração com legenda (detalhado na Aula 04)
<time datetime=""> Data/hora legível por máquina
<div> Só quando não existe elemento semântico adequado — agrupamento puramente visual

Repare que <header> e <footer> podem aparecer mais de uma vez: um para o site, outro dentro de cada <article>. Só <main> é único.

💡 Dica Teste da semântica: se você trocar todo o seu CSS por um arquivo vazio, a página ainda faz sentido lida de cima a baixo? Se sim, sua semântica está boa. É exatamente o que um leitor de tela e um buscador "veem".

Para decidir entre <section> e <article>: pergunte se o trecho faria sentido publicado sozinho, em outro site. Um palestrante com nome, bio e foto, sim → <article>. A seção "Sobre o evento" da página inicial só faz sentido dentro dela → <section>.

🔬 Investigue Crie exercicios/aula02/minimo.html contendo a linha <p>Oi — sem doctype, sem <html>, sem <body>, sem fechar o <p>. Abra no Live Server e vá à aba Elements do DevTools. O navegador construiu <html>, <head>, <body> e fechou o <p> sozinho: você está vendo o DOM, não o arquivo. Agora acrescente uma <table> com um <tr> direto dentro (sem <tbody>) e olhe de novo: o <tbody> aparece no DOM sem você ter escrito. Quando o JavaScript da Unidade 3 percorrer a árvore, é essa árvore que ele vai encontrar.

Entidades HTML

Alguns caracteres têm significado especial no HTML e precisam ser escapados:

Caractere Entidade
< &lt;
> &gt;
& &amp;
" &quot;
espaço fixo (não separável) &nbsp;
© &copy;
® &reg;
(travessão) &mdash;

Com <meta charset="UTF-8">, acentos, © e podem ser digitados diretamente. As entidades que você precisa usar são as três primeiras — sem elas, o navegador acha que < abre uma tag e & inicia uma entidade.

8. Validação no W3C

O validador (https://validator.w3.org/) é o corretor ortográfico do HTML. Ele compara o seu código com a especificação e lista cada erro com a linha exata. Três formas de usar:

  1. Validate by URI — para sites já publicados.
  2. Validate by File Upload — envie o arquivo .html.
  3. Validate by Direct Input — cole o código. É a forma mais rápida durante a aula.

A meta é sempre a mesma mensagem: "Document checking completed. No errors or warnings to show."

Mensagem do validador O que significa
Stray end tag “p”. Um </p> sem <p> correspondente — geralmente um fechamento duplicado ou cruzado
Unclosed element “ul”. Faltou </ul>
End tag “strong” violates nesting rules. Fechamento cruzado (§1)
Element “ul” not allowed as child of element “ol” in this context. Lista aninhada fora do <li> (§4)
Element “h2” not allowed as child of element “ul”. <li> pode ser filho direto de lista
Duplicate ID “topo”. Dois elementos com o mesmo id — âncoras vão quebrar
Consider adding a “lang” attribute to the “html” start tag Faltou lang="pt-BR" (aviso, mas corrija)
A table row was N columns wide and exceeded the column count colspan/rowspan desbalanceado (§6)

Erros são listados na ordem em que ocorrem, e um erro costuma causar vários: uma tag não fechada no início gera dezenas de mensagens depois. Corrija o primeiro, revalide, repita.

💻 Mão na massa — Início, programação e palestrantes do site do evento

Hoje o index.html da Aula 01 vira uma página de verdade, e nascem programacao.html e palestrantes.html. As páginas inscricao.html e contato.html ganham só o esqueleto — elas são o assunto da Aula 03.

Passo 1 — O <head> completo da página inicial

Abra site-evento/index.html e substitua o <head>:

site-evento/index.html (trecho: <head>)

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Semana Acadêmica de Sistemas de Informação: três dias de palestras, minicursos e oficinas para estudantes e profissionais de tecnologia.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Início — Semana Acadêmica de Sistemas de Informação</title>
</head>

O <title> segue o padrão "Página — Nome do site": é assim que ele aparece na aba e no histórico.

Passo 2 — Cabeçalho e navegação

O cabeçalho é idêntico nas cinco páginas — é o que faz o site parecer um site. Escreva uma vez, com capricho, e copie nas outras.

site-evento/index.html (trecho: início do <body>)

HTML
<body>
  <header id="topo">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <p>Três noites de outubro · Auditório Central</p>
    <nav>
      <ul>
        <li><a href="index.html">Início</a></li>
        <li><a href="programacao.html">Programação</a></li>
        <li><a href="inscricao.html">Inscrição</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
      </ul>
    </nav>
  </header>

Os cinco href são caminhos relativos: todos os arquivos estão na mesma pasta. O id="a02-topo" vai servir para os links "voltar ao topo".

Passo 3 — Conteúdo principal em seções

site-evento/index.html (trecho: <main>)

HTML
  <main>
    <p>Nesta página:</p>
    <ul>
      <li><a href="#sobre">Sobre o evento</a></li>
      <li><a href="#como-participar">Como participar</a></li>
      <li><a href="#glossario">Glossário</a></li>
    </ul>

    <section id="sobre">
      <h2>Sobre o evento</h2>
      <p>
        A Semana Acadêmica de Sistemas de Informação reúne estudantes, professores e
        profissionais de tecnologia da região norte de Mato Grosso em três dias de
        <strong>palestras, minicursos e oficinas práticas</strong>. O evento é
        <em>gratuito</em> e aberto à comunidade.
      </p>
      <p>
        Nesta edição, o tema central é <q>Desenvolvimento web para problemas reais</q>:
        cada atividade parte de um caso concreto — do agronegócio à saúde pública —
        e mostra como HTML, CSS e JavaScript resolvem parte dele.
      </p>
      <p>Emitimos certificado de <strong>20 horas</strong> para quem participar de pelo menos 75% das atividades.</p>
      <p><a href="#topo">Voltar ao topo</a></p>
    </section>

    <section id="como-participar">
      <h2>Como participar</h2>
      <ol>
        <li>Leia a <a href="programacao.html">programação</a> e escolha as atividades.</li>
        <li>Preencha o <a href="inscricao.html">formulário de inscrição</a>.</li>
        <li>Confira o e-mail de confirmação (verifique a caixa de spam).</li>
        <li>No primeiro dia, faça o credenciamento no saguão do auditório a partir das 18h30.</li>
        <li>Participe, assine a lista de presença e receba o certificado por e-mail.</li>
      </ol>
      <p><a href="#topo">Voltar ao topo</a></p>
    </section>

    <section id="glossario">
      <h2>Glossário</h2>
      <dl>
        <dt>Palestra</dt>
        <dd>Apresentação expositiva de 50 minutos, com perguntas ao final.</dd>
        <dt>Minicurso</dt>
        <dd>Atividade prática de 3 horas, com computador, para até 30 participantes.</dd>
        <dt>Oficina</dt>
        <dd>Atividade curta (90 minutos) em que os participantes constroem algo juntos.</dd>
        <dt>Mesa-redonda</dt>
        <dd>Conversa entre três ou quatro convidados mediada por um professor do curso.</dd>
        <dt>Credenciamento</dt>
        <dd>Confirmação de presença e retirada do crachá no primeiro dia.</dd>
      </dl>
      <p><a href="#topo">Voltar ao topo</a></p>
    </section>

    <blockquote cite="https://www.w3.org/">
      <p>A força da Web está em sua universalidade. O acesso por todos, independentemente de deficiência, é um aspecto essencial.</p>
      <footer><cite>Tim Berners-Lee</cite>, criador da Web</footer>
    </blockquote>
  </main>

Cada <section> tem um <h2> — regra de ouro: seção sem título provavelmente deveria ser um <div>. Os id dos <section> alimentam a lista "Nesta página" e as âncoras funcionam sem uma linha de CSS ou JavaScript.

Passo 4 — Rodapé

site-evento/index.html (trecho: <footer> e fechamento)

HTML
  <footer>
    <p>Realização: Comissão Organizadora da Semana Acadêmica de Sistemas de Informação.</p>
    <p>
      <a href="mailto:contato@semanasi.com.br">contato@semanasi.com.br</a> ·
      <a href="tel:+556635111000">(66) 3511-1000</a>
    </p>
    <p>&copy; Semana Acadêmica de Sistemas de Informação. Todos os direitos reservados.</p>
  </footer>
</body>
</html>

Salve e olhe o Live Server: uma página sem estilo, mas com estrutura clara. Clique nos links "Nesta página" e "Voltar ao topo" e observe o # na URL.

Passo 5 — A página de programação

Crie site-evento/programacao.html. Copie o <head> (trocando o <title> e a description) e o <header> inteiro do index.html. O <main> é uma tabela:

site-evento/programacao.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Programação completa da Semana Acadêmica de Sistemas de Informação: horários, atividades e locais dos três dias.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Programação — Semana Acadêmica de Sistemas de Informação</title>
</head>
<body>
  <header id="topo">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <p>Três noites de outubro · Auditório Central</p>
    <nav>
      <ul>
        <li><a href="index.html">Início</a></li>
        <li><a href="programacao.html">Programação</a></li>
        <li><a href="inscricao.html">Inscrição</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h2>Programação</h2>
    <p>Todas as atividades acontecem no período noturno. Minicursos exigem inscrição prévia e têm vagas limitadas.</p>

    <table>
      <caption>Programação dos três dias do evento</caption>
      <thead>
        <tr>
          <th scope="col">Horário</th>
          <th scope="col">Atividade</th>
          <th scope="col">Responsável</th>
          <th scope="col">Local</th>
        </tr>
      </thead>
      <tbody id="dia-1">
        <tr>
          <th colspan="4" scope="colgroup">Dia 1</th>
        </tr>
        <tr>
          <th scope="row"><time datetime="18:30">18h30</time></th>
          <td>Credenciamento</td>
          <td>Comissão organizadora</td>
          <td>Saguão</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="19:00">19h00</time></th>
          <td>Abertura e palestra magna: o futuro do desenvolvimento web</td>
          <td>Eduarda Ribeiro</td>
          <td>Auditório Central</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="20:00">20h00</time></th>
          <td>Minicurso: Git e GitHub do zero</td>
          <td>Diego Nascimento</td>
          <td>Laboratório 2</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="21:00">21h00</time></th>
          <td>Dashboards que os produtores realmente usam</td>
          <td>Bruno Takahashi</td>
          <td>Auditório Central</td>
        </tr>
      </tbody>
      <tbody id="dia-2">
        <tr>
          <th colspan="4" scope="colgroup">Dia 2</th>
        </tr>
        <tr>
          <th scope="row"><time datetime="19:00">19h00</time></th>
          <td>Minicurso: acessibilidade na prática</td>
          <td>Diego Nascimento</td>
          <td>Laboratório 1</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="19:00">19h00</time></th>
          <td>Minicurso: primeiros passos com redes neurais</td>
          <td>Ana Lúcia Ferreira</td>
          <td>Laboratório 3</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="20:30">20h30</time></th>
          <td>Segurança em aplicações web: dez erros comuns</td>
          <td>Carla Mendes</td>
          <td>Auditório Central</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="21:30">21h30</time></th>
          <td colspan="3">Confraternização com coffee break — saguão</td>
        </tr>
      </tbody>
      <tbody id="dia-3">
        <tr>
          <th colspan="4" scope="colgroup">Dia 3</th>
        </tr>
        <tr>
          <th scope="row"><time datetime="18:30">18h30</time></th>
          <td>Maratona de programação</td>
          <td>Eduarda Ribeiro</td>
          <td>Laboratórios 1 e 2</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="19:00">19h00</time></th>
          <td>Minicurso: phishing e engenharia social</td>
          <td>Carla Mendes</td>
          <td>Laboratório 3</td>
        </tr>
        <tr>
          <th scope="row"><time datetime="22:00">22h00</time></th>
          <td>Encerramento e premiação</td>
          <td>Eduarda Ribeiro</td>
          <td>Auditório Central</td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td colspan="4">Programação sujeita a alterações. Consulte esta página na véspera de cada dia.</td>
        </tr>
      </tfoot>
    </table>

    <p><a href="#topo">Voltar ao topo</a></p>
  </main>

  <footer>
    <p>Realização: Comissão Organizadora da Semana Acadêmica de Sistemas de Informação.</p>
    <p>
      <a href="mailto:contato@semanasi.com.br">contato@semanasi.com.br</a> ·
      <a href="tel:+556635111000">(66) 3511-1000</a>
    </p>
    <p>&copy; Semana Acadêmica de Sistemas de Informação. Todos os direitos reservados.</p>
  </footer>
</body>
</html>

Três detalhes importantes nessa tabela:

  • Um <tbody> por dia, cada um com id. Isso permite links diretos como programacao.html#dia-2 a partir de outras páginas.
  • A linha de título de cada dia usa <th colspan="4">: uma célula ocupa as quatro colunas. Note que a linha da confraternização tem <th> + <td colspan="3"> — somando 4 de novo.
  • O <tfoot> carrega a observação final. Sem CSS ele aparece no fim, onde deve.

Passo 6 — A página de palestrantes

site-evento/palestrantes.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Conheça os palestrantes e ministrantes da Semana Acadêmica de Sistemas de Informação.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Palestrantes — Semana Acadêmica de Sistemas de Informação</title>
</head>
<body>
  <header id="topo">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <p>Três noites de outubro · Auditório Central</p>
    <nav>
      <ul>
        <li><a href="index.html">Início</a></li>
        <li><a href="programacao.html">Programação</a></li>
        <li><a href="inscricao.html">Inscrição</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h2>Palestrantes e ministrantes</h2>
    <p>Convidados desta edição, em ordem alfabética. Os demais entram na Aula 04, quando a página ganhar fotos.</p>

    <article id="ana-lucia-ferreira">
      <h3>Ana Lúcia Ferreira</h3>
      <p><strong>Professora e pesquisadora</strong> na área de inteligência artificial.</p>
      <p>Pesquisa redes neurais aplicadas à previsão de safra de soja e coordena um grupo de estudos em aprendizado de máquina.</p>
      <dl>
        <dt>Atividades</dt>
        <dd><a href="programacao.html#dia-2">Minicurso: primeiros passos com redes neurais</a> (dia 2)</dd>
        <dt>Contato</dt>
        <dd><a href="https://www.linkedin.com/" target="_blank" rel="noopener noreferrer">Perfil no LinkedIn</a></dd>
      </dl>
    </article>

    <article id="bruno-takahashi">
      <h3>Bruno Takahashi</h3>
      <p><strong>Cientista de dados</strong> na startup AgroData, especialista em visualização de dados.</p>
      <p>Constrói painéis que produtores rurais realmente usam no dia a dia e escreve sobre dados abertos e cidades inteligentes.</p>
      <dl>
        <dt>Atividades</dt>
        <dd><a href="programacao.html#dia-1">Dashboards que os produtores realmente usam</a> (dia 1)</dd>
        <dd><a href="programacao.html#dia-3">Dados abertos e cidades inteligentes</a> (dia 3)</dd>
        <dt>Contato</dt>
        <dd><a href="https://github.com/" target="_blank" rel="noopener noreferrer">Perfil no GitHub</a></dd>
      </dl>
    </article>

    <article id="carla-mendes">
      <h3>Carla Mendes</h3>
      <p><strong>Pesquisadora em segurança da informação</strong> na UFMT.</p>
      <p>Estuda a relação entre experiência de uso e ataques de engenharia social, e treina equipes de desenvolvimento em segurança.</p>
      <dl>
        <dt>Atividades</dt>
        <dd><a href="programacao.html#dia-2">Segurança em aplicações web: dez erros comuns</a> (dia 2)</dd>
        <dd><a href="programacao.html#dia-3">Minicurso: phishing e engenharia social</a> (dia 3)</dd>
        <dt>Contato</dt>
        <dd><a href="mailto:carla@exemplo.com">carla@exemplo.com</a></dd>
      </dl>
    </article>

    <p><a href="#topo">Voltar ao topo</a></p>
  </main>

  <footer>
    <p>Realização: Comissão Organizadora da Semana Acadêmica de Sistemas de Informação.</p>
    <p>
      <a href="mailto:contato@semanasi.com.br">contato@semanasi.com.br</a> ·
      <a href="tel:+556635111000">(66) 3511-1000</a>
    </p>
    <p>&copy; Semana Acadêmica de Sistemas de Informação. Todos os direitos reservados.</p>
  </footer>
</body>
</html>

Cada palestrante é um <article>: faria sentido sozinho em outro site. Os títulos são <h3> porque estão abaixo do <h2> "Palestrantes e ministrantes" — hierarquia respeitada. Os links para programacao.html#dia-2 combinam caminho relativo com fragmento.

Passo 7 — Esqueleto das páginas restantes

Para o menu não levar a um 404, crie inscricao.html e contato.html com o <head>, o <header> e o <footer> copiados, e um <main> mínimo:

site-evento/inscricao.html (trecho: <main>)

HTML
  <main>
    <h2>Inscrição</h2>
    <p>O formulário de inscrição será construído na próxima aula.</p>
  </main>

Faça o mesmo em contato.html, trocando o título para "Contato". Ajuste o <title> e a description de cada uma.

Passo 8 — Validar as cinco páginas

Abra https://validator.w3.org/#validate_by_input, cole o código de cada página e corrija até ver "No errors or warnings to show". Faça isso nas cinco. É a última etapa de toda aula da Unidade 1.

Como testar

  • As cinco páginas abrem no Live Server e o menu leva de uma para outra sem 404.
  • Em index.html, os links "Nesta página" rolam até a seção e a URL ganha #sobre, #como-participar, #glossario.
  • Em palestrantes.html, clicar em "Minicurso: primeiros passos com redes neurais" abre programacao.html já posicionada no Dia 2.
  • Na aba Elements, a tabela mostra três <tbody> e um <tfoot>; cada <tr> soma 4 colunas.
  • O validador não acusa nenhum erro em nenhuma das cinco páginas.

Resultado esperado: um site de cinco páginas, sem estilo, navegável, com hierarquia de títulos correta, tabela de programação estruturada e zero erros no W3C. Ele parece "feio" — e vai continuar assim até a Aula 05. O que importa agora é a estrutura.

🧪 Laboratório

Nível A — Fixação

Os itens A1 a A6 revisam a arquitetura da Aula 01; os demais são sobre o HTML de hoje.

A1. Diferencie as camadas de apresentação, aplicação e dados, dizendo onde cada uma executa.

A2. Um blog cujo conteúdo vem de um banco de dados MySQL é estático ou dinâmico? Justifique.

A3. Explique a diferença fundamental entre MPA e SPA em uma frase para cada.

A4. Qual verbo HTTP você usaria para: (a) listar produtos; (b) cadastrar um cliente; (c) excluir um pedido; (d) atualizar o preço de um item?

A5. Classifique cada código como erro de cliente ou de servidor: 403, 500, 404, 502, 400, 503.

A6. O que acontece com uma página HTML que não declara <!DOCTYPE html>?

A7. Corrija os erros do trecho abaixo (há pelo menos quatro):

HTML
<p>Texto <strong>importante <em>e enfático</strong></em>.
<a href=programacao.html>Programação</a>
<ul>
  <li>Um
  <li>Dois
</ul>

A8. Qual a diferença semântica entre <strong> e <b>? E entre <em> e <i>?

A9. Escreva o HTML de uma lista não ordenada com 3 itens, sendo que o segundo contém uma sublista ordenada com 2 itens.

A10. Escreva um link que abra https://www.wikipedia.org em nova aba, com os atributos de segurança corretos.

A11. O que faz o atributo scope em um <th>?

A12. Escreva o HTML de uma célula que ocupe 3 colunas e outra que ocupe 2 linhas.

A13. Para cada situação, indique o elemento semântico mais adequado: (a) menu principal; (b) uma notícia completa; (c) barra lateral de links relacionados; (d) rodapé com copyright; (e) imagem com legenda; (f) conteúdo principal da página.

A14. Por que <main> deve aparecer apenas uma vez por página?

A15. Escreva as entidades HTML para: <, >, &, © e espaço não separável.

A16. O que é um elemento vazio? Cite quatro.

Nível B — Aplicação

B1. Crie exercicios/aula02/curriculo.html — um currículo pessoal completo usando apenas HTML semântico, sem nenhum CSS. Deve conter: dados pessoais no <header>; seções de formação, experiência, habilidades e projetos; listas apropriadas em cada uma (ordenada onde a cronologia importa); uma tabela de idiomas com nível; links para e-mail, telefone, LinkedIn e GitHub; <footer>.

Resultado esperado: validador do W3C com zero erros; a hierarquia h1h2 sem pulos; os links mailto: e tel: funcionando.

Dica

Use a página de palestrantes como modelo: <header> com seu nome no <h1>, <main> com uma <section> por bloco do currículo, <dl> para pares como "Período / Instituição". Para a tabela de idiomas, <th scope="row"> no nome do idioma.

B2. Escreva o HTML (sem CSS) de uma página de notícia com: cabeçalho do site, navegação, artigo com título, subtítulo, data em <time>, autor, cinco parágrafos, uma citação em destaque, uma seção de "leia também" em <aside> e rodapé do artigo com as tags do assunto.

Resultado esperado: um <article> com <header> e <footer> próprios, além do <header> e <footer> do site; o <aside> fora do <article>; zero erros no validador.

Dica

O exemplo da §7 é quase esse esqueleto. O subtítulo da notícia é um <p> dentro do <header> do artigo, não um <h2> — subtítulo jornalístico é descrição, não seção. As tags do assunto cabem em uma <ul> dentro do <footer> do artigo.

B3. Reproduza em HTML a tabela com a sequência das 15 aulas desta trilha (está na Aula 01), usando <caption>, <thead>, <tbody> e <th scope> corretamente. Mescle células nas linhas que fecham unidade (06, 10, 15) para indicar "Marco do projeto".

Resultado esperado: uma tabela de 15 linhas de dados, três <th scope="row"> de marco mesclados com colspan, e cada linha somando o mesmo número de colunas.

Dica

Comece pela tabela de programação do Mão na massa: um <tbody> por unidade deixa a estrutura mais clara. Nas linhas de marco, se a tabela tem 3 colunas e você quer "Aula" + "Marco do projeto", use <th scope="row"> + <td colspan="2">.

B4. Pegue o trecho abaixo (div soup) e reescreva-o inteiramente com HTML5 semântico:

HTML
<div id="topo">
  <div class="titulo">Blog do Curso</div>
  <div class="menu"><div><a href="#">Home</a></div><div><a href="#">Posts</a></div></div>
</div>
<div id="conteudo">
  <div class="post">
    <div class="titulo-post">Como estudar programação</div>
    <div class="data">14 de outubro</div>
    <div class="texto">Estudar programação exige prática diária e projetos pequenos que cresçam aos poucos.</div>
  </div>
</div>
<div id="rodape">Copyright Semana Acadêmica de Sistemas de Informação</div>

Resultado esperado: zero <div> no resultado; <header>, <nav> com <ul>, <main>, <article> com <h2> e <time datetime>, <footer> com &copy;.

Dica

Cada class do original está gritando o nome do elemento semântico: titulo<h1>, menu<nav>, post<article>, data<time>. Só o # dos links deve virar um caminho real.

B5. Construa um índice navegável: uma página longa com 6 seções, cada uma com id, e um menu no topo com links de âncora para cada seção. Ao final de cada seção, inclua um link "voltar ao topo".

Resultado esperado: clicar em cada item do menu rola até a seção certa e a URL muda para #id-da-secao; "voltar ao topo" funciona em todas.

Dica

Cada seção precisa de conteúdo suficiente para a página rolar — copie três parágrafos da §1 desta aula em cada uma. Os id não podem ter espaços nem acentos e não podem se repetir.

B6. Compare arquiteturas: escolha três sistemas que você usa (por exemplo SIGAA, Instagram e um blog pessoal) e produza uma tabela HTML classificando cada um quanto a: estático/dinâmico, MPA/SPA e quantas camadas você supõe existirem. Justifique cada classificação com uma evidência observável (comportamento ao navegar, aba Network).

Resultado esperado: uma <table> com <caption>, <thead> e três linhas de dados, seguida de um parágrafo de justificativa por sistema.

Dica

Na aba Network, filtre por Doc: uma MPA carrega um novo documento HTML a cada clique; uma SPA carrega um documento no início e depois só Fetch/XHR com JSON.

B7. Use o validador do W3C em três sites reais e registre quantos erros e avisos cada um apresenta. Escolha um erro de cada site, explique o que significa e como corrigir.

Resultado esperado: uma lista com site, número de erros, número de avisos, o texto literal de um erro e a correção proposta.

Dica

Use Validate by URI. Sites grandes costumam ter dezenas de erros — isso não os impede de funcionar (lembre-se do "Você sabia?" da §1), mas mostra por que o validador é um padrão de qualidade e não uma obrigação técnica.

B8. Página institucional de um curso. Construa exercicios/aula02/curso.html, uma página sobre um curso técnico ou de graduação — o seu, se você estiver cursando um, ou o curso de Sistemas de Informação de uma universidade à sua escolha, como no exemplo a seguir —, contendo obrigatoriamente: estrutura completa do documento com todas as <meta> vistas; <header> com <h1> e <nav> com 4 links de âncora interna; <main> com três <section>, cada uma com <h2> e conteúdo; uma lista ordenada com as etapas do ciclo requisição-resposta (Aula 01); uma lista de definições com 5 termos técnicos da Aula 01; uma tabela com a grade de uma fase do curso (disciplina, carga horária, professor); um <blockquote> com citação e <cite>; <footer> com direitos autorais usando &copy;.

Resultado esperado: zero erros no validador; os 4 links de âncora funcionando; a tabela com <caption>, <thead> e <th scope>.

Dica

É a mesma receita do index.html do evento, com outro conteúdo. Se travar em algum elemento, procure-o na página do site do evento e copie a estrutura.

Nível C — Desafio

C1. Site institucional de 4 páginas. Construa, em exercicios/aula02/curso/, um site sobre um curso técnico ou de graduação — o seu, ou o curso de Sistemas de Informação de uma universidade pública à sua escolha (a grade curricular costuma estar disponível no site da instituição) — com quatro páginas interligadas: index.html (apresentação), grade.html (tabela com a grade curricular completa), corpo-docente.html (lista de professores com formação) e contato.html (endereço, telefone, e-mail e mapa em link). Requisitos: navegação idêntica em todas as páginas; caminhos relativos corretos; HTML5 semântico; zero erros no validador; nomes de arquivos em minúsculas sem acentos. Sem CSS ainda — este site será estilizado nas Aulas 05 e 06.

Dica

Faça primeiro o <header> com o <nav> de 4 links e só depois as páginas — copie o cabeçalho pronto em cada uma. Para a grade curricular, um <tbody> por fase, como na programação do evento. O "mapa em link" é um <a> para o Google Maps com target="_blank" e rel.

🏆 Desafios

⭐ Caça ao bug: oito erros escondidos

htmlbuginvestigacao

O trecho abaixo "funciona" — abre no navegador e mostra tudo. Mesmo assim, ele tem pelo menos oito problemas: alguns o validador do W3C encontra, outros só um olho humano treinado percebe. Encontre todos, classifique-os e corrija.

HTML
<html>
<head>
  <title>Semana de SI</title>
  <meta charset="UTF-8">
</head>
<body>
  <h1>Semana Acadêmica</h1>
  <h1>Programação</h1>
  <h4>Dia 1</h4>
  <p>Palestra de abertura com <strong>convidada <em>especial</strong></em>.</p>
  <p>Para se inscrever, <a href="inscricao.html" target="_blank">clique aqui</a>.<br><br>
  As vagas são limitadas.</p>
  <ul>
    <li>Minicursos</li>
    <ul>
      <li>HTML e CSS</li>
    </ul>
    <li>Oficinas</li>
  </ul>
  <table>
    <tr><td>Horário</td><td>Atividade</td></tr>
    <tr><td>19h</td><td colspan="3">Abertura</td></tr>
  </table>
</body>
</html>

Critérios de pronto

  • Uma lista numerada com cada problema, a linha em que está e uma classificação: "o validador encontra" ou "só um humano encontra".
  • Pelo menos oito problemas listados (há mais — quem achar dez merece destaque especial).
  • A versão corrigida, validada com zero erros e zero avisos.
  • Uma frase explicando por que os problemas que o validador não encontra são, ainda assim, problemas.
Pistas
  1. Cole o código como está no validador e leia as mensagens — elas resolvem a primeira metade.
  2. Releia a tabela de "Erros comuns" desta aula: cada linha dela é um candidato.
  3. Pense em: quantos <h1>? Que nível vem depois de <h1>? O <ul> filho está dentro de um <li>? O target="_blank" está acompanhado? O texto do link diz para onde vai? A tabela tem cabeçalhos de verdade? E o que falta na primeira linha do arquivo?
  4. Conte as colunas da tabela linha a linha.

⭐ Sem mouse, sem CSS

acessibilidadehtmlinvestigacao

Boa parte das pessoas que usam a Web não usa mouse: quem navega por teclado, quem usa leitor de tela, quem tem lesão por esforço repetitivo. E buscadores não "veem" CSS nenhum. Hoje você vai usar o site do evento (e um site real) do jeito que eles usam — e descobrir se a estrutura que você escreveu se sustenta sozinha.

Critérios de pronto

  • No index.html do site do evento, navegue só com o teclado (Tab, Shift+Tab, Enter): registre a ordem em que os links recebem foco e se algum ficou inalcançável.
  • No Firefox, abra um site real (o da sua universidade ou escola, ou um jornal), desligue os estilos (Exibir → Estilo da página → Sem estilo) e responda: a página ainda faz sentido lida de cima a baixo? Onde a ordem do conteúdo surpreendeu você?
  • Um parágrafo comparando as duas experiências e apontando uma melhoria concreta que você faria no site real, com o elemento HTML que usaria.
  • No site do evento, uma melhoria implementada a partir do que você observou. Sugestão: acrescente id="a02-conteudo" ao <main> das cinco páginas (ele ainda não tem) e, como primeiro elemento do <body>, um link <a href="#a02-conteudo">Pular para o conteúdo</a> — sem CSS ele fica visível o tempo todo, e está tudo bem: escondê-lo até receber foco é assunto da Aula 07.
Pistas
  1. A ordem do foco segue a ordem do HTML — não há como mudá-la sem reordenar o código (e é assim que deve ser).
  2. No Chrome não há menu "sem estilo", mas a extensão Web Developer (Chris Pederick) tem CSS → Disable All Styles.
  3. Leitores de tela oferecem uma lista só de títulos e outra só de links. Imagine essa lista para a sua página: os textos de link fazem sentido fora de contexto?
  4. O link "pular para o conteúdo" é o primeiro elemento do <body> em quase todo site bem feito — procure-o com Tab em .
⭐⭐

⭐⭐ Documentação técnica dos status HTTP

htmlhttpprojeto

A tabela de status HTTP da Aula 01 tem cinco linhas. Um desenvolvedor consulta esses códigos toda semana — e a maioria dos sites de referência é em inglês. Produza http-referencia.html: uma página que documente os códigos de status HTTP com uma tabela por faixa (1xx a 5xx), com pelo menos 4 códigos por faixa, contendo código, nome em inglês, descrição em português e um exemplo de quando ocorre. Inclua um índice com âncoras internas para cada faixa.

Critérios de pronto

  • Cinco <section>, uma por faixa, cada uma com <h2> e id, e um índice no topo com âncoras para as cinco.
  • Cinco tabelas com <caption>, <thead>, <th scope="col"> e pelo menos 4 linhas de dados cada (20 códigos no total).
  • Pelo menos um código por faixa que não aparece na Aula 01 (por exemplo 418, 429, 451, 307, 206).
  • Um link "voltar ao índice" ao fim de cada seção.
  • Zero erros no validador.
Pistas
  1. A referência completa está em https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Status — cada código tem uma página própria com exemplos.
  2. O <th scope="row"> cabe bem no código numérico: ele é o "nome" da linha.
  3. Escreva os exemplos com casos que você já viu: 404 ao renomear o index.html na Aula 01, 304 no desafio de cache.
  4. Esta página vai ganhar CSS na Aula 06 — mantenha-a na pasta exercicios/.
⭐⭐

⭐⭐ Análise arquitetural de um sistema real

investigacaodevtoolshttp

Você aprendeu camadas, MPA/SPA e APIs em teoria. Agora aplique em um sistema que você usa todo dia: escolha um (SIGAA, um e-commerce, uma rede social, um app de banco no navegador) e produza um relatório de 2 páginas com: diagrama da arquitetura provável (camadas e componentes), evidências obtidas na aba Network (formatos de resposta, chamadas de API visíveis, uso de CDN identificável pelos domínios), classificação MPA/SPA justificada, e uma proposta fundamentada de como você reorganizaria a arquitetura se precisasse suportar 10× mais usuários.

Critérios de pronto

  • Um diagrama (papel, Draw.io ou Excalidraw) com pelo menos três camadas e os componentes que você identificou (CDN, API, servidor de aplicação, banco).
  • Pelo menos cinco evidências da aba Network, cada uma com o que foi observado (URL, Content-Type, domínio) e o que ela indica.
  • A classificação MPA ou SPA com a evidência decisiva (documento novo a cada clique, ou JSON via Fetch/XHR).
  • A proposta para 10× mais usuários citando pelo menos duas técnicas da §8 e da §12 da Aula 01 (CDN, cache, separação de camadas, balanceamento).
Pistas
  1. Na aba Network, o filtro Fetch/XHR mostra as chamadas de API; o filtro Doc mostra os documentos HTML. A proporção entre eles já classifica MPA/SPA.
  2. Clique com o botão direito no cabeçalho das colunas e ative Domain: domínios como cdn., static., cloudfront.net, akamaihd.net denunciam CDN.
  3. Respostas com Content-Type: application/json são a camada de aplicação falando; text/html é a camada de apresentação sendo entregue.
  4. Para "10× mais usuários", pense no que é caro (gerar HTML dinâmico, consultar banco) e no que é barato (servir arquivo estático do cache).
⭐⭐⭐

⭐⭐⭐ Reproduza um artigo da Wikipédia só com HTML

htmlacessibilidadeprojeto

A Wikipédia é um dos sites mais bem estruturados da Web: sumário com âncoras, infobox em tabela, seções hierárquicas, notas de rodapé com links de ida e volta. Escolha um artigo em português (sugestão: "HTML" ou "Tim Berners-Lee") e reproduza a sua estrutura integralmente em HTML semântico, sem uma linha de CSS. Ao final, compare a sua versão com o artigo real de estilos desligados: se ficaram parecidos, você entendeu HTML.

Critérios de pronto

  • Arquivo único exercicios/aula02/wikipedia.html com título, introdução, sumário (<nav> com <ol> de âncoras, incluindo subitens aninhados), pelo menos 6 seções (<h2>) e 4 subseções (<h3>).
  • A infobox reproduzida como <table> com <caption> e <th scope="row"> em cada linha.
  • Pelo menos 5 referências: no texto, um link <a href="#a02-ref-1"><sup>[1]</sup></a>; na seção "Referências", uma <ol> com id="a02-ref-1" em cada item e um link de volta ao ponto de citação.
  • Um <blockquote> com cite e pelo menos uma <figure> com <figcaption> (pode usar uma imagem de https://commons.wikimedia.org/ com alt — a Aula 04 aprofunda).
  • Zero erros no validador; navegação completa por teclado; o sumário funcionando.
  • Uma comparação escrita (10 linhas) entre a sua página e o artigo real com estilos desligados no Firefox.
Pistas
  1. Abra o artigo real, pressione Ctrl+U e estude o código-fonte — ele é enorme, mas procure <h2>, <table class="infobox" e <ol class="references".
  2. Para o link de volta na referência, o ponto de citação precisa de um id também: <sup id="a02-cite-1"> e, na referência, <a href="#a02-cite-1">↑</a>.
  3. Sumário aninhado: o <ol> das subseções vai dentro do <li> da seção — o mesmo padrão da §4.
  4. Faça em etapas: estrutura de títulos → sumário → tabela → referências. Valide a cada etapa; um erro de aninhamento no início gera dezenas de mensagens depois.

🐛 Erros comuns

Sintoma Causa Solução
Validador: End tag “strong” violates nesting rules. Fechamento cruzado (<strong><em></strong></em>) Fechar na ordem inversa da abertura
Validador: Element “ul” not allowed as child of element “ul”. Lista aninhada colocada fora do <li> O <ul> filho vai dentro do <li> pai
Validador: Duplicate ID “topo”. e a âncora leva ao lugar errado Dois elementos com o mesmo id id único por página; use nomes descritivos
Página cheia de <h1> "porque ficou bonito" Título usado como tamanho de fonte Um <h1> por página; hierarquia sequencial; tamanho é CSS
Parágrafos "colados" ou separados com <br><br> <br> usado como espaçamento Um <p> por parágrafo; espaçamento é CSS
Link abre em nova aba, mas o validador ou o Lighthouse avisa sobre noopener target="_blank" sem rel Acrescentar rel="noopener noreferrer"
Leitor de tela lê "clique aqui, clique aqui, clique aqui" Texto de link sem significado Texto que descreve o destino
A tabela fica torta, com célula sobrando à direita colspan/rowspan desbalanceado Cada linha deve somar o mesmo número de colunas
Âncora #secao não rola até o lugar O id de destino não existe, tem grafia diferente ou está com acento Copiar o id exato; sem espaços nem acentos
Página inteira de <div> Falta de elementos semânticos header, nav, main, section, article, aside, footer
& sozinho no texto vira algo estranho ou gera aviso & não escapado &amp;

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulos sobre estrutura do documento e elementos de texto. TERUEL, E. C. HTML5 — Guia Prático, capítulo introdutório. Anote um elemento HTML que apareceu na leitura e não nesta aula, e pesquise o que ele faz na MDN.

Parte 2 — Produção (30 min). Produza o exercício B1 (currículo em HTML semântico), em arquivo .html, acompanhado de captura de tela do validador W3C mostrando "Document checking completed. No errors or warnings to show."

No projeto autoral: replique no seu meu-projeto/ o que o site do evento ganhou hoje — index.html completo (cabeçalho, navegação com as cinco páginas, três seções com <h2>, uma lista ordenada, uma lista de definições, um <blockquote>, rodapé), mais uma página com uma tabela de dados do seu domínio e uma página com <article> por item (o equivalente aos palestrantes).

Parte 3 — Registro (10 min). Escreva, no seu repositório ou no fórum da turma (se houver), uma nota "Semântica importa": encontre um site real com problemas de semântica (uso excessivo de div, títulos fora de ordem, links "clique aqui") e descreva três problemas encontrados com a correção proposta para cada. Se puder, compare com o que outra pessoa encontrou.

Critério de pronto: o currículo e as três páginas do projeto autoral passam no validador com zero erros; a navegação do projeto autoral não leva a nenhum 404.

Guarde: curriculo.html, a captura do validador e a pasta meu-projeto/ — a partir da Aula 15 tudo isso vai para um repositório Git.

✅ Checkpoint do projeto

Ao fim desta aula, em site-evento/:

  • [ ] index.html com <head> completo (charset, viewport, description, author, title) e <header> com <h1> e <nav> de cinco links.
  • [ ] index.html com três <section> (#sobre, #como-participar, #glossario), uma <ol>, uma <dl>, um <blockquote> com <cite> e um <footer> com mailto:, tel: e &copy;.
  • [ ] programacao.html com tabela completa: <caption>, <thead>, três <tbody> com id, <tfoot>, th scope e colspan.
  • [ ] palestrantes.html com um <article> por palestrante, <h3> abaixo do <h2>, <dl> de atividades e links para programacao.html#dia-N.
  • [ ] inscricao.html e contato.html com cabeçalho, navegação e rodapé idênticos e <main> mínimo.
  • [ ] As cinco páginas com zero erros no validador do W3C.
  • [ ] Em meu-projeto/: o mesmo avanço com o seu tema (feito na atividade assíncrona).

📚 Para aprofundar

Na próxima aula, a página de inscrição deixa de ser um esqueleto: você vai aprender o elemento <form>, os tipos de <input>, rótulos, agrupamento, validação nativa e a diferença entre GET e POST — e vai ver, na aba Network, os dados do formulário viajando até o servidor.

Nível 1Unidade 1 · Arquitetura da Web e HTML3 aulas de 50 min + 1 h EAD

Aula 03 — Introdução aos formulários

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Explicar o que acontece entre o clique em "Enviar" e a chegada dos dados ao servidor, identificando cada par nome=valor na aba Network do DevTools.
  • Decidir entre GET e POST justificando a escolha pelo tipo de operação, pelo tamanho dos dados e pelos riscos de cada método.
  • Montar um <form> completo com action, method e os campos adequados a cada dado coletado.
  • Escolher o type de <input> certo para cada informação e explicar o ganho prático de cada escolha, principalmente no celular.
  • Associar rótulos a campos com <label> (por for/id e na forma envolvente) e agrupar campos relacionados com <fieldset>/<legend>.
  • Usar <select>, <optgroup>, <datalist> e <textarea>, sabendo quando cada um é a escolha certa.
  • Aplicar a validação nativa do HTML (required, minlength, pattern, min, max, step) e explicar por que ela nunca substitui a validação no servidor.

📋 Pré-requisitos

  • [ ] Pasta introducao-web/site-evento/ com as cinco páginas da Aula 02 (index.html, programacao.html, inscricao.html, palestrantes.html, contato.html), todas validadas no W3C.
  • [ ] VS Code com Live Server e Prettier, e o navegador com DevTools (F12). Hoje você vai passar boa parte da aula na aba Network.
  • [ ] Revisar da Aula 02: caminhos relativos, hierarquia de títulos, <nav> como lista e o hábito de validar cada página no W3C.
  • [ ] Pasta meu-projeto/ com o tema autoral definido e as páginas equivalentes já criadas.

Na aula passada você aprendeu a marcar conteúdo: textos, listas, links e tabelas, e construiu três das cinco páginas do site do evento com HTML semântico e validado. As páginas inscricao.html e contato.html ficaram só com o esqueleto, esperando esta aula. Hoje elas ganham o elemento que transforma um site em sistema: o formulário — a porta por onde os dados das pessoas entram no seu programa.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Por que formulários importam; o elemento <form>; GET × POST; os dados na aba Network
2 50 min <input> e seus tipos; atributos essenciais; <label>; <select>, <datalist>, <textarea>, radio e checkbox; <fieldset>
3 50 min Botões; validação nativa; checklist de acessibilidade; Mão na massa: a página de inscrição completa

1. Por que formulários importam

1.1 A porta de entrada de todo sistema

Pense em qualquer sistema web que você usa: SIGAA, internet banking, loja on-line, rede social, portal da prefeitura. Todos têm uma coisa em comum — em algum momento alguém digitou alguma coisa. Login, matrícula, transferência, pedido, comentário, denúncia: tudo entra por um formulário.

Isso faz do formulário o elemento com maior consequência de todo o HTML. Uma tabela mal marcada deixa o site feio para um leitor de tela. Um formulário mal construído:

  • Gera dado sujo. Um campo de telefone sem restrição recebe (66) 9 9999-9999, 66999999999, 9999-9999 e me liga no zap. Quem sofre é o banco de dados, o relatório e a pessoa que vai tentar ligar.
  • Afasta usuários. Cada campo desnecessário, cada teclado errado no celular, cada erro sem explicação é um motivo para desistir. Formulário longo demais é a principal causa de carrinho abandonado no comércio eletrônico.
  • Exclui pessoas. Um campo sem <label> é, para quem usa leitor de tela, apenas "campo de edição, em branco". Sem rótulo, não há como saber o que digitar.
  • Cria retrabalho. Todo dado que não foi validado na entrada vira, depois, uma planilha de correção manual.

Nesta aula você constrói a estrutura. Na Aula 04 fecha o ciclo com campos avançados e upload de arquivos. Na Aula 06 estiliza o formulário com CSS. Na Aula 14 acrescenta validação em JavaScript, com expressões regulares e mensagens acessíveis. O formulário de inscrição do evento vai atravessar toda a trilha.

1.2 O que acontece quando você clica em "Enviar"

Você já viu na Aula 01 o caminho de uma requisição HTTP. O formulário usa exatamente esse caminho, com um detalhe novo: ele monta a requisição para você.

Texto
1. Você preenche os campos e clica no botão de envio.
2. O navegador percorre o formulário e monta uma lista de pares nome=valor,
   usando o atributo name de cada campo preenchido.
3. Se method="get": a lista vira a query string da URL (depois do ?).
   Se method="post": a lista vira o corpo (body) da requisição.
4. O navegador envia a requisição para o endereço do atributo action.
5. O servidor lê os pares, faz o que tem de fazer e devolve uma resposta.
6. O navegador exibe a resposta — normalmente uma página de confirmação.

Repare no passo 2: é o name que vira a chave. Não é o id, não é o texto do <label>, não é o placeholder. Um campo sem name simplesmente não existe do ponto de vista do servidor. Guarde isso; é o erro número um da lista de Erros comuns desta aula.

🧠 Você sabia? Formulários existem na Web desde 1993 — são mais antigos que o CSS, que o JavaScript e que a maioria das pessoas que os usam. Eles apareceram no HTML 2.0 e praticamente não mudaram de sintaxe desde então: um <form> escrito em 1995 ainda funciona hoje, em qualquer navegador. Essa estabilidade tem um nome na Web: compatibilidade retroativa. É por isso que uma página antiga não "para de funcionar" — a plataforma quase nunca remove nada, ela só acrescenta. O preço dessa escolha é conviver com atributos estranhos como enctype, cujo nome só faz sentido quando você conhece a história. O benefício é que o que você aprende hoje continua valendo daqui a dez anos.

2. O elemento <form>

Todo formulário começa e termina no elemento <form>. Ele não desenha nada na tela: é o envelope que agrupa os campos, define para onde os dados vão e como viajam.

exemplos/form-minimo.html (trecho)

HTML
<form action="/processa-cadastro" method="post">
  <label for="nome">Nome completo</label>
  <input type="text" id="nome" name="nome" required>

  <button type="submit">Enviar</button>
</form>

Três linhas de conteúdo, e já temos um sistema: um rótulo, um campo e um botão que dispara a requisição.

2.1 Atributos do <form>

Atributo Função
action URL que vai receber os dados. Ausente ou vazio: a própria página
method get (padrão) ou post
enctype Como os dados são codificados; use multipart/form-data para upload (Aula 04)
novalidate Desliga a validação nativa do navegador (útil ao testar validação em JS)
autocomplete on (padrão) ou off — permite ou bloqueia o preenchimento automático
target Onde exibir a resposta: _self (padrão) ou _blank (nova aba)
name Nome do formulário; usado por JavaScript e por scripts antigos

Nesta trilha você ainda não tem um servidor para receber os dados — isso é assunto do Nível 2. Por enquanto, o action aponta para um endereço fictício (/inscrever), e o que interessa é observar a requisição sendo montada no DevTools. O servidor do Live Server vai responder 404 Not Found, e está tudo bem: os dados foram enviados, e é isso que vamos inspecionar.

2.2 GET × POST

Essa é a decisão mais importante do <form> — e a que mais confunde quem está começando.

Aspecto GET POST
Onde vão os dados Na URL, como query string No corpo da requisição
Visível na barra de endereço Sim Não
Limite prático de tamanho Sim (cerca de 2.000 caracteres) Praticamente ilimitado
Fica no histórico e nos favoritos Sim Não
Pode ser recarregado sem efeito colateral Sim Não (o navegador pergunta antes)
Uso típico Busca, filtros, paginação Cadastro, login, envio de dados

A regra prática: GET para ler, POST para alterar.

Se a operação apenas consulta informação e poderia ser repetida cem vezes sem consequência, use GET. É por isso que a URL de uma busca é compartilhável: programacao.html?dia=2&trilha=web descreve exatamente o que você está vendo, e mandar esse link para outra pessoa mostra a ela a mesma tela. Essa propriedade — repetir sem mudar nada — chama-se idempotência.

Se a operação cria, altera ou apaga alguma coisa, use POST. Uma inscrição feita duas vezes por engano vira duas inscrições. Por isso o navegador avisa antes de reenviar um POST quando você aperta F5.

Como fica a URL de um GET. Um formulário com method="get", action="busca.html" e um campo name="q" preenchido com html leva o navegador para:

Texto
busca.html?q=html

Com mais campos, os pares são separados por &:

Texto
programacao.html?dia=2&trilha=web&ordem=horario

Caracteres especiais são codificados: um espaço vira + ou %20, o @ vira %40, o acento de programação vira programa%C3%A7%C3%A3o. Esse processo é a codificação de URL (percent-encoding), e o navegador faz tudo sozinho.

⚠️ Atenção Nunca envie senha por GET. Ela ficaria visível na barra de endereço, no histórico do navegador, no cache, nos registros (logs) do servidor e no cabeçalho Referer enviado a sites de terceiros. Já houve vazamento de milhões de senhas exatamente assim. Formulário de login é sempre method="post".

🔎 Por baixo do capô POST não é "seguro" no sentido de escondido: os dados vão no corpo da requisição, em texto puro, e qualquer pessoa com acesso à rede pode lê-los se a conexão for http://. O que protege de verdade é o HTTPS, que cifra a requisição inteira — URL, cabeçalhos e corpo. POST protege dos olhos de quem está do seu lado vendo a tela e do histórico do navegador; o HTTPS protege do resto do mundo. Você vai ver os detalhes do certificado e do cadeado na trilha Deploy; por ora, guarde a distinção entre não aparecer na URL e estar cifrado.

2.3 Vendo os dados viajarem

Este é o experimento central da aula. Faça uma vez acompanhando o texto e depois refaça sozinho, sem consultar.

🔬 Investigue Crie um arquivo exemplos/get-vs-post.html com dois formulários idênticos — um com method="get" e outro com method="post", ambos com action="recebe.html" e dois campos (name="usuario" e name="mensagem"). Abra com o Live Server, abra o DevTools na aba Network e marque "Preserve log". Preencha e envie o primeiro: na lista de requisições aparece recebe.html?usuario=maria&mensagem=oi — os dados estão na própria URL, e você os vê também na barra de endereço. Agora envie o segundo: a URL fica limpa; clique na requisição, vá em Payload (ou Requisição, no Firefox) e veja usuario: maria e mensagem: oi. Repita o POST e aperte F5: o navegador pergunta se você quer reenviar os dados. Agora apague o atributo name de um dos campos e envie de novo: o campo some da requisição, mesmo estando preenchido na tela. Esse é o bug mais silencioso dos formulários.

2.4 Um formulário sem servidor ainda ensina

Enquanto não existe um back-end, três truques deixam o experimento útil:

  1. action vazio (action=""): o formulário envia para a própria página. Com method="get", os dados aparecem na URL e a página recarrega — ótimo para ver a query string sem erro 404.
  2. action="recebe.html": crie uma página simples de "recebido com sucesso". Com GET, ela abre com os dados na URL.
  3. Aba Network: funciona sempre, mesmo com resposta 404. A requisição saiu; é ela que interessa.

3. Campos de entrada: <input> e seus tipos

O <input> é o campo mais versátil do HTML. Ele não tem tag de fechamento (é um elemento vazio) e muda completamente de comportamento conforme o atributo type.

3.1 O catálogo de tipos

exemplos/tipos-de-input.html (trecho do <form>)

HTML
<input type="text"           name="nome"        id="nome">
<input type="email"          name="email"       id="email">
<input type="password"       name="senha"       id="senha">
<input type="number"         name="idade"       id="idade" min="16" max="120" step="1">
<input type="tel"            name="telefone"    id="telefone">
<input type="url"            name="site"        id="site">
<input type="date"           name="nascimento"  id="nascimento">
<input type="time"           name="horario"     id="horario">
<input type="datetime-local" name="agendamento" id="agendamento">
<input type="month"          name="competencia" id="competencia">
<input type="week"           name="semana"      id="semana">
<input type="search"         name="busca"       id="busca">
<input type="color"          name="cor"         id="cor" value="#0b3d5c">
<input type="range"          name="nota"        id="nota" min="0" max="10" step="0.5">
<input type="file"           name="anexo"       id="anexo" accept=".pdf,.docx">
<input type="checkbox"       name="termos"      id="termos">
<input type="radio"          name="turno"       id="manha" value="manha">
<input type="hidden"         name="origem"      value="site">

Nenhum desses tipos exige uma linha de JavaScript. O navegador entrega calendário, seletor de cor, controle deslizante e validação de formato de graça.

type Ganho prático imediato
email Teclado com @ no celular e validação de formato
tel Teclado numérico no celular (sem validar formato)
number Setas de incremento e validação numérica com min, max e step
date Seletor de calendário nativo, no idioma do sistema
url Validação de endereço (exige o esquema, como https://)
search Botão de limpar (×) e integração com o histórico de buscas
color Seletor de cores do sistema operacional
range Controle deslizante — bom para valores aproximados
file Botão de escolher arquivo, filtrado por accept
hidden Dado enviado sem aparecer na tela (origem, identificador, etapa)

💡 Dica Escolher o type certo não é detalhe estético: no celular, é ele que decide qual teclado aparece. Um campo de telefone com type="text" obriga a pessoa a trocar de teclado antes de digitar — uma fricção pequena que, multiplicada por milhares de usuários, vira abandono. Teste o seu formulário no celular pelo IP do Live Server na mesma rede Wi-Fi: é o teste mais barato de usabilidade que existe.

⚠️ Atenção type="number" serve para quantidades, não para "sequências de dígitos". CPF, CEP, telefone, matrícula e número de cartão não são números: eles têm zeros à esquerda que importam, podem ter pontuação e nunca são somados. Em type="number", o navegador remove zeros à esquerda, mostra setas de incremento sem sentido e permite notação como 1e5. Para esses campos, use type="text" com pattern — e, a partir da Aula 04, com inputmode="numeric" para o teclado certo.

3.2 Atributos essenciais dos campos

HTML
<label for="nome">Nome completo (obrigatório)</label>
<input type="text" id="nome" name="nome" value="Maria"
       placeholder="Ex.: Maria da Silva"
       required minlength="3" maxlength="80"
       autocomplete="name" autofocus>
Atributo O que faz
id Liga o campo ao <label for="…">; precisa ser único na página
name Chave enviada ao servidor; sem ele o campo não é enviado
value Valor inicial, já preenchido e já enviável
placeholder Dica dentro do campo; some ao digitar e não é enviada
required Bloqueia o envio enquanto o campo estiver vazio
minlength / maxlength Mínimo e máximo de caracteres
autocomplete Diz ao navegador que dado é este, para preenchimento automático
autofocus Coloca o foco neste campo ao abrir a página; use no máximo um por página
readonly Não editável, mas é enviado
disabled Não editável, não é enviado e sai da ordem do Tab

Quatro pares que costumam ser confundidos:

  • id × name. O id é para a página (ligar o <label>, servir de âncora, ser encontrado pelo JavaScript na Aula 13) e precisa ser único no documento. O name é para o servidor e pode se repetir — é assim que um grupo de caixas de seleção manda vários valores com a mesma chave.
  • placeholder × value. O placeholder é um texto cinza que some quando você digita; ele não é enviado. O value é conteúdo de verdade: aparece preenchido e é enviado se não for alterado.
  • readonly × disabled. Os dois impedem edição. readonly envia o valor; disabled não envia e também tira o campo da navegação por Tab. Se você precisa que um valor calculado chegue ao servidor, use readonly (ou um hidden), nunca disabled.
  • minlength × min. minlength conta caracteres (texto); min compara valores (números e datas). Trocar um pelo outro é um erro clássico: minlength="16" em um campo de idade exige um número de dezesseis dígitos.

🔎 Por baixo do capô O atributo autocomplete não é um simples liga/desliga. Ele tem um vocabulário padronizado de mais de cinquenta valores — name, given-name, family-name, email, tel, street-address, postal-code, cc-number, one-time-code — e é isso que permite ao navegador (e ao gerenciador de senhas do celular) preencher um cadastro inteiro em um toque. Preencher autocomplete corretamente é acessibilidade: consta na WCAG 2.1 como critério 1.3.5 ("identificar a finalidade da entrada"), justamente porque reduz a carga de digitação para pessoas com deficiência motora ou cognitiva. Um formulário com autocomplete="off" em tudo, "por segurança", só torna a vida mais difícil — e leva as pessoas a escolherem senhas piores.

4. <label> — obrigatório, não opcional

Todo campo precisa de um rótulo associado. Não é recomendação de estilo: é requisito de acessibilidade e de usabilidade — e o checklist de qualidade do Marco do projeto cobra isso.

4.1 As duas formas de associar

HTML
<!-- Forma 1: atributo for apontando para o id do campo (preferida) -->
<label for="email">E-mail institucional</label>
<input type="email" id="email" name="email">

<!-- Forma 2: o <label> envolve o campo -->
<label>
  E-mail institucional
  <input type="email" name="email">
</label>

A forma 1 é a preferida porque separa rótulo e campo no HTML — o que dá liberdade total ao CSS para colocá-los lado a lado, um acima do outro ou em colunas, sem lutar contra a estrutura. Ela também é a única que funciona quando o rótulo está em outra célula de uma tabela ou em outro contêiner.

A forma 2 dispensa id e é útil em casos pequenos, especialmente caixas de seleção dentro de uma lista. Cuidado: se você usar as duas ao mesmo tempo (<label for="x"> envolvendo o campo de id="a03-x"), o for precisa apontar para esse mesmo campo, senão o validador acusa erro.

4.2 Por que o rótulo é obrigatório

  1. Acessibilidade. Ao focar um campo, o leitor de tela anuncia o rótulo: "E-mail institucional, campo de edição". Sem <label>, a pessoa ouve apenas "campo de edição" e não tem como saber o que digitar.
  2. Usabilidade para todo mundo. Clicar no rótulo foca o campo. Em uma caixa de seleção de 16 × 16 pixels, o rótulo multiplica por dez a área clicável — a diferença entre acertar e errar o toque no celular.
  3. Clareza permanente. O rótulo continua visível depois que a pessoa digitou. O placeholder, não.

⚠️ Atenção placeholder não é <label>. Ele desaparece assim que a pessoa começa a digitar (e quem se distraiu no meio do preenchimento não sabe mais o que aquele campo pedia), costuma ter contraste baixo demais para a WCAG, não é lido de forma confiável por todas as tecnologias assistivas e, em campos preenchidos automaticamente, some sem nunca ter sido lido. Use placeholder só para exemplo de formato ("Ex.: 78550-000") — e sempre além do rótulo, nunca no lugar dele.

4.3 Marcando campos obrigatórios

Marcar obrigatoriedade só com um asterisco vermelho falha duas vezes: quem não enxerga a cor não percebe, e quem usa leitor de tela ouve "asterisco". Faça assim:

HTML
<label for="nome">Nome completo (obrigatório)</label>
<input type="text" id="nome" name="nome" required minlength="5" maxlength="100"
       autocomplete="name">

O texto "(obrigatório)" no rótulo é lido por todos. O atributo required faz o navegador barrar o envio e, de quebra, é anunciado pelos leitores de tela como "obrigatório". E, no rodapé do formulário, uma frase explicando a convenção resolve o resto: "Os campos marcados como obrigatórios precisam ser preenchidos".

5. Os outros campos

5.1 <textarea> — texto longo

HTML
<label for="mensagem">Mensagem</label>
<textarea id="mensagem" name="mensagem" rows="6" cols="40"
          maxlength="500" placeholder="Escreva sua mensagem"></textarea>
  • rows e cols definem o tamanho inicial em linhas e colunas de caractere; o CSS pode sobrescrever (Aula 06).
  • maxlength limita os caracteres, exatamente como no <input>.

⚠️ Atenção <textarea> não usa o atributo value: o conteúdo inicial vai entre as tags. E tudo que estiver ali dentro — inclusive espaços e quebras de linha de indentação — vira conteúdo do campo. Escreva sempre <textarea></textarea> colado quando o campo deve começar vazio. Um <textarea> "indentado bonitinho" nasce com espaços em branco dentro, e um required passa a aceitar um campo que parece vazio.

5.2 <select> — escolha em lista fechada

HTML
<label for="curso">Curso</label>
<select id="curso" name="curso" required>
  <option value="">Selecione o seu curso</option>
  <optgroup label="Computação">
    <option value="si">Sistemas de Informação</option>
    <option value="ads">Análise e Desenvolvimento de Sistemas</option>
  </optgroup>
  <optgroup label="Engenharias">
    <option value="civil" selected>Engenharia Civil</option>
    <option value="agro">Engenharia Agronômica</option>
  </optgroup>
  <optgroup label="Outros">
    <option value="outro">Outro curso</option>
  </optgroup>
</select>

Pontos que fazem diferença:

  • O value é o que vai para o servidor; o texto entre as tags é o que a pessoa lê. Quando o value é omitido, o próprio texto é enviado.
  • A primeira <option> com value="" é o truque que faz required funcionar em um <select>: enquanto ela estiver escolhida, o campo é considerado vazio. Sem ela, o primeiro item já conta como resposta válida — e você recebe "Sistemas de Informação" de quem nunca olhou a lista.
  • <optgroup label="…"> agrupa opções. O label é obrigatório e é anunciado pelo leitor de tela.
  • selected define a opção inicial. Use com parcimônia: uma resposta pré-marcada é uma resposta que muita gente vai deixar como está.

Seleção múltipla:

HTML
<label for="linguagens">Linguagens que você já usou</label>
<select id="linguagens" name="linguagens" multiple size="4">
  <option value="html">HTML</option>
  <option value="css">CSS</option>
  <option value="js">JavaScript</option>
  <option value="python">Python</option>
  <option value="java">Java</option>
</select>

multiple permite escolher vários (com Ctrl ou Shift) e size define quantas opções ficam visíveis. Na prática, um <select multiple> é pouco descoberto pelos usuários: em telas de toque, quase ninguém adivinha como selecionar mais de um. Prefira caixas de seleção quando as opções forem poucas.

5.3 Radio e checkbox

HTML
<fieldset>
  <legend>Turno de preferência</legend>

  <input type="radio" id="matutino" name="turno" value="matutino" checked>
  <label for="matutino">Matutino</label>

  <input type="radio" id="vespertino" name="turno" value="vespertino">
  <label for="vespertino">Vespertino</label>

  <input type="radio" id="noturno" name="turno" value="noturno">
  <label for="noturno">Noturno</label>
</fieldset>

<fieldset>
  <legend>Oficinas de interesse</legend>

  <input type="checkbox" id="of-git" name="oficinas" value="git">
  <label for="of-git">Git e GitHub do zero</label>

  <input type="checkbox" id="of-api" name="oficinas" value="api">
  <label for="of-api">Introdução a APIs REST</label>

  <input type="checkbox" id="of-acess" name="oficinas" value="acessibilidade">
  <label for="of-acess">Acessibilidade na prática</label>
</fieldset>

A regra decisiva: botões de rádio do mesmo grupo compartilham o mesmo name e têm value diferentes. É o name igual que faz um desmarcar o outro. Dar um name diferente para cada rádio é o erro mais comum da aula — e o sintoma é fácil de reconhecer: todos ficam marcáveis ao mesmo tempo.

Nas caixas de seleção, o name repetido tem outro efeito: o servidor recebe uma lista de valores sob a mesma chave (oficinas=git&oficinas=api).

Aspecto Radio Checkbox
Escolha Uma entre várias Várias independentes, ou nenhuma
Atributo name Igual em todo o grupo Igual (lista) ou diferente (sim/não)
Dá para desmarcar clicando Não Sim
Valor enviado quando não marcado Nada Nada

💡 Dica Uma caixa de seleção não marcada não é enviada — o servidor não recebe termos=false, ele simplesmente não recebe termos. Se você precisa distinguir "respondeu não" de "não respondeu", use dois botões de rádio ("Sim" e "Não") em vez de uma caixa de seleção.

5.4 <datalist> — sugestão com digitação livre

HTML
<label for="instituicao">Instituição de ensino</label>
<input type="text" id="instituicao" name="instituicao" list="instituicoes"
       autocomplete="organization">
<datalist id="instituicoes">
  <option value="Universidade Estadual — Campus Sinop"></option>
  <option value="UFMT — Campus Sinop"></option>
  <option value="Instituto Federal — Campus Sorriso"></option>
  <option value="UNIC"></option>
  <option value="Outra instituição"></option>
</datalist>

O <input> aponta para o <datalist> pelo atributo list, que recebe o id da lista. À medida que a pessoa digita, o navegador filtra as sugestões — mas qualquer valor fora da lista continua sendo aceito. É a diferença essencial em relação ao <select>.

Situação Use
O conjunto de respostas válidas é fechado e conhecido <select>
A lista é longa e a digitação acelera a busca, mas há exceções <input list> + <datalist>
Você precisa garantir que o valor esteja na lista <select> (e revalide no servidor)
Poucas opções (até cinco) e todas devem ficar visíveis Botões de rádio

5.5 <fieldset> e <legend> — agrupar é semântica

HTML
<fieldset>
  <legend>Dados pessoais</legend>
  <!-- campos do grupo -->
</fieldset>

<fieldset> agrupa campos relacionados; <legend> (que precisa ser o primeiro filho) dá título ao grupo. Por padrão o navegador desenha uma borda em volta — na Aula 06 você troca isso por algo mais bonito.

O valor real está na acessibilidade: para um leitor de tela, a legenda é anunciada junto com cada campo do grupo. Sem <fieldset>, três botões de rádio rotulados "Matutino", "Vespertino" e "Noturno" são apenas três opções soltas; com ele, viram "Turno de preferência, Matutino, botão de opção, 1 de 3". É o que torna um grupo de rádios compreensível sem visão.

Use <fieldset> sempre em grupos de rádio e de caixas de seleção, e em blocos temáticos de formulários longos (dados pessoais, endereço, pagamento). Não use para envolver um campo só.

6. Botões

HTML
<button type="submit">Enviar inscrição</button>
<button type="reset">Limpar formulário</button>
<button type="button">Calcular total</button>
type O que faz
submit Valida e envia o formulário. É o padrão dentro de um <form>
reset Devolve todos os campos aos valores iniciais
button Não faz nada sozinho; existe para receber uma ação em JavaScript (Aula 13)

⚠️ Atenção Dentro de um <form>, um <button> sem type age como submit. Isso causa envios acidentais em botões criados para outra finalidade ("mostrar mais", "adicionar linha", "calcular"). Declare sempre o type — é uma linha que evita um bug difícil de perceber, porque a página parece só "recarregar sozinha".

<button> ou <input type="submit">? Os dois enviam. Prefira <button>: ele aceita conteúdo HTML dentro (um ícone SVG ao lado do texto, por exemplo), enquanto o <input> só aceita o texto do atributo value. E nunca use uma imagem com um manipulador de clique no lugar de um botão: <img onclick="enviar()"> não é focável pelo teclado, não é anunciado como botão e não funciona com Enter.

Evite type="reset". Ele fica ao lado do botão de envio, tem o mesmo tamanho e apaga meia hora de preenchimento com um clique errado. Praticamente nenhum formulário profissional oferece "Limpar" hoje. Se você incluir um, coloque-o longe do botão principal e com aparência secundária.

7. Validação nativa do HTML

O navegador valida os campos antes de enviar, sem uma linha de JavaScript. Você só precisa declarar as regras.

exemplos/validacao-nativa.html (trecho)

HTML
<form action="/cadastrar" method="post">
  <label for="v-nome">Nome completo (obrigatório)</label>
  <input type="text" id="v-nome" name="nome" required minlength="5" maxlength="100"
         autocomplete="name">

  <label for="v-email">E-mail (obrigatório)</label>
  <input type="email" id="v-email" name="email" required autocomplete="email">

  <label for="v-idade">Idade</label>
  <input type="number" id="v-idade" name="idade" min="16" max="120" step="1">

  <label for="v-cep">CEP</label>
  <input type="text" id="v-cep" name="cep"
         pattern="[0-9]{5}-[0-9]{3}"
         placeholder="Ex.: 78550-000"
         title="Use o formato 00000-000, com o hífen"
         autocomplete="postal-code">

  <label for="v-senha">Senha (obrigatório)</label>
  <input type="password" id="v-senha" name="senha" required
         minlength="8"
         autocomplete="new-password">

  <button type="submit">Cadastrar</button>
</form>

7.1 Os atributos de validação

Atributo O que valida
required O campo não pode ficar vazio
minlength / maxlength Quantidade mínima e máxima de caracteres
min / max Valor mínimo e máximo (números, datas e horas)
step Incremento permitido (step="0.5", step="any")
type O formato inerente do tipo (email, url, number, date)
pattern Uma expressão regular que o valor precisa satisfazer
title Texto exibido junto da mensagem quando o pattern falha

7.2 pattern e title andam juntos

pattern recebe uma expressão regular — um assunto que a Aula 14 aprofunda. Por enquanto, três padrões resolvem quase tudo:

HTML
<!-- CEP: cinco dígitos, hífen, três dígitos -->
<input type="text" name="cep" pattern="[0-9]{5}-[0-9]{3}"
       title="Use o formato 00000-000, com o hífen">

<!-- CPF: 000.000.000-00 -->
<input type="text" name="cpf" pattern="[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}"
       title="Use o formato 000.000.000-00, com pontos e hífen">

<!-- Matrícula: exatamente oito dígitos -->
<input type="text" name="matricula" pattern="[0-9]{8}"
       title="A matrícula tem 8 dígitos, sem pontos ou traços">

Como ler [0-9]{5}-[0-9]{3}: "um dígito de 0 a 9, repetido cinco vezes; depois um hífen; depois um dígito repetido três vezes". O ponto precisa de barra invertida (\.) porque, sozinho, . significa "qualquer caractere".

Sem title, o navegador mostra uma mensagem inútil como "Corresponda ao formato solicitado." — a pessoa fica sabendo que errou, mas não o que fazer. Com title, a mensagem inclui o seu texto. pattern sem title é considerado formulário incompleto neste material.

7.3 As mensagens são do navegador

Quando a validação falha, quem escreve a mensagem é o navegador, no idioma do sistema operacional:

Texto
Preencha este campo.
Inclua um "@" no endereço de e-mail.
O valor precisa ser maior ou igual a 16.
Aumente o texto para 5 caracteres ou mais.
Selecione um item na lista.

Você não controla esse texto no HTML puro (na Aula 14 vai controlar, com a Constraint Validation API do JavaScript). O que você controla é o title do pattern e, principalmente, o rótulo: um campo bem rotulado gera menos erro do que qualquer mensagem bem escrita.

📌 Vale gravar Três perguntas voltam sempre: (1) o que acontece com um campo sem name ao enviar — ele não é enviado; (2) por que os rádios de um grupo precisam do mesmo name — é o que os torna mutuamente exclusivos; (3) a validação nativa garante que o dado chega correto ao servidor? Não. Ela é conveniência para o usuário, não segurança.

7.4 Validação no cliente não é segurança

⚠️ Atenção Toda a validação desta seção pode ser desligada em cinco segundos: basta abrir o DevTools e apagar o atributo required, ou acrescentar novalidate ao <form>, ou enviar a requisição por fora do navegador com uma ferramenta de linha de comando. Validação no cliente é usabilidade; validação no servidor é segurança. O servidor precisa revalidar absolutamente tudo — e isso vale para o resto da sua carreira, não só para esta trilha. Você vai implementar essa segunda camada no Nível 2, com Express.

O atributo novalidate no <form> desliga a validação nativa da página inteira. Ele parece um tiro no pé, mas tem uso legítimo: quando o formulário passa a ter validação em JavaScript, com mensagens próprias e acessíveis, você desliga a nativa para não ter duas mensagens competindo. É exatamente o que a Aula 14 faz.

8. Acessibilidade em formulários — checklist

Passe esta lista em todo formulário que você construir nesta trilha. Ela também é a base do desafio ⭐⭐ de hoje.

  • [ ] Todo campo tem <label> associado por for/id (ou envolvente). Nenhum rótulo é só placeholder.
  • [ ] Grupos de rádio e de caixas de seleção estão dentro de <fieldset> com <legend>.
  • [ ] A obrigatoriedade está no texto do rótulo, não apenas em uma cor ou em um asterisco.
  • [ ] A ordem de tabulação é a ordem visual: siga a ordem do HTML e não use tabindex positivo.
  • [ ] O foco é visível em todos os campos e botões (não remova o contorno sem colocar outro indicador).
  • [ ] autocomplete está preenchido com o valor semântico correto (name, email, tel, postal-code, organization, bday).
  • [ ] Cada <input type="file"> diz, no rótulo, os formatos e o tamanho aceitos.
  • [ ] O formulário inteiro é utilizável só com o teclado: Tab para avançar, Espaço para marcar, Enter para enviar.
  • [ ] Não há campo pedindo informação que você não vai usar. O campo mais acessível é o que não existe.

🔬 Investigue Abra qualquer formulário que você já tenha escrito e guarde o mouse. Navegue só com Tab, Shift+Tab, setas (nos rádios) e Espaço. Anote: você sempre sabe onde está o foco? A ordem faz sentido? Consegue marcar todas as opções? Consegue enviar com Enter? Depois repita clicando no texto de cada rótulo: se o campo correspondente não recebe o foco, aquele <label> não está associado — e o problema é for sem id correspondente, ou id duplicado na página.

💻 Mão na massa — a página de inscrição da Semana Acadêmica

A inscricao.html que você criou na Aula 02 tem apenas a frase "O formulário de inscrição será construído na próxima aula". Chegou a hora. Ao final destes passos, a página terá um formulário de inscrição completo, acessível e validado, com quatro grupos de campos.

Trabalhe em introducao-web/site-evento/inscricao.html.

Passo 1 — Cabeçalho, <head> e o esqueleto do <main>

Abra inscricao.html e substitua o conteúdo inteiro por este esqueleto. O <head>, o <header> e o <footer> são os mesmos das outras páginas — só mudam <title> e description.

site-evento/inscricao.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Formulário de inscrição na Semana Acadêmica de Sistemas de Informação: dados pessoais, acadêmicos e escolha de oficinas.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Inscrição — Semana Acadêmica de Sistemas de Informação</title>
</head>
<body>
  <header id="topo">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <p>Três noites de outubro · Auditório Central</p>
    <nav>
      <ul>
        <li><a href="index.html">Início</a></li>
        <li><a href="programacao.html">Programação</a></li>
        <li><a href="inscricao.html">Inscrição</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h2>Inscrição</h2>
    <p>
      A inscrição é gratuita e dá direito a certificado de 20 horas para quem
      participar de pelo menos 75% das atividades. As oficinas têm vagas
      limitadas e são preenchidas por ordem de inscrição.
    </p>
    <p>Os campos marcados como <strong>obrigatório</strong> precisam ser preenchidos.</p>
  </main>

  <footer>
    <p>Realização: Comissão Organizadora da Semana Acadêmica de Sistemas de Informação.</p>
    <p>
      <a href="mailto:contato@semanasi.com.br">contato@semanasi.com.br</a> ·
      <a href="tel:+556635111000">(66) 3511-1000</a>
    </p>
    <p>&copy; Semana Acadêmica de Sistemas de Informação. Todos os direitos reservados.</p>
  </footer>
</body>
</html>

Salve e confira no Live Server: a página abre com o mesmo cabeçalho das outras e o menu continua funcionando.

Passo 2 — Abrir o <form>

Dentro do <main>, logo depois do parágrafo sobre os campos obrigatórios, abra o formulário:

site-evento/inscricao.html (trecho: início do <form>)

HTML
    <form action="/inscrever" method="post" id="form-inscricao">
      <input type="hidden" name="origem" value="site-evento">

Três decisões, cada uma com um motivo:

  • method="post": a inscrição cria um registro. Não é uma consulta, não deve ficar no histórico e não pode ser repetida por engano com F5.
  • action="/inscrever": o endereço que vai receber os dados. Ele ainda não existe — o Live Server vai responder 404, e nós vamos observar a requisição na aba Network mesmo assim.
  • <input type="hidden" name="origem">: um dado que a pessoa não digita, mas que o servidor precisa. Aqui, de onde veio a inscrição — útil quando o mesmo endereço também recebe inscrições feitas no balcão do credenciamento.

Passo 3 — Grupo 1: dados pessoais

site-evento/inscricao.html (trecho: primeiro <fieldset>)

HTML
      <fieldset>
        <legend>Dados pessoais</legend>

        <p>
          <label for="nome">Nome completo (obrigatório)</label>
          <input type="text" id="nome" name="nome" required
                 minlength="5" maxlength="100"
                 placeholder="Ex.: Maria Aparecida da Silva"
                 autocomplete="name">
        </p>

        <p>
          <label for="cpf">CPF (obrigatório)</label>
          <input type="text" id="cpf" name="cpf" required
                 pattern="[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}"
                 placeholder="Ex.: 000.000.000-00"
                 title="Use o formato 000.000.000-00, com pontos e hífen">
        </p>

        <p>
          <label for="nascimento">Data de nascimento</label>
          <input type="date" id="nascimento" name="nascimento"
                 min="1930-01-01" autocomplete="bday">
        </p>

        <p>
          <label for="email">E-mail (obrigatório)</label>
          <input type="email" id="email" name="email" required
                 placeholder="Ex.: maria@exemplo.br"
                 autocomplete="email">
        </p>

        <p>
          <label for="telefone">Telefone com DDD</label>
          <input type="tel" id="telefone" name="telefone"
                 placeholder="Ex.: (66) 99999-0000"
                 autocomplete="tel">
        </p>
      </fieldset>

Cada par rótulo + campo está dentro de um <p>. Isso não é decoração: sem CSS, elementos de formulário são todos inline e ficariam todos na mesma linha, um grudado no outro. O <p> dá a quebra e o espaçamento mínimos até a Aula 06 assumir o controle.

Repare no type="date" com min="1930-01-01": o formato do atributo é sempre AAAA-MM-DD, independentemente de como o navegador mostra a data para o usuário (no Brasil, DD/MM/AAAA). Essa separação entre formato de máquina e formato humano é a mesma do atributo datetime que você usou nas tabelas da Aula 02.

Passo 4 — Grupo 2: dados acadêmicos

site-evento/inscricao.html (trecho: segundo <fieldset>)

HTML
      <fieldset>
        <legend>Dados acadêmicos</legend>

        <p>
          <label for="instituicao">Instituição de ensino</label>
          <input type="text" id="instituicao" name="instituicao"
                 list="instituicoes"
                 placeholder="Comece a digitar para ver sugestões"
                 autocomplete="organization">
          <datalist id="instituicoes">
            <option value="Universidade Estadual — Campus Sinop"></option>
            <option value="UFMT — Campus Sinop"></option>
            <option value="Instituto Federal — Campus Sorriso"></option>
            <option value="UNIC — Sinop"></option>
            <option value="Escola de ensino médio"></option>
          </datalist>
        </p>

        <p>
          <label for="curso">Curso (obrigatório)</label>
          <select id="curso" name="curso" required>
            <option value="">Selecione o seu curso</option>
            <optgroup label="Computação">
              <option value="si">Sistemas de Informação</option>
              <option value="ads">Análise e Desenvolvimento de Sistemas</option>
              <option value="cc">Ciência da Computação</option>
            </optgroup>
            <optgroup label="Engenharias">
              <option value="civil">Engenharia Civil</option>
              <option value="agro">Engenharia Agronômica</option>
            </optgroup>
            <optgroup label="Outros">
              <option value="outro">Outro curso</option>
              <option value="nenhum">Ainda não estou na graduação</option>
            </optgroup>
          </select>
        </p>

        <p>
          <label for="fase">Fase ou semestre</label>
          <input type="number" id="fase" name="fase" min="1" max="8" step="1"
                 placeholder="Ex.: 3">
        </p>

        <p>
          <label for="matricula">Matrícula</label>
          <input type="text" id="matricula" name="matricula"
                 pattern="[0-9]{8}"
                 placeholder="Ex.: 20240001"
                 title="A matrícula tem 8 dígitos, sem pontos ou traços">
        </p>
      </fieldset>

Compare de propósito os dois campos de escolha deste grupo:

  • Instituição é um <input list> com <datalist>: as cinco instituições da região cobrem quase todos os casos, mas alguém de fora precisa poder digitar a sua.
  • Curso é um <select> com <optgroup>: o conjunto é fechado, existe a opção "Outro curso" e a primeira <option> tem value="" para que o required funcione de verdade.

Passo 5 — Grupo 3: participação

site-evento/inscricao.html (trecho: terceiro <fieldset>)

HTML
      <fieldset>
        <legend>Participação</legend>

        <fieldset>
          <legend>Turno de preferência (obrigatório)</legend>

          <input type="radio" id="turno-matutino" name="turno" value="matutino" required>
          <label for="turno-matutino">Matutino</label>

          <input type="radio" id="turno-vespertino" name="turno" value="vespertino">
          <label for="turno-vespertino">Vespertino</label>

          <input type="radio" id="turno-noturno" name="turno" value="noturno">
          <label for="turno-noturno">Noturno</label>
        </fieldset>

        <fieldset>
          <legend>Oficinas de interesse</legend>

          <input type="checkbox" id="of-git" name="oficinas" value="git">
          <label for="of-git">Git e GitHub do zero (dia 1, 40 vagas)</label>
          <br>

          <input type="checkbox" id="of-html" name="oficinas" value="html-css">
          <label for="of-html">Do zero ao primeiro site com HTML e CSS (dia 2, 30 vagas)</label>
          <br>

          <input type="checkbox" id="of-api" name="oficinas" value="api-rest">
          <label for="of-api">Introdução a APIs REST (dia 2, 30 vagas)</label>
          <br>

          <input type="checkbox" id="of-acessibilidade" name="oficinas" value="acessibilidade">
          <label for="of-acessibilidade">Acessibilidade na prática (dia 3, 25 vagas)</label>
        </fieldset>

        <p>
          <label for="camiseta">Tamanho da camiseta</label>
          <select id="camiseta" name="camiseta">
            <option value="">Não quero camiseta</option>
            <option value="p">P</option>
            <option value="m">M</option>
            <option value="g">G</option>
            <option value="gg">GG</option>
          </select>
        </p>
      </fieldset>

Dois detalhes que valem nota:

  • required em um grupo de rádios só precisa aparecer uma vez, em qualquer um dos botões do grupo. O navegador entende que o grupo inteiro é obrigatório, porque todos compartilham o mesmo name.
  • <fieldset> dentro de <fieldset> é válido e é a marcação correta aqui: "Participação" é o bloco temático, e dentro dele há dois grupos de escolha que precisam, cada um, da sua própria legenda.

Os <br> entre as caixas de seleção são um paliativo até a Aula 06: sem CSS, todas ficariam na mesma linha. Não use <br> para espaçamento em conteúdo normal — aqui ele marca uma quebra real entre opções de uma escolha.

Passo 6 — Grupo 4: informações complementares

site-evento/inscricao.html (trecho: quarto <fieldset>)

HTML
      <fieldset>
        <legend>Informações complementares</legend>

        <p>
          <label for="acessibilidade">Você precisa de algum recurso de acessibilidade?</label>
          <textarea id="acessibilidade" name="acessibilidade" rows="4" cols="50"
                    maxlength="500"
                    placeholder="Ex.: intérprete de Libras, lugar reservado, material ampliado"></textarea>
        </p>

        <p>
          <label for="restricao">Restrição alimentar (para o coffee break)</label>
          <input type="text" id="restricao" name="restricao" maxlength="120"
                 placeholder="Ex.: sem lactose">
        </p>

        <p>
          <label for="como-soube">Como você ficou sabendo do evento?</label>
          <select id="como-soube" name="como_soube">
            <option value="">Prefiro não responder</option>
            <option value="professor">Um professor divulgou em aula</option>
            <option value="redes">Redes sociais do curso</option>
            <option value="colega">Um colega me contou</option>
            <option value="cartaz">Cartaz no campus</option>
          </select>
        </p>

        <p>
          <input type="checkbox" id="termos" name="termos" value="aceito" required>
          <label for="termos">
            Li e aceito o regulamento do evento e autorizo o uso da minha imagem
            nas fotos de divulgação (obrigatório).
          </label>
        </p>

        <p>
          <button type="submit">Enviar inscrição</button>
        </p>
      </fieldset>
    </form>

O aceite dos termos é uma caixa de seleção com required: o formulário não é enviado enquanto ela não for marcada. Repare que o rótulo vem depois do campo — a convenção para caixas de seleção e rádios é rótulo à direita, ao contrário dos campos de texto.

O campo name="como_soube" usa sublinhado em vez de hífen. Não é obrigatório, mas é hábito: chaves com hífen dão trabalho em algumas linguagens do lado do servidor, onde dados.como-soube seria interpretado como uma subtração.

Passo 7 — Fechar a página

Depois do </form>, antes de fechar o <main>, acrescente o link de volta ao topo, como nas outras páginas:

site-evento/inscricao.html (trecho: fim do <main>)

HTML
      <p><a href="#topo">Voltar ao topo</a></p>
    </main>

Passo 8 — Testar a validação nativa

  1. Abra inscricao.html no Live Server.
  2. Clique direto em Enviar inscrição, com tudo vazio. O navegador rola até o primeiro campo inválido, coloca o foco nele e mostra "Preencha este campo.".
  3. Preencha o nome com três letras: "Aumente o texto para 5 caracteres ou mais."
  4. Digite maria.exemplo.br no e-mail: "Inclua um '@' no endereço de e-mail."
  5. Digite 12345678900 no CPF: a mensagem traz o seu title — "Use o formato 000.000.000-00, com pontos e hífen".
  6. Coloque 12 na fase: "O valor precisa ser menor ou igual a 8."
  7. Deixe o curso em "Selecione o seu curso": "Selecione um item na lista."
  8. Não marque nenhum turno: "Selecione uma dessas opções."

Cada mensagem veio de um atributo diferente. Volte ao HTML e identifique qual.

Passo 9 — Ver os dados na aba Network

  1. Abra o DevTools (F12), vá à aba Network e marque Preserve log.
  2. Preencha o formulário inteiro, marcando duas oficinas.
  3. Envie. Vai aparecer uma requisição inscrever com status 404 — esperado, porque não existe servidor nesse endereço.
  4. Clique nela e abra Payload (Chrome) ou Requisição (Firefox). Confira: cada campo aparece com o seu name; oficinas aparece duas vezes, uma para cada caixa marcada; origem aparece com site-evento, mesmo você nunca tendo digitado nada; a camiseta, se você deixou "Não quero camiseta", aparece com valor vazio.
  5. Agora troque method="post" por method="get", recarregue e envie de novo. Os mesmos dados aparecem na URL, separados por &. Repare na codificação: o espaço do nome virou +, o @ do e-mail virou %40 e os parênteses do telefone viraram %28 e %29.
  6. Volte para method="post". Inscrição não é consulta.

Passo 10 — Validar no W3C

Abra https://validator.w3.org/#validate_by_input, cole a página inteira e corrija até ver "No errors or warnings to show". Os erros mais prováveis aqui são:

  • Duplicate ID — dois campos com o mesmo id (fácil de acontecer copiando e colando um <p>).
  • The for attribute of the label element must refer to a non-hidden form control — um for apontando para um id que você renomeou depois.
  • Element optgroup is missing required attribute label — um <optgroup> sem label.

Como testar

  • A página abre no Live Server com o mesmo cabeçalho e rodapé das outras quatro.
  • Enviar o formulário vazio mostra a mensagem do navegador no primeiro campo obrigatório, sem sair da página.
  • Clicar no texto de cada rótulo coloca o foco (ou marca a opção) no campo correspondente — teste em todos, inclusive nos rádios e nas caixas de seleção.
  • Marcar um turno desmarca automaticamente o anterior; marcar duas oficinas mantém as duas marcadas.
  • Só com o teclado, você consegue percorrer o formulário inteiro na ordem visual e enviar com Enter.
  • Na aba Network, a requisição POST traz um par nome=valor para cada campo preenchido — inclusive o hidden.
  • O validador do W3C não acusa nenhum erro.

Resultado esperado: uma página de inscrição feia e completamente funcional, com quatro blocos de campos, mais de vinte controles, validação nativa em mais de dez deles e zero erros no W3C. A aparência é assunto da Unidade 2; hoje o que importa é que o dado sai certo.

🧪 Laboratório

Nível A — Fixação

A1. Qual type de <input> é o mais adequado para cada dado, e por quê: (a) CPF; (b) data de nascimento; (c) senha; (d) avaliação de 0 a 10 com controle deslizante; (e) upload de currículo; (f) cor favorita; (g) e-mail; (h) site pessoal?

A2. Cite três diferenças entre GET e POST e dê um exemplo de formulário real para cada método.

A3. Por que placeholder não substitui <label>? Dê três motivos diferentes.

A4. Qual é a diferença prática entre readonly e disabled? Em qual situação a escolha errada faz um dado desaparecer do servidor?

A5. O que acontece com um campo preenchido que não tem o atributo name quando o formulário é enviado? Como você comprovaria isso em 30 segundos?

A6. Escreva um grupo de três botões de rádio para escolha de turno, com <fieldset>, <legend> e <label> corretos, e com o grupo marcado como obrigatório.

A7. Por que os botões de rádio de um grupo precisam ter o mesmo name? O que acontece na tela se cada um tiver um name diferente?

A8. O que faz o atributo required? Ele garante que o dado chegará preenchido ao servidor? Justifique em duas frases.

A9. Escreva um <select> de estados do Centro-Oeste com um <optgroup> chamado "Centro-Oeste" contendo MT, MS, GO e DF, e uma primeira opção que faça o required funcionar.

A10. Qual é a diferença entre <select> e <datalist>? Dê um exemplo de dado do site do evento em que cada um seria a escolha certa.

A11. Explique o que este pattern aceita, caractere por caractere: [0-9]{2}/[0-9]{2}/[0-9]{4}. Escreva um title adequado para ele.

A12. Um <button> sem atributo type dentro de um <form>: o que ele faz ao ser clicado? Que bug isso costuma causar?

Nível B — Aplicação

B1. Construa exercicios/aula03/login.html: um formulário de login com e-mail, senha (mínimo 8 caracteres), caixa de seleção "manter conectado", botão "Entrar" e um link "Esqueci minha senha". Todos os campos com <label>, autocomplete correto (email e current-password) e validação nativa. O método precisa ser post.

Resultado esperado: enviar com os campos vazios mostra a mensagem do navegador no e-mail; digitar uma senha de 5 caracteres mostra a mensagem de tamanho mínimo; clicar no texto "Manter conectado" marca a caixa; a aba Network mostra a senha no corpo da requisição, e nunca na URL.

Dica

O autocomplete de senha tem dois valores diferentes: current-password em telas de login e new-password em cadastro e troca de senha. Usar o valor certo é o que faz o gerenciador de senhas do celular oferecer a senha salva em vez de sugerir uma nova.

B2. Construa exercicios/aula03/matricula.html: um formulário de matrícula em disciplinas com dados do aluno (nome, matrícula, curso), seleção múltipla de disciplinas (caixas de seleção, no mínimo seis opções, com a carga horária no texto do rótulo), turno preferencial (rádios), observações (<textarea> de 500 caracteres) e aceite do regulamento (obrigatório). Use um <fieldset> por grupo.

Resultado esperado: quatro <fieldset> com <legend>; marcar três disciplinas gera três pares disciplinas=… na aba Network; o envio é bloqueado enquanto o aceite não estiver marcado; zero erros no validador do W3C.

Dica

Todas as caixas de seleção de disciplinas usam o mesmo name (disciplinas) e value diferentes — é assim que o servidor recebe uma lista. Cada uma precisa, ainda assim, de um id único para o seu <label>.

B3. Construa exercicios/aula03/busca.html: um formulário de busca com method="get", um campo name="q", um <select> de categoria (palestras, minicursos, oficinas) e um <select> de ordenação (por horário, por título). Envie e transcreva a URL gerada, explicando em uma frase cada parte da query string e por que este formulário usa GET e não POST.

Resultado esperado: a URL final tem a forma busca.html?q=acessibilidade&categoria=oficinas&ordem=titulo; o texto explica os três pares, o papel do ? e do &, e argumenta que a busca é idempotente e compartilhável.

Dica

Use action="busca.html" (a própria página) para não cair em 404. Depois de enviar, copie a URL da barra de endereço e cole em uma aba nova: se a mesma busca aparece, você acabou de demonstrar por que buscas usam GET.

B4. Caça aos problemas. O formulário abaixo tem pelo menos oito problemas de acessibilidade, semântica e funcionamento. Encontre todos, reescreva o formulário corrigido e entregue um documento listando cada problema, a correção e o motivo.

exercicios/aula03/formulario-quebrado.html (trecho)

HTML
<form>
  <div>Nome: <input type="text"></div>
  <div>Email: <input type="text" placeholder="Email"></div>
  <div>Senha: <input type="text" name="senha"></div>
  <div>
    <input type="radio" name="a" value="m">Masculino
    <input type="radio" name="b" value="f">Feminino
  </div>
  <div>Curso: <input type="text" name="curso"></div>
  <div>CEP: <input type="number" name="cep"></div>
  <div><img src="enviar.png" onclick="enviar()"></div>
</form>

Resultado esperado: uma versão corrigida com method declarado, <label> associado a cada campo, name em todos, type adequado a cada dado, um único name para o grupo de rádios (dentro de <fieldset> com <legend>), um <button type="submit"> no lugar da imagem clicável e validação nativa nos campos aplicáveis; e uma lista com pelo menos oito itens justificados.

Dica

Comece pelos problemas que impedem o formulário de funcionar (campos sem name, rádios com name diferente, botão que não é botão) e depois passe aos de acessibilidade (rótulos, agrupamento, foco). O campo de senha com type="text" e o CEP com type="number" são dois erros de tipo, cada um com uma consequência diferente.

B5. Estenda a página de contato. Em site-evento/contato.html, construa um formulário de mensagem com: nome, e-mail, assunto (<select> com quatro opções), mensagem (<textarea> com maxlength="600") e uma escolha entre "quero receber resposta por e-mail" e "por telefone" (rádios), com o telefone aparecendo como campo opcional. Método post, action="/contato".

Resultado esperado: o formulário completo dentro do <main> da contato.html, com <fieldset> no grupo de rádios, autocomplete correto em nome, e-mail e telefone, e zero erros no W3C.

Dica

Um <select> de assunto com value="" na primeira opção mais required garante uma escolha consciente. Para o telefone opcional, não use required: em HTML puro você ainda não consegue tornar um campo obrigatório em função de outro — isso é JavaScript, e você fará na Aula 14.

Nível C — Desafio

C1. Formulário de matrícula institucional. Reproduza, em HTML puro, um formulário de matrícula acadêmica com pelo menos 25 campos organizados em cinco <fieldset>: identificação, documentos, endereço, dados acadêmicos e informações complementares. Requisitos: todo campo com <label> associado e autocomplete semântico; validação nativa em todos os campos aplicáveis (required, pattern, min/max, minlength); ordem de tabulação lógica sem tabindex positivo; zero erros no validador do W3C. Ao final, escreva meia página justificando a escolha do type de cada campo não trivial (CPF, CEP, data, matrícula, renda, telefone).

Dica

Comece pela lista de dados em papel, agrupando-os antes de escrever uma linha de HTML: os cinco <fieldset> saem naturalmente do agrupamento. Para o endereço, use os valores de autocomplete específicos (postal-code, address-line1, address-level2, address-level1) — é o que faz o navegador preencher tudo depois do CEP. Teste com o teclado antes de validar no W3C.

C2. Formulário de inscrição do projeto autoral. No seu meu-projeto/, construa a página de formulário equivalente à inscricao.html — pedido, reserva, cadastro, matrícula, agendamento: o que fizer sentido no seu tema — com no mínimo 12 campos, 3 <fieldset>, um <select> com <optgroup>, um <datalist>, um grupo de rádios, um grupo de caixas de seleção, um <textarea> e validação nativa em pelo menos 5 campos. Este formulário é um dos requisitos do Marco 1 do projeto (Aula 06).

Dica

Não copie os campos do site do evento trocando os textos: pergunte-se que dados o seu domínio realmente precisa coletar. Um brechó precisa de tamanho e forma de pagamento; uma agenda de quadras precisa de data, horário e número de jogadores. Cada campo que você não usar depois é um campo que não deveria existir.

🏆 Desafios

⭐ O formulário de três campos

htmlformulariosacessibilidade

Todo mundo já desistiu de preencher um cadastro. Qual é o menor formulário capaz de inscrever alguém em um evento? Pegue a inscricao.html que você acabou de construir e faça o caminho inverso: corte tudo que não é indispensável e chegue a uma versão de três campos. Depois defenda cada corte. A pergunta que guia o exercício é sempre a mesma: "o que a organização do evento faria de diferente se soubesse este dado?" — se a resposta for "nada", o campo sai.

Critérios de pronto

  • Um arquivo inscricao-minima.html com exatamente três campos visíveis, todos com <label>, autocomplete e validação nativa.
  • Um arquivo cortes.md listando cada campo removido da versão completa e a justificativa em uma linha.
  • Pelo menos dois campos removidos com a justificativa "pode ser perguntado depois" e a explicação de quando seria o momento certo de perguntar.
  • Uma medição honesta: cronometre o preenchimento das duas versões (a completa e a mínima) por outra pessoa e registre os dois tempos.

Pistas

  1. Comece pelos campos que a organização consegue deduzir de outro campo (a matrícula diz o curso; o CPF diz a data de nascimento em outro sistema).
  2. Procure "progressive disclosure" e pense em quais dados podem ser pedidos só no credenciamento, presencialmente.
  3. Um campo obrigatório custa mais que um opcional: o opcional que quase ninguém preenche talvez devesse simplesmente não existir.
  4. Nome, e-mail e mais um. Qual é o terceiro, e por quê?
⭐⭐

⭐⭐ Auditoria de um formulário real

formulariosacessibilidadeinvestigacaohtml

Escolha um formulário público de verdade: matrícula de uma universidade, agendamento de uma prefeitura, cadastro de uma loja, abertura de conta de um banco. Abra o DevTools e leia o HTML dele. Quantos campos têm <label> de verdade? Quantos usam placeholder como rótulo? O grupo de rádios está em <fieldset>? Dá para preencher tudo só com o teclado? Você vai descobrir que sites enormes, com equipes enormes, erram exatamente as coisas desta aula — e vai aprender mais lendo o erro dos outros do que acertando sozinho.

Critérios de pronto

  • Um relatório de duas páginas com o endereço do formulário auditado e a data da auditoria.
  • Cada problema encontrado classificado por severidade (impede o uso / atrapalha / incomoda), com o trecho de HTML original copiado do DevTools e o trecho corrigido ao lado.
  • O resultado de uma passagem completa só com o teclado, descrevendo onde você travou (ou registrando que não travou).
  • Uma reescrita completa do formulário em HTML acessível, com todos os problemas corrigidos, que valide sem erros no W3C.
  • Pelo menos um problema encontrado com o checklist da seção 8 que uma ferramenta automática não apontaria.

Pistas

  1. No DevTools, com o campo selecionado, procure o painel Accessibility: ele mostra o "nome acessível" calculado do elemento. Nome vazio significa rótulo ausente.
  2. Rode também o Lighthouse (aba do DevTools) na categoria Accessibility, ou a extensão axe DevTools — mas lembre-se: as ferramentas pegam cerca de um terço dos problemas.
  3. O teste do teclado é o mais revelador e não precisa de ferramenta nenhuma: Tab, Shift+Tab, setas, Espaço, Enter.
  4. Compare a ordem do Tab com a ordem visual: quando o CSS move um campo de lugar, elas divergem — e o formulário fica confuso para quem não usa mouse.
⭐⭐

⭐⭐ Dado sujo: o formulário que estraga o banco

formularioshtmlinvestigacaobug

Um mesmo campo "telefone", sem restrição nenhuma, recebeu na vida real: 66999990000, (66) 99999-0000, 66 9 9999 0000, +55 66 99999-0000, 9999-0000 (falar com a Maria) e não tenho. Nenhum está errado do ponto de vista de quem digitou — todos estão errados do ponto de vista de quem vai usar o dado. Sua missão: descobrir quanto de sujeira um formulário mal projetado gera e provar que dá para reduzir isso só com HTML.

Critérios de pronto

  • Uma página coleta-suja.html com cinco campos sem nenhuma restrição (telefone, CEP, data, valor em reais e nome) e uma página coleta-limpa.html com os mesmos cinco dados, agora com type, pattern, min/max, maxlength e title adequados.
  • Dez pessoas (colegas, familiares) preenchendo as duas versões, com as respostas registradas em uma tabela.
  • Uma contagem de quantos valores de cada versão poderiam entrar direto em um banco de dados sem tratamento manual.
  • Uma conclusão de dez linhas explicando qual restrição trouxe o maior ganho por linha de HTML escrita.
  • Um campo em que você não conseguiu impedir a sujeira só com HTML, com a explicação do porquê e do que faltaria (adiante o que a Aula 14 vai resolver).

Pistas

  1. Para registrar as respostas sem servidor, use method="get" e copie a URL gerada a cada envio — a query string já é a sua tabela de coleta.
  2. pattern resolve formato, mas não resolve conteúdo: 000.000.000-00 passa no padrão de CPF e não é um CPF válido. Guarde esse caso para a conclusão.
  3. Compare type="date" com um campo de texto pedindo "DD/MM/AAAA": a diferença na sujeira costuma ser a maior de todas.
  4. Para valores em reais, teste type="number" com step="0.01" e observe o que acontece quando alguém digita vírgula.
⭐⭐⭐

⭐⭐⭐ Formulário multietapas sem JavaScript

htmlformulariosacessibilidadeprojeto

Formulários longos assustam. A solução clássica é dividir em etapas ("Etapa 2 de 4") — e quase todo mundo faz isso com JavaScript. Mas dá para construir um formulário de várias etapas usando apenas HTML, com uma página por etapa e os dados sendo carregados de uma para a próxima em campos ocultos. O resultado funciona sem JavaScript, funciona com o botão Voltar do navegador e é totalmente acessível. Descubra como e construa.

Critérios de pronto

  • Quatro páginas (etapa-1.html a etapa-4.html), cada uma com o seu <form method="get"> apontando para a etapa seguinte e um <progress> ou um texto "Etapa N de 4".
  • Os dados das etapas anteriores viajam até a última página e aparecem lá em campos ocultos, prontos para o envio final.
  • Uma página revisao.html que mostra, em texto, tudo o que foi preenchido antes do envio definitivo.
  • O botão Voltar do navegador funciona em todas as etapas e mantém as respostas já dadas.
  • Validação nativa em cada etapa: não é possível avançar com um campo obrigatório vazio.
  • Um texto de meia página comparando essa abordagem com a de "abas em JavaScript", listando duas vantagens e duas desvantagens concretas de cada uma.

Pistas

  1. Com method="get", tudo que foi preenchido está na URL da próxima etapa — e uma URL pode ser lida, copiada e retomada depois.
  2. <input type="hidden"> é o elemento que carrega um dado de uma página para a outra sem exibi-lo. Você precisará de um por campo herdado.
  3. Sem servidor, os campos ocultos da etapa seguinte não se preenchem sozinhos: para o exercício, preencha os value à mão a partir da URL e explique no texto o que um servidor faria automaticamente.
  4. Cuidado com dados sensíveis: uma senha nunca poderia atravessar etapas assim. Diga isso no seu texto final — vale ponto.

🐛 Erros comuns

Sintoma Causa Solução
O campo está preenchido, mas não aparece na aba Network nem na URL Falta o atributo name (ou o campo está disabled) Todo campo que deve ser enviado precisa de name; troque disabled por readonly se o valor precisa chegar ao servidor
Todos os botões de rádio ficam marcados ao mesmo tempo Cada rádio recebeu um name diferente Mesmo name no grupo inteiro, value diferente em cada um
Clicar no rótulo não foca o campo for sem id correspondente, ou id duplicado na página Confira o par for/id; o validador acusa Duplicate ID quando há repetição
O <select> com required nunca acusa erro A primeira <option> tem um valor real em vez de value="" Inclua uma primeira opção <option value="">Selecione…</option>
O <textarea> com required é aceito parecendo vazio Espaços e quebras de linha de indentação viraram conteúdo do campo Escreva <textarea></textarea> colado, sem nada entre as tags
A página "recarrega sozinha" ao clicar num botão qualquer <button> sem type dentro do <form> age como submit Declare type="button" em todos os botões que não enviam
A mensagem de erro do pattern é "Corresponda ao formato solicitado." Falta o atributo title no campo Sempre acompanhe pattern de um title explicando o formato esperado
O CPF perde os zeros à esquerda e mostra setinhas de incremento Uso de type="number" em uma sequência de dígitos Use type="text" com pattern (e inputmode="numeric", a partir da Aula 04)
A senha aparece na barra de endereço depois do envio O formulário está com method="get" Troque para method="post"; nenhum dado sensível vai por GET
O validador acusa The for attribute of the label element must refer to a non-hidden form control O for aponta para um id que não existe, foi renomeado ou pertence a um campo hidden Corrija o for para o id do campo visível correspondente
O validador acusa Element optgroup is missing required attribute label <optgroup> sem o atributo label Todo <optgroup> precisa de label; ele é anunciado pelo leitor de tela
Só o último valor do grupo de caixas de seleção chega ao servidor Cada caixa recebeu um name diferente, e o servidor foi programado para uma chave só Use o mesmo name em todas as caixas do grupo e value distintos

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). SILVA, Maurício Samy. Criando sites com HTML, capítulo de formulários. TERUEL, Evandro C. HTML 5 — Guia Prático, capítulo de formulários HTML5. Na MDN em pt-BR, o guia "Seu primeiro formulário" e a referência de <input>. Anote dois atributos que aparecem nas leituras e não apareceram nesta aula.

Parte 2 — Produção (30 min). Exercícios B2 (matrícula em disciplinas) e B4 (caça aos problemas), em arquivos .html comentados dentro de exercicios/aula03/. Para o B4, produza também o documento listando cada problema identificado, a correção aplicada e a justificativa.

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: em 10 a 15 linhas, explique como a escolha do tipo de campo e das restrições de validação influencia a qualidade do que chega ao banco de dados. Traga um exemplo concreto de dado sujo que você já viu (um telefone impossível, um nome em caixa alta, uma data no futuro) e diga qual atributo desta aula teria evitado.

Critério de pronto: os dois arquivos abrem no navegador, validam sem erros no W3C, permitem envio apenas com os campos obrigatórios preenchidos e podem ser percorridos inteiramente pelo teclado, com o rótulo de cada campo funcionando ao clique.

Guarde: a pasta do projeto (ou, se você já usa Git — assunto do capítulo 02 da trilha Deploy e da Aula 15 —, faça commit e push).

✅ Checkpoint do projeto

Ao fim desta aula, em site-evento/ e em meu-projeto/:

  • [ ] inscricao.html com um <form method="post" action="/inscrever"> e um <input type="hidden"> de origem.
  • [ ] Quatro <fieldset> de primeiro nível com <legend> (dados pessoais, dados acadêmicos, participação e informações complementares), com os grupos de escolha em <fieldset> aninhados.
  • [ ] Pelo menos oito tipos diferentes de <input> em uso (text, email, tel, date, number, radio, checkbox, hidden).
  • [ ] Um <select> com <optgroup> e primeira opção value="", um <datalist> e um <textarea> com maxlength.
  • [ ] <label> associado a todos os campos, com a obrigatoriedade indicada no texto do rótulo.
  • [ ] Validação nativa em pelo menos seis campos: required, minlength, min/max e dois pattern acompanhados de title.
  • [ ] Um grupo de rádios com o mesmo name e um grupo de caixas de seleção com o mesmo name.
  • [ ] autocomplete semântico em nome, e-mail, telefone, instituição e data de nascimento.
  • [ ] O formulário inteiro percorrível e enviável apenas com o teclado.
  • [ ] contato.html com o formulário de mensagem do exercício B5.
  • [ ] Zero erros no validador do W3C nas cinco páginas.
  • [ ] Em meu-projeto/: o formulário equivalente do seu tema (exercício C2), com no mínimo 12 campos.

📚 Para aprofundar

Na próxima aula você fecha o capítulo de formulários com os campos que faltaram — upload de arquivos com enctype, <output>, <progress>, <meter>, inputmode e campos ligados a um formulário à distância — e passa para o que dá vida às páginas: listas bem estruturadas, imagens responsivas com srcset e <picture>, vídeo com legendas e conteúdo externo em <iframe>.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Por que formulários importam1.1 A porta de entrada de todo sistema1.2 O que acontece quando você clica em "Enviar"2. O elemento <form>2.1 Atributos do <form>2.2 GET × POST2.3 Vendo os dados viajarem2.4 Um formulário sem servidor ainda ensina3. Campos de entrada: <input> e seus tipos3.1 O catálogo de tipos3.2 Atributos essenciais dos campos4. <label> — obrigatório, não opcional4.1 As duas formas de associar4.2 Por que o rótulo é obrigatório4.3 Marcando campos obrigatórios5. Os outros campos5.1 <textarea> — texto longo5.2 <select> — escolha em lista fechada5.3 Radio e checkbox5.4 <datalist> — sugestão com digitação livre5.5 <fieldset> e <legend> — agrupar é semântica6. Botões7. Validação nativa do HTML7.1 Os atributos de validação7.2 pattern e title andam juntos7.3 As mensagens são do navegador7.4 Validação no cliente não é segurança8. Acessibilidade em formulários — checklist💻 Mão na massa — a página de inscrição da Semana AcadêmicaPasso 1 — Cabeçalho, <head> e o esqueleto do <main>Passo 2 — Abrir o <form>Passo 3 — Grupo 1: dados pessoaisPasso 4 — Grupo 2: dados acadêmicosPasso 5 — Grupo 3: participaçãoPasso 6 — Grupo 4: informações complementaresPasso 7 — Fechar a páginaPasso 8 — Testar a validação nativaPasso 9 — Ver os dados na aba NetworkPasso 10 — Validar no W3CComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ O formulário de três campos⭐⭐ Auditoria de um formulário real⭐⭐ Dado sujo: o formulário que estraga o banco⭐⭐⭐ Formulário multietapas sem JavaScript🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 1 · Arquitetura da Web e HTML3 aulas de 50 min + 1 h EAD

Aula 04 — Formulários, mídias e listas

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Completar um formulário com campos avançados (<output>, <progress>, <meter>, upload múltiplo, inputmode, atributos form e formaction) e justificar a escolha de cada um.
  • Aplicar listas ordenadas, não ordenadas e de definição para organizar conteúdo — e explicar por que um menu de navegação é uma lista.
  • Inserir imagens com alt adequado a cada situação (informativa, decorativa, dentro de link, logotipo) e escolher o formato de arquivo certo.
  • Usar <figure>, <figcaption>, srcset, sizes e <picture> para imagens responsivas e com formatos modernos.
  • Incorporar áudio, vídeo (com legendas em WebVTT) e conteúdo externo em <iframe> de forma acessível e segura.
  • Medir o peso de uma página na aba Network do DevTools e reduzi-lo otimizando as mídias.

📋 Pré-requisitos

  • [ ] VS Code com a extensão Live Server e o navegador com DevTools (F12) funcionando.
  • [ ] A pasta do projeto do evento com as cinco páginas validadas no W3C: index.html, programacao.html, palestrantes.html (Aula 02), inscricao.html com o formulário (Aula 03) e contato.html (esqueleto da Aula 02).
  • [ ] Três fotos suas (ou livres de direitos) e um vídeo curto em .mp4 — usaremos para medir peso de página. Se não tiver, baixe amostras do Unsplash ou do Pexels.

Na Aula 03 você construiu a página de inscrição da Semana Acadêmica de Sistemas de Informação: <form>, tipos de <input>, <label>, <select>, <datalist>, <fieldset> e validação nativa. Hoje você fecha o capítulo de formulários com os campos que faltavam e passa para o que dá vida a uma página: listas bem estruturadas, imagens que carregam rápido, vídeo com legenda e conteúdo externo incorporado. Ao final, as cinco páginas do site do evento — todas já existentes — ganham logo, banner responsivo, listas de dois níveis, vídeo legendado, áudio, mapa e os campos de formulário que faltavam.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Formulários: campos avançados, <output>, <meter>, upload; listas ul, ol, dl, aninhamento e menus
2 50 min Imagens: alt por situação, formatos, figure, srcset/sizes, <picture>, loading="lazy"
3 50 min Áudio, vídeo, <track>, <iframe>; Mão na massa: ampliando as cinco páginas do evento; medição de peso no DevTools

1. Formulários: fechando o ciclo

Na Aula 03 você viu os tipos de <input>, <textarea>, <select>, <datalist>, <fieldset> e a validação nativa. Faltam alguns elementos e atributos que aparecem em formulários reais — inscrição em evento, cadastro com anexo, pesquisa de satisfação — e que resolvem problemas específicos.

1.1 Upload de arquivos de verdade

Um campo type="file" só funciona de fato quando o formulário sabe empacotar bytes, não apenas texto. Isso é responsabilidade do atributo enctype:

inscricao.html (trecho)

HTML
<form action="/inscrever" method="post" enctype="multipart/form-data">
  <fieldset>
    <legend>Comprovante de matrícula</legend>

    <label for="comprovante">Arquivo (PDF ou imagem, até 2 MB)</label>
    <input type="file" id="comprovante" name="comprovante"
           accept=".pdf,image/png,image/jpeg" required>

    <label for="certificados">Certificados anteriores (opcional, vários)</label>
    <input type="file" id="certificados" name="certificados"
           accept="application/pdf" multiple>
  </fieldset>

  <button type="submit">Enviar inscrição</button>
</form>
Atributo Efeito
enctype="multipart/form-data" Sem ele, o navegador envia só o nome do arquivo, não o conteúdo
accept Filtra o seletor de arquivos por extensão ou tipo MIME (image/* aceita qualquer imagem)
multiple Permite selecionar vários arquivos; o name vira uma lista no servidor
capture="environment" No celular, abre a câmera traseira direto em vez da galeria

⚠️ Atenção accept é conveniência, não segurança. O usuário pode trocar o filtro para "Todos os arquivos" e enviar o que quiser. Como toda validação no navegador, o servidor precisa checar tipo e tamanho de novo — esse lembrete vai se repetir até a última aula.

1.2 Mais de um valor em um campo só

multiple também vale para type="email": o campo aceita vários endereços separados por vírgula e valida cada um.

HTML
<label for="coautores">E-mails dos coautores (separe por vírgula)</label>
<input type="email" id="coautores" name="coautores" multiple
       placeholder="ana@exemplo.br, joao@exemplo.br">

1.3 <output>, <progress> e <meter> — mostrar, não coletar

Nem tudo dentro de um formulário é um campo de entrada. Três elementos existem para exibir valores com significado:

HTML
<!-- <output>: resultado de um cálculo. O atributo for aponta os campos envolvidos -->
<label for="lote">Lote</label>
<select id="lote" name="lote">
  <option value="30">1º lote — R$ 30</option>
  <option value="45">2º lote — R$ 45</option>
</select>

<label for="qtd">Quantidade de ingressos</label>
<input type="number" id="qtd" name="qtd" min="1" max="5" value="1">

<p>Total: <output id="total" for="lote qtd">R$ 30</output></p>

<!-- <progress>: andamento de uma tarefa (etapa 2 de 4 da inscrição) -->
<label for="etapa">Andamento da inscrição</label>
<progress id="etapa" value="2" max="4">2 de 4</progress>

<!-- <meter>: medida dentro de uma faixa conhecida (vagas ocupadas) -->
<label for="vagas">Vagas ocupadas no minicurso de Git</label>
<meter id="vagas" value="38" min="0" max="40" low="20" high="35" optimum="0">38 de 40</meter>
Elemento Serve para Não serve para
<output> Resultado calculado a partir de outros campos (total, IMC, prazo) Texto estático
<progress> Quanto de uma tarefa já foi feito (upload, passo a passo) Medida que pode subir e descer
<meter> Valor escalar em uma faixa: espaço em disco, vagas, força da senha Andamento de tarefa

O texto entre as tags de <progress> e <meter> é o fallback: aparece em navegadores muito antigos e é o que um leitor de tela lê quando não consegue interpretar a barra. Sem JavaScript o <output> fica parado no valor inicial — na Aula 13 você o atualizará em tempo real.

1.4 Campos fora do <form> e botões que mudam o destino

Dois atributos pouco conhecidos resolvem layouts que parecem impossíveis:

HTML
<form id="form-inscricao" action="/inscrever" method="post">
  <label for="nome">Nome completo</label>
  <input type="text" id="nome" name="nome" required>
</form>

<!-- Este campo está FORA do form no HTML, mas pertence a ele pelo atributo form -->
<label for="newsletter">Quero receber novidades do evento</label>
<input type="checkbox" id="newsletter" name="newsletter" form="form-inscricao">

<!-- Botões podem sobrescrever action, method e a validação do form -->
<button type="submit" form="form-inscricao">Enviar inscrição</button>
<button type="submit" form="form-inscricao"
        formaction="/rascunho" formnovalidate>Salvar rascunho</button>
  • form="id" liga um campo ou botão a um formulário, mesmo estando em outra região da página (um botão fixo no rodapé, por exemplo).
  • formaction e formmethod mudam o destino só daquele botão.
  • formnovalidate envia sem passar pela validação nativa — útil para "salvar rascunho", em que campos obrigatórios ainda podem estar vazios.

1.5 inputmode — o teclado certo sem mudar o tipo

Na Aula 03 você viu que type="tel" abre o teclado numérico no celular. Mas e um campo de CPF, que precisa de pattern e é do tipo text? Use inputmode:

HTML
<label for="cpf">CPF</label>
<input type="text" id="cpf" name="cpf" inputmode="numeric"
       pattern="[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}"
       placeholder="000.000.000-00" title="Formato: 000.000.000-00"
       autocomplete="off">

Valores úteis: numeric (só dígitos), decimal (dígitos e vírgula/ponto), tel, email, url, search. O type continua controlando a validação; o inputmode controla o teclado.

🔬 Investigue Abra o DevTools, ative o modo dispositivo (Ctrl+Shift+M) e escolha um celular. Crie três campos: type="text", type="text" inputmode="numeric" e type="number". Foque cada um. O emulador não mostra o teclado virtual, mas na aba Elements você verá que a validação de type="number" rejeita "12abc" e a de type="text" inputmode="numeric" aceita — a diferença entre validar e sugerir teclado fica visível. Se tiver um celular na mão, abra a página pelo IP do Live Server na mesma rede Wi-Fi e veja o teclado de verdade.

2. Listas

Uma lista é uma sequência de itens relacionados. Parece óbvio, mas o HTML oferece três tipos com semânticas diferentes, e escolher o certo muda como o leitor de tela anuncia o conteúdo ("lista com 5 itens") e como você o estilizará na Unidade 2.

2.1 Os três tipos

HTML
<!-- Lista não ordenada: a ordem NÃO importa -->
<ul>
  <li>HTML</li>
  <li>CSS</li>
  <li>JavaScript</li>
</ul>

<!-- Lista ordenada: a ordem IMPORTA (passos, ranking, cronograma) -->
<ol>
  <li>Analisar a URL</li>
  <li>Resolver o DNS</li>
  <li>Abrir a conexão TCP</li>
  <li>Enviar a requisição HTTP</li>
</ol>

<!-- Lista de definições: pares termo → descrição (glossário, ficha técnica) -->
<dl>
  <dt>HTML</dt>
  <dd>Linguagem de marcação que estrutura o conteúdo.</dd>
  <dt>CSS</dt>
  <dd>Linguagem de estilo que define a apresentação.</dd>
  <dt>JavaScript</dt>
  <dd>Linguagem de programação que adiciona comportamento.</dd>
</dl>
Elemento Quando usar Exemplo real
<ul> Itens sem ordem significativa Menu de navegação, lista de requisitos, tags
<ol> A sequência carrega informação Passo a passo, programação do dia, ranking
<dl> Pares nome/valor Glossário, ficha técnica de produto, perguntas e respostas

Um <dt> pode ter vários <dd> (um termo com duas definições) e vários <dt> podem compartilhar um <dd> (sinônimos). Isso torna a <dl> a escolha certa para FAQ e para a ficha de um palestrante (Nome → valor, Instituição → valor, Tema → valor).

2.2 Atributos da lista ordenada

HTML
<!-- Numeração romana, começando em 3, decrescente -->
<ol type="I" start="3" reversed>
  <li>Terceiro colocado</li>
  <li>Segundo colocado</li>
  <li>Campeão</li>
</ol>

<!-- Pular a numeração em um item -->
<ol>
  <li>Abertura</li>
  <li value="5">Encerramento (os itens 2 a 4 estão em outra página)</li>
</ol>
Atributo Valores Uso
type 1, a, A, i, I Tipo do marcador (numérico, alfabético, romano)
start inteiro Primeiro número da sequência
reversed booleano Conta para baixo (top 10, contagem regressiva)
value (no <li>) inteiro Força o número de um item específico

Na Unidade 2 você verá que a aparência do marcador (bolinha, traço, ícone) é assunto de CSS (list-style). Os atributos acima mudam o significado — por isso ficam no HTML.

2.3 Listas aninhadas: o erro que o validador sempre pega

O <ul> filho vai dentro do <li> pai, nunca solto entre dois <li>:

HTML
<!-- CERTO: o <ul> das sessões fica dentro do <li> da trilha -->
<ul>
  <li>Trilha Front-end
    <ul>
      <li>HTML semântico na prática</li>
      <li>CSS moderno sem framework</li>
    </ul>
  </li>
  <li>Trilha Back-end
    <ul>
      <li>APIs REST com Node</li>
      <li>Bancos de dados na nuvem</li>
    </ul>
  </li>
</ul>
HTML
<!-- ERRADO: <ul> diretamente dentro de <ul>. O W3C acusa "Element ul not allowed as child of element ul" -->
<ul>
  <li>Trilha Front-end</li>
  <ul>
    <li>HTML semântico na prática</li>
  </ul>
</ul>

Os únicos filhos permitidos de <ul> e <ol> são <li> (e <script>/<template>, que não renderizam). Qualquer outra coisa — um <p>, um <div>, outro <ul> — precisa estar dentro de um <li>.

2.4 Por que um menu é uma lista

Um menu de navegação é um conjunto de links relacionados. Marcá-lo como <ul> dentro de <nav> entrega três coisas de graça:

  1. O leitor de tela anuncia "navegação, lista com 5 itens" — o usuário sabe o tamanho do menu antes de percorrê-lo.
  2. A estrutura se mantém mesmo sem CSS: os itens ficam um embaixo do outro, legíveis.
  3. Na Aula 07 você transformará essa mesma lista em um menu horizontal só com CSS, sem tocar no HTML.

programacao.html (trecho do cabeçalho)

HTML
<header id="topo">
  <img src="img/logo-sasi.svg" alt="" width="160" height="48">
  <h1>Semana Acadêmica de Sistemas de Informação</h1>
  <p>Três noites de outubro · Auditório Central</p>
  <nav aria-label="Principal">
    <ul>
      <li><a href="index.html">Início</a></li>
      <li><a href="programacao.html" aria-current="page">Programação</a></li>
      <li><a href="inscricao.html">Inscrição</a></li>
      <li><a href="palestrantes.html">Palestrantes</a></li>
      <li><a href="contato.html">Contato</a></li>
    </ul>
  </nav>
</header>

aria-current="page" marca o link da página atual — o leitor de tela anuncia "página atual" e, na Unidade 2, você o usará como gancho para destacar o item ativo.

🧠 Você sabia? O Safari (e, com ele, o VoiceOver do iPhone) remove a semântica de lista quando o CSS aplica list-style: none a uma <ul> fora de <nav>. O leitor de tela deixa de anunciar "lista com 5 itens" e lê os links soltos. Foi uma decisão deliberada da Apple, porque muitos desenvolvedores usavam listas para tudo. A solução é justamente a que você aprendeu hoje: manter o menu dentro de <nav>, onde a semântica é preservada, ou acrescentar role="list" na <ul> quando a lista precisa ser anunciada e vive fora da navegação.

3. Imagens

Imagem é, disparado, o maior peso da maioria dos sites — e também a maior fonte de barreiras de acessibilidade. Uma <img> bem escrita resolve os dois problemas.

3.1 A <img> completa

HTML
<img src="img/campus-sinop.jpg"
     alt="Vista aérea do campus universitário em Sinop ao entardecer"
     width="800" height="600"
     loading="lazy">
Atributo Função
src Caminho da imagem (relativo à página, como nos links da Aula 02)
alt Texto alternativo — obrigatório em toda <img>
width / height Dimensões intrínsecas. O navegador reserva o espaço antes de baixar o arquivo e evita o "pulo" do layout
loading="lazy" Só baixa quando a imagem estiver perto de entrar na tela. Ganho grande em páginas longas
title Tooltip ao passar o mouse. Não substitui o alt

Sobre width e height: declare sempre os valores reais do arquivo. Eles não impedem o CSS de redimensionar depois (você fará max-width: 100% na Aula 08); servem para o navegador calcular a proporção e reservar a área. Sem eles, o texto abaixo da imagem "pula" quando ela chega — o que o Google mede como Cumulative Layout Shift e penaliza no ranqueamento.

3.2 Escrevendo bons alt

O alt é o que uma pessoa cega ouve, o que aparece quando a imagem não carrega e o que o buscador indexa. A regra não é "descreva a imagem"; é "entregue a mesma informação que a imagem entrega para quem vê".

Situação Regra Exemplo
Imagem informativa Descreva a informação, não a aparência alt="Gráfico de barras: inscrições subiram de 120 na primeira edição para 180 na terceira"
Imagem decorativa alt="" (vazio, mas presente) — o leitor de tela pula Linha divisória, textura de fundo
Imagem dentro de link Descreva o destino, não a imagem alt="Página inicial da Wikipédia"
Logotipo O nome, sem a palavra "logo" alt="Wikipédia"

Nunca escreva alt="imagem", alt="foto.jpg", alt="clique aqui" ou omita o atributo. Sem alt, o leitor de tela lê o nome do arquivo — "DSC underline zero zero quatro dois ponto jpg".

Um teste simples: feche os olhos e peça para alguém ler só o alt. Se você entende o que a imagem comunica, está bom. Se ouve "foto bonita do campus", não está.

3.3 <figure> e <figcaption>

Quando a imagem tem uma legenda visível — figura numerada, crédito, explicação — envolva-a em <figure>:

HTML
<figure>
  <img src="img/modelo-cliente-servidor.png"
       alt="Diagrama: o navegador envia uma requisição HTTP e o servidor devolve uma resposta"
       width="640" height="360">
  <figcaption>
    Figura 1 — Modelo cliente-servidor. Fonte: organização do evento.
  </figcaption>
</figure>

alt e figcaption não são redundantes: o alt é para quem não vê a imagem; a figcaption é a legenda para todos. Um leitor de tela lê os dois — por isso a legenda não deve repetir o alt palavra por palavra. <figure> também serve para trechos de código, tabelas, citações e vídeos com legenda — qualquer conteúdo autocontido ao qual o texto se refere.

3.4 Formatos de imagem

Formato Melhor para Transparência Observação
JPG Fotografias Não Compressão com perdas; ajuste a qualidade (75–85 costuma bastar)
PNG Logos, ícones, capturas de tela Sim Sem perdas; arquivos maiores em fotos
WebP Substituto moderno de JPG e PNG Sim Cerca de 30% menor; suporte universal nos navegadores atuais
AVIF Compressão máxima Sim Ainda menor que WebP; suporte amplo, com exceções em navegadores antigos
SVG Logos, ícones, diagramas Sim Vetorial: escala sem perder qualidade; arquivo minúsculo; é texto (XML)
GIF Animações simples Sim (binária) Obsoleto; prefira vídeo em MP4/WebM (10× menor)

💡 Dica Regra de otimização: nunca coloque na web uma foto de 4000×3000 px e 6 MB para exibi-la em um espaço de 400 px. Redimensione e comprima antes de subir — Squoosh (no navegador), TinyPNG ou GIMP. Um site de evento com dez fotos de celular sem tratamento pesa 40 MB; o mesmo site, tratado, pesa 1 MB. No 4G da zona rural de Sinop, isso é a diferença entre carregar em 2 segundos e em 1 minuto.

3.5 Imagens responsivas: srcset e sizes

Um celular de 360 px de largura não precisa baixar a mesma foto de 1600 px que o monitor do laboratório. Com srcset você oferece a mesma imagem em vários tamanhos e o navegador escolhe:

HTML
<img src="img/banner-800.jpg"
     srcset="img/banner-400.jpg 400w,
             img/banner-800.jpg 800w,
             img/banner-1600.jpg 1600w"
     sizes="(max-width: 600px) 100vw, 50vw"
     alt="Banner da Semana Acadêmica de Sistemas de Informação"
     width="800" height="450">

Como o navegador decide:

  1. sizes: "se a janela tem até 600 px, a imagem ocupará 100% da largura (100vw); senão, 50%".
  2. Calcula a largura em pixels que a imagem terá na tela, multiplicada pela densidade da tela (um celular com tela 2× precisa do dobro).
  3. Escolhe em srcset o menor arquivo que ainda cobre essa largura. Os números com w são as larguras reais de cada arquivo.
  4. O src continua sendo o fallback para navegadores que não entendem srcset.

⚠️ Atenção srcset sem sizes faz o navegador supor que a imagem ocupa 100% da janela — e ele baixará o arquivo grande mesmo para uma miniatura. Os dois atributos andam juntos.

3.6 Formatos alternativos com <picture>

srcset resolve tamanho; <picture> resolve formato (e também troca de enquadramento entre telas, a chamada "direção de arte"):

HTML
<picture>
  <source srcset="img/banner.avif" type="image/avif">
  <source srcset="img/banner.webp" type="image/webp">
  <img src="img/banner.jpg"
       alt="Banner da Semana Acadêmica de Sistemas de Informação"
       width="1600" height="900" loading="lazy">
</picture>

O navegador lê os <source> de cima para baixo e usa o primeiro cujo type ele suporta. O <img> final é obrigatório: é ele que carrega alt, width, height, loading — e serve de garantia para navegadores antigos. Os <source> também aceitam srcset com vários tamanhos e sizes, combinando as duas técnicas.

🔬 Investigue Coloque o <picture> acima em uma página, abra o DevTools na aba Network, filtre por Img e recarregue. Qual arquivo foi baixado — .avif, .webp ou .jpg? Agora, na coluna Type, confira o tipo real. Depois teste uma <img> com srcset de três larguras: redimensione a janela de 400 px até 1400 px recarregando a cada vez e anote qual arquivo veio em cada largura. Marque a opção Disable cache antes, senão o navegador reaproveita o arquivo anterior e o experimento mente.

3.7 Prioridade de carregamento

Nem toda imagem deve ser lazy. A imagem principal do topo da página (o banner, o "herói") precisa chegar primeiro: ela é o que o usuário vê antes de qualquer rolagem e o que as ferramentas de performance medem como Largest Contentful Paint.

HTML
<!-- Banner do topo: prioridade alta, sem lazy -->
<img src="img/banner-800.jpg" alt="Banner do evento" width="800" height="450"
     fetchpriority="high" decoding="async">

<!-- Fotos da galeria, abaixo da dobra: lazy -->
<img src="img/palestrante-01.jpg" alt="Ana Lúcia Ferreira" width="400" height="400"
     loading="lazy" decoding="async">

decoding="async" permite que o navegador decodifique a imagem sem travar o desenho do texto. Regra prática: as duas ou três primeiras imagens visíveis sem rolar não levam lazy; todas as outras levam.

4. Áudio e vídeo

Antes do HTML5, vídeo na web exigia o plugin Flash. Hoje <audio> e <video> são nativos, controláveis por teclado e legendáveis — desde que você use os atributos certos.

4.1 <audio>

HTML
<audio controls preload="metadata">
  <source src="midia/depoimento.mp3" type="audio/mpeg">
  <source src="midia/depoimento.ogg" type="audio/ogg">
  Seu navegador não suporta áudio HTML5.
  <a href="midia/depoimento.mp3">Baixe o arquivo</a>.
</audio>

Assim como em <picture>, o navegador usa o primeiro <source> que consegue tocar. O texto e o link no final são o fallback — aparecem só onde o elemento não é suportado. MP3 é aceito em todo lugar; OGG é opcional.

4.2 <video>

HTML
<video controls width="640" height="360"
       poster="img/capa-abertura.jpg" preload="metadata">
  <source src="midia/abertura.mp4" type="video/mp4">
  <source src="midia/abertura.webm" type="video/webm">
  <track src="legendas/abertura-pt.vtt" kind="captions"
         srclang="pt" label="Português" default>
  Seu navegador não suporta vídeo HTML5.
  <a href="midia/abertura.mp4">Baixe o vídeo</a>.
</video>
Atributo Efeito
controls Exibe os controles nativos (play, volume, tempo, tela cheia, legendas)
autoplay Inicia sozinho — os navegadores bloqueiam se houver som
muted Sem som. Necessário para autoplay funcionar
loop Repete indefinidamente
poster Imagem exibida antes do play (a "capa")
preload none (nada), metadata (só duração e dimensões), auto (o navegador decide)
playsinline No iPhone, toca dentro da página em vez de abrir em tela cheia

Sobre preload: o padrão de cada navegador varia, e auto pode baixar o vídeo inteiro mesmo que o usuário nunca aperte play. Para vídeos que não são o foco da página, metadata é a escolha segura. MP4 (H.264) toca em todos os navegadores; WebM é menor, mas opcional.

4.3 Legendas com <track> e WebVTT

Vídeo sem legenda é conteúdo inacessível para pessoas surdas — e, na prática, para qualquer pessoa no ônibus sem fone. Legenda não é extra: é requisito do critério 1.2.2 da WCAG, e volta no checklist de qualidade do Marco do projeto.

O <track> aponta para um arquivo WebVTT, um texto simples com marcações de tempo:

legendas/abertura-pt.vtt

Texto
WEBVTT

00:00:00.000 --> 00:00:03.500
Bem-vindos à Semana Acadêmica de Sistemas de Informação.

00:00:04.000 --> 00:00:08.000
Nesta edição, teremos três dias de palestras, minicursos e maratona de programação.

00:00:08.500 --> 00:00:12.000
As inscrições estão abertas no site do evento.

00:00:12.500 --> 00:00:15.000
[música de abertura]

Regras do formato: a primeira linha é WEBVTT; cada bloco tem um intervalo início --> fim no formato hh:mm:ss.mmm (as horas podem ser omitidas) seguido do texto; blocos separados por uma linha em branco. Sons relevantes vão entre colchetes — é isso que diferencia captions (para quem não ouve) de subtitles (tradução).

kind Para que serve
captions Legenda completa com falas e sons relevantes (acessibilidade)
subtitles Tradução das falas (o usuário ouve, mas não entende o idioma)
descriptions Descrição textual do que aparece na tela, para pessoas cegas
chapters Marcadores de capítulo para navegar no vídeo

Um <video> pode ter vários <track> — um por idioma ou por tipo. default marca o que liga sozinho. O Live Server serve arquivos .vtt sem configuração; em servidores próprios, o tipo MIME text/vtt precisa estar registrado (você verá isso na trilha Deploy).

🔎 Por baixo do capô Por que os navegadores bloqueiam autoplay com som? Há alguns anos, Chrome e Safari adotaram políticas que só permitem reprodução automática sem som ou depois de uma interação do usuário com a página (clique, toque, tecla). O motivo foi o abuso de anúncios em vídeo que começavam a gritar assim que a página abria. Hoje, autoplay sem muted simplesmente não toca — e o console avisa: play() failed because the user didn't interact with the document first. Se a interface depende de vídeo de fundo, ele será silencioso.

5. <iframe> — conteúdo externo

Um <iframe> abre uma outra página dentro da sua: um vídeo do YouTube, um mapa, um formulário do Google, um widget de clima.

HTML
<iframe src="https://www.youtube.com/embed/ID_DO_VIDEO"
        title="Vídeo: abertura da edição anterior da Semana Acadêmica"
        width="560" height="315"
        loading="lazy"
        allow="fullscreen; picture-in-picture"
        referrerpolicy="strict-origin-when-cross-origin">
</iframe>
Atributo Função
src Endereço da página incorporada (use a URL de embed que o serviço fornece, não a do site)
title Obrigatório para acessibilidade: é o que o leitor de tela anuncia ao entrar no quadro
loading="lazy" Só carrega quando estiver perto da tela — iframes pesam mais que imagens
allow Lista de permissões concedidas ao conteúdo (tela cheia, câmera, geolocalização)
sandbox Restringe o que o conteúdo pode fazer (scripts, formulários, pop-ups); vazio = bloqueia tudo

Mapa incorporado do Google Maps (o serviço gera este código em "Compartilhar → Incorporar um mapa"):

HTML
<iframe src="https://www.google.com/maps/embed?pb=CODIGO_GERADO_PELO_MAPS"
        title="Mapa: localização do Auditório Central, em Sinop"
        width="600" height="450"
        loading="lazy"
        referrerpolicy="no-referrer-when-downgrade">
</iframe>

⚠️ Atenção Um <iframe> executa código de terceiros dentro da sua página: ele pode rastrear o visitante, mostrar anúncios e, se o serviço for comprometido, servir conteúdo malicioso. Só incorpore fontes confiáveis. Para conteúdo que você não controla (um widget de fórum, por exemplo), use sandbox="allow-scripts allow-same-origin" e libere apenas o necessário. E nunca coloque um <iframe> de um site que você não tem permissão para incorporar: muitos enviam o cabeçalho X-Frame-Options: DENY e o quadro fica em branco — esse é um dos erros mais comuns da aula.

Também existem <embed> e <object>, dos tempos dos plugins. Hoje só têm um uso corrente: exibir um PDF dentro da página (<object data="edital.pdf" type="application/pdf">). Para todo o resto, <iframe>.

6. Medindo o peso da página

Tudo o que você aprendeu hoje tem um custo em bytes. A aba Network do DevTools mostra esse custo com precisão:

  1. Abra o DevTools (F12) e vá à aba Network.
  2. Marque Disable cache — senão a segunda visita esconde o peso real.
  3. Recarregue a página (Ctrl+R).
  4. Leia a barra de status no rodapé: N requests · X MB transferred · Y MB resources · Z s.
  5. Clique no cabeçalho da coluna Size para ordenar: os maiores arquivos ficam no topo. Quase sempre são imagens ou vídeo.
  6. No menu Throttling (ao lado de "Disable cache"), escolha Slow 4G e recarregue: é assim que metade dos seus usuários vê o site.
Faixa de peso total Avaliação
até 1 MB Bom para uma página com fotos
1 a 3 MB Aceitável; procure a imagem mais pesada e otimize
acima de 3 MB Problema. Alguém subiu foto de celular sem tratar

A coluna Size mostra dois números: o de cima é o transferido (comprimido pela rede); o de baixo, o tamanho real do recurso. Para imagens e vídeo eles são quase iguais — esses formatos já são comprimidos e a rede não ganha nada extra. Para HTML e CSS, o transferido costuma ser 3 a 5× menor (compressão gzip/brotli do servidor).

Fluxo de otimização de uma imagem no Squoosh (squoosh.app, roda no navegador, sem instalar nada):

  1. Arraste a foto original.
  2. Em Resize, reduza para o dobro da largura em que ela será exibida (uma foto mostrada em 400 px sai com 800 px, para telas de alta densidade).
  3. Em Compress, escolha WebP com qualidade 75 e compare visualmente com o original arrastando a linha divisória.
  4. Repita em AVIF e MozJPEG para gerar os três formatos do <picture>.
  5. Anote o peso antes e depois — você fará isso na Mão na massa.

💻 Mão na massa — Mídias, listas e campos avançados nas páginas do evento

As cinco páginas do site da Semana Acadêmica de Sistemas de Informação já existem: index.html, programacao.html e palestrantes.html nasceram na Aula 02, e inscricao.html recebeu o formulário completo na Aula 03. Hoje nenhuma página é criada do zero — você amplia o que já está lá.

⚠️ Atenção Nada do que as Aulas 02 e 03 entregaram é jogado fora. O <header id="a04-topo"> com o <h1> do site, o <h2> que titula cada página, o rodapé de três parágrafos e a tabela da programação continuam exatamente onde estão. Todo o trabalho de hoje é acrescentar — se em algum passo você se pegar apagando conteúdo antigo, pare e releia o passo.

Ao final, a pasta do projeto ficará assim:

Texto
site-evento/
├── index.html              ← ganha o banner responsivo
├── programacao.html        ← ganha trilhas, vídeo, áudio e mapa
├── inscricao.html          ← ganha os campos avançados da seção 1
├── palestrantes.html       ← ganha as fotos e o elenco completo
├── contato.html
├── img/
│   ├── logo-sasi.svg
│   ├── banner.avif
│   ├── banner.webp
│   ├── banner.jpg
│   ├── banner-400.jpg
│   ├── banner-800.jpg
│   ├── banner-1600.jpg
│   ├── capa-abertura.jpg
│   └── palestrante-01.jpg  (até palestrante-06.jpg)
├── midia/
│   ├── abertura.mp4
│   └── depoimento.mp3
└── legendas/
    └── abertura-pt.vtt

Passo 1 — Logo e navegação marcada nas cinco páginas

O <head> de cada página não muda hoje: charset, viewport, description, meta name="author" e title continuam como você os escreveu na Aula 02. O que muda é o cabeçalho: entram o logo e dois atributos de acessibilidade do <nav>.

programacao.html (trecho: <header>, depois da mudança)

HTML
  <header id="topo">
    <img src="img/logo-sasi.svg" alt="" width="160" height="48">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <p>Três noites de outubro · Auditório Central</p>
    <nav aria-label="Principal">
      <ul>
        <li><a href="index.html">Início</a></li>
        <li><a href="programacao.html" aria-current="page">Programação</a></li>
        <li><a href="inscricao.html">Inscrição</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
      </ul>
    </nav>
  </header>

Três decisões deste passo:

  • alt="" no logo. Pela tabela da seção 3.2, um logotipo leva o nome da organização no alt — mas aqui o <h1> logo abaixo já diz exatamente esse nome. Repetir faria o leitor de tela anunciar o mesmo texto duas vezes seguidas; então a imagem é decorativa e recebe alt vazio (presente, e vazio). Na Aula 07, quando o logo virar um link para a página inicial, o alt passa a descrever o destino.
  • aria-label="Principal" distingue este <nav> de qualquer outro que a página venha a ter.
  • aria-current="page" muda em cada página: em index.html ele vai no link "Início", em palestrantes.html no link "Palestrantes", e assim por diante. Repita o cabeçalho nas cinco páginas, trocando só onde o atributo está.

Passo 2 — Banner responsivo na página inicial

Em index.html, o banner entra como primeiro elemento do <main>, antes da lista "Nesta página". Ele combina as duas técnicas da seção 3: <picture> escolhe o formato, srcset/sizes escolhem o tamanho. Como é a primeira imagem visível, ela não leva lazy.

index.html (trecho: início do <main>)

HTML
  <main>
    <picture>
      <source srcset="img/banner.avif" type="image/avif">
      <source srcset="img/banner.webp" type="image/webp">
      <img src="img/banner-800.jpg"
           srcset="img/banner-400.jpg 400w,
                   img/banner-800.jpg 800w,
                   img/banner-1600.jpg 1600w"
           sizes="(max-width: 700px) 100vw, 1100px"
           alt="Auditório lotado na abertura da edição anterior da Semana Acadêmica"
           width="1600" height="900"
           fetchpriority="high" decoding="async">
    </picture>

    <p>Nesta página:</p>

Gere os seis arquivos no Squoosh a partir da mesma foto: três larguras em JPG e a versão de 1600 px também em WebP e AVIF. Anote o peso de cada um em um comentário HTML acima do <picture> — você vai comparar no Passo 10.

Passo 3 — Trilhas em lista aninhada

A tabela da programação continua sendo a fonte de horários. O que falta é uma visão por assunto, e ela é uma lista de dois níveis. Em programacao.html, logo depois do </table> e antes do parágrafo "Voltar ao topo", acrescente:

programacao.html (trecho: novo <section> dentro do <main>)

HTML
    <section id="trilhas">
      <h3>Trilhas do evento</h3>
      <p>As mesmas atividades da tabela acima, agrupadas por assunto.</p>
      <ul>
        <li>Desenvolvimento Web
          <ul>
            <li>Abertura e palestra magna: o futuro do desenvolvimento web</li>
            <li>Minicurso: Git e GitHub do zero</li>
            <li>Minicurso: acessibilidade na prática</li>
            <li>Maratona de programação</li>
          </ul>
        </li>
        <li>Ciência de Dados
          <ul>
            <li>Dashboards que os produtores realmente usam</li>
            <li>Dados abertos e cidades inteligentes</li>
          </ul>
        </li>
        <li>Inteligência Artificial
          <ul>
            <li>Minicurso: primeiros passos com redes neurais</li>
            <li>Visão computacional no controle de pragas</li>
          </ul>
        </li>
        <li>Segurança
          <ul>
            <li>Segurança em aplicações web: dez erros comuns</li>
            <li>Minicurso: phishing e engenharia social</li>
          </ul>
        </li>
      </ul>
    </section>

Repare que cada <ul> filho está dentro do <li> pai — é o erro da seção 2.3 que o validador sempre pega. O título é <h3> porque o <h2>Programação</h2> no topo do <main> é o título da página; as seções que vêm depois ficam um nível abaixo.

Passo 4 — As cinco mais procuradas, com <ol reversed>

Ainda em programacao.html, depois da seção de trilhas, um ranking. Aqui os atributos da lista ordenada (seção 2.2) fazem trabalho semântico de verdade: a contagem é decrescente, e a marcação diz isso.

programacao.html (trecho: novo <section> dentro do <main>)

HTML
    <section id="mais-procuradas">
      <h3>As cinco atividades mais procuradas</h3>
      <p>Posição pela procura na edição anterior, da quinta para a primeira.</p>
      <ol reversed>
        <li>Dados abertos e cidades inteligentes</li>
        <li>Segurança em aplicações web: dez erros comuns</li>
        <li>Maratona de programação</li>
        <li>Minicurso: primeiros passos com redes neurais</li>
        <li>Minicurso: Git e GitHub do zero</li>
      </ol>
    </section>

Com reversed, o navegador numera 5, 4, 3, 2, 1 — sem uma linha de CSS e sem digitar número nenhum. Se depois você inserir um item no meio, a contagem se reajusta sozinha.

Passo 5 — Vídeo de abertura com legenda

programacao.html (trecho: novo <section> dentro do <main>)

HTML
    <section id="abertura">
      <h3>Abertura da edição anterior</h3>
      <video controls width="640" height="360"
             poster="img/capa-abertura.jpg" preload="metadata" playsinline>
        <source src="midia/abertura.mp4" type="video/mp4">
        <track src="legendas/abertura-pt.vtt" kind="captions"
               srclang="pt" label="Português" default>
        Seu navegador não suporta vídeo HTML5.
        <a href="midia/abertura.mp4">Baixe o vídeo</a>.
      </video>
    </section>

Crie o arquivo de legendas com pelo menos quatro blocos, seguindo o exemplo da seção 4.3. Se o vídeo for seu, transcreva as falas reais; se for um vídeo de amostra sem fala, descreva o que aparece e os sons.

legendas/abertura-pt.vtt

Texto
WEBVTT

00:00:00.000 --> 00:00:03.000
[música de abertura]

00:00:03.500 --> 00:00:07.000
Boa noite e sejam bem-vindos à Semana Acadêmica de Sistemas de Informação.

00:00:07.500 --> 00:00:11.000
Serão três noites de palestras, minicursos e maratona de programação.

00:00:11.500 --> 00:00:14.000
[aplausos]

Passo 6 — Depoimento em áudio dentro de <figure>

programacao.html (trecho: novo <section> dentro do <main>)

HTML
    <section id="depoimento">
      <h3>Depoimento de uma participante</h3>
      <figure>
        <audio controls preload="metadata">
          <source src="midia/depoimento.mp3" type="audio/mpeg">
          Seu navegador não suporta áudio HTML5.
          <a href="midia/depoimento.mp3">Baixe o áudio</a>.
        </audio>
        <figcaption>Maria Eduarda, estudante do 4º semestre, sobre o minicurso de Git.</figcaption>
      </figure>
    </section>

Este é o caso clássico de <figure>: um conteúdo autocontido (o áudio) com uma legenda visível que o identifica.

Passo 7 — Mapa do local em <iframe>

programacao.html (trecho: último <section> do <main>, antes do "Voltar ao topo")

HTML
    <section id="onde">
      <h3>Onde acontece</h3>
      <p>Auditório Central, em Sinop.</p>
      <iframe src="https://www.google.com/maps/embed?pb=CODIGO_GERADO_PELO_MAPS"
              title="Mapa: Auditório Central, em Sinop"
              width="600" height="450"
              loading="lazy"
              referrerpolicy="no-referrer-when-downgrade">
      </iframe>
    </section>

Para obter o código: abra o Google Maps, pesquise o local que vai sediar o evento (por exemplo, o auditório da sua escola), clique em Compartilhar → Incorporar um mapa → Copiar HTML e substitua o src acima pelo que veio no código copiado. Mantenha o title — o código do Maps não o inclui.

Passo 8 — Fotos e elenco completo em palestrantes.html

A Aula 02 deixou três <article> nesta página, cada um com <h3>, dois parágrafos e um <dl> de atividades e contato. Hoje você faz duas coisas: acrescenta a foto dentro de cada <article> existente e completa o elenco com os três convidados que faltavam.

Em cada um dos três artigos que já existem, a <img> entra logo depois do <h3>:

palestrantes.html (trecho: o artigo de Ana Lúcia Ferreira, depois da mudança)

HTML
    <article id="ana-lucia-ferreira">
      <h3>Ana Lúcia Ferreira</h3>
      <img src="img/palestrante-01.jpg"
           alt="Ana Lúcia Ferreira em frente a um quadro com um diagrama de rede neural"
           width="400" height="400" decoding="async">
      <p><strong>Professora e pesquisadora</strong> na área de inteligência artificial.</p>

Faça o mesmo em Bruno Takahashi (img/palestrante-02.jpg) e Carla Mendes (img/palestrante-03.jpg), com alt que descreva aquela pessoa naquela foto. Esses três ficam visíveis sem rolar a página, então não levam loading="lazy".

Depois do artigo de Carla Mendes, e antes do parágrafo "Voltar ao topo", acrescente os três novos — agora com loading="lazy", porque estão abaixo da dobra:

palestrantes.html (trecho: os três novos <article>)

HTML
    <article id="diego-nascimento">
      <h3>Diego Nascimento</h3>
      <img src="img/palestrante-04.jpg"
           alt="Diego Nascimento sentado em frente a dois monitores com um portal do governo aberto"
           width="400" height="400" loading="lazy" decoding="async">
      <p><strong>Desenvolvedor web</strong> na Prefeitura de Sinop, responsável pelos portais de serviços ao cidadão.</p>
      <p>Trabalha com acessibilidade em portais públicos e conduz auditorias de conformidade com a WCAG.</p>
      <dl>
        <dt>Atividades</dt>
        <dd><a href="programacao.html#dia-1">Minicurso: Git e GitHub do zero</a> (dia 1)</dd>
        <dd><a href="programacao.html#dia-2">Minicurso: acessibilidade na prática</a> (dia 2)</dd>
        <dt>Contato</dt>
        <dd><a href="https://www.linkedin.com/" target="_blank" rel="noopener noreferrer">Perfil no LinkedIn</a></dd>
      </dl>
    </article>

    <article id="eduarda-ribeiro">
      <h3>Eduarda Ribeiro</h3>
      <img src="img/palestrante-05.jpg"
           alt="Eduarda Ribeiro apresentando slides em um auditório"
           width="400" height="400" loading="lazy" decoding="async">
      <p><strong>Professora</strong> do curso de Sistemas de Informação.</p>
      <p>Coordena a Semana Acadêmica e orienta projetos de extensão que levam estudantes do HTML ao deploy.</p>
      <dl>
        <dt>Atividades</dt>
        <dd><a href="programacao.html#dia-1">Abertura e palestra magna: o futuro do desenvolvimento web</a> (dia 1)</dd>
        <dd><a href="programacao.html#dia-3">Maratona de programação</a> (dia 3)</dd>
        <dt>Contato</dt>
        <dd><a href="mailto:eduarda@exemplo.com">eduarda@exemplo.com</a></dd>
      </dl>
    </article>

    <article id="felipe-arruda">
      <h3>Felipe Arruda</h3>
      <img src="img/palestrante-06.jpg"
           alt="Felipe Arruda em um canavial, segurando um tablet com imagens de drone"
           width="400" height="400" loading="lazy" decoding="async">
      <p><strong>Engenheiro de visão computacional</strong> na cooperativa Coopercana.</p>
      <p>Usa imagens de drone e modelos de aprendizado profundo para detectar pragas antes que elas se espalhem.</p>
      <dl>
        <dt>Atividades</dt>
        <dd><a href="programacao.html#dia-2">Visão computacional no controle de pragas</a> (dia 2)</dd>
        <dt>Contato</dt>
        <dd><a href="https://github.com/" target="_blank" rel="noopener noreferrer">Perfil no GitHub</a></dd>
      </dl>
    </article>

Cada alt descreve a pessoa — o que a foto mostra. O parágrafo abaixo traz o minicurrículo — a informação que o texto acrescenta. Nada se repete.

💡 Dica Os seis nomes desta página são os mesmos que vão alimentar o js/dados.js na Aula 12 e a página gerada por JavaScript na Aula 13. Manter o elenco estável agora poupa retrabalho depois — e é exatamente o que acontece em um projeto real.

Passo 9 — Campos avançados em inscricao.html

O formulário da Aula 03 tem quatro <fieldset> e nenhum arquivo para enviar. Hoje ele ganha o quinto grupo e o atributo que faz upload funcionar.

Primeiro, no <form>, acrescente o enctype:

inscricao.html (trecho: abertura do <form>)

HTML
    <form action="/inscrever" method="post" id="form-inscricao"
          enctype="multipart/form-data">

Depois, antes do </form>, o novo grupo:

inscricao.html (trecho: novo <fieldset>)

HTML
      <fieldset>
        <legend>Comprovante e pagamento</legend>

        <p>
          <label for="comprovante">Comprovante de matrícula (PDF ou imagem, até 5 MB) — obrigatório</label>
          <input type="file" id="comprovante" name="comprovante"
                 accept=".pdf,image/*" required>
        </p>

        <p>
          <label for="certificados">Certificados de edições anteriores (opcional, vários arquivos)</label>
          <input type="file" id="certificados" name="certificados" accept=".pdf" multiple>
        </p>

        <p>
          <label for="cpf">CPF — obrigatório</label>
          <input type="text" id="cpf" name="cpf" inputmode="numeric" required
                 pattern="[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}"
                 placeholder="000.000.000-00" title="Formato: 000.000.000-00"
                 autocomplete="off">
        </p>

        <p>
          <label for="lote">Lote</label>
          <select id="lote" name="lote">
            <option value="30">1º lote — R$ 30</option>
            <option value="45">2º lote — R$ 45</option>
          </select>
        </p>

        <p>
          <label for="qtd">Quantidade de ingressos</label>
          <input type="number" id="qtd" name="qtd" min="1" max="5" value="1">
        </p>

        <p>Total: <output id="total" for="lote qtd">R$ 30</output></p>

        <p>
          <label for="vagas">Vagas ocupadas no minicurso de Git</label>
          <meter id="vagas" value="38" min="0" max="40" low="20" high="35" optimum="0">38 de 40</meter>
        </p>
      </fieldset>

E, junto ao botão de envio que já existe, um segundo botão que salva sem validar:

inscricao.html (trecho: os dois botões)

HTML
      <button type="submit">Enviar inscrição</button>
      <button type="submit" formaction="/rascunho" formnovalidate>Salvar rascunho</button>

O <output> fica parado em "R$ 30" por enquanto: sem JavaScript ele não calcula nada. Na Aula 13, com eventos, ele passa a mudar sozinho quando o lote ou a quantidade mudam.

Passo 10 — Validação e medição

  1. Valide as cinco páginas em validator.w3.org (aba Validate by File Upload). Meta: zero erros.
  2. Abra index.html no Live Server, DevTools → Network, marque Disable cache, recarregue e anote o peso total no rodapé da aba (transferred). Repita em programacao.html.
  3. Ordene por Size. Otimize no Squoosh os três maiores arquivos que forem imagens; substitua-os na pasta img/.
  4. Recarregue e anote o novo peso. Registre os dois valores em um comentário HTML no topo do <body> de cada uma das duas páginas:
HTML
<!-- Peso da página antes da otimização: 7,8 MB · depois: 1,1 MB (redução de 86%) -->

Como testar

  • Menu: os cinco links levam às cinco páginas, todas existentes — nenhum 404. Em cada página, o item correspondente tem aria-current="page".
  • index.html: na aba Network, filtrando por Img, o banner baixado é .avif (ou .webp, conforme o navegador). Estreitando a janela para 400 px e recarregando com Disable cache, o arquivo escolhido pelo srcset muda.
  • programacao.html: a tabela dos três dias continua lá, com <caption>, três <tbody> e <tfoot>; a lista de trilhas mostra as atividades recuadas sob cada assunto; o ranking numera de 5 para 1.
  • Vídeo: o poster aparece antes do play; ao dar play, o botão CC dos controles liga a legenda e o texto aparece nos tempos definidos.
  • Áudio: o player aparece com play, tempo e volume, e a legenda da <figure> fica abaixo dele.
  • Mapa: o quadro mostra o campus; se estiver em branco, confira se o src é a URL de embed (contém /maps/embed?), não a URL normal do Maps.
  • palestrantes.html: seis artigos, cada um com foto e <dl>; na aba Network, as três últimas fotos só são baixadas quando você rola até elas.
  • inscricao.html: o seletor de arquivos filtra PDF e imagens; o botão "Enviar inscrição" bloqueia se o CPF estiver vazio ou fora do formato, e o "Salvar rascunho" envia mesmo assim.
  • Validador: zero erros nas cinco páginas.
  • Network: o peso de cada página ficou abaixo de 2 MB (com o vídeo em preload="metadata", ele não conta até o play).

Resultado esperado: o mesmo site de cinco páginas da Aula 02, agora com identidade visual (logo), navegação marcada, banner responsivo, listas de dois níveis, mídia acessível e o formulário completo — sem uma linha de CSS e sem ter descartado nada do que já estava pronto.

🧪 Laboratório

Nível A — Fixação

A1. Escreva o alt adequado para: (a) o logotipo da sua universidade ou escola; (b) uma linha decorativa entre duas seções; (c) um gráfico de pizza mostrando 60% de aprovados e 40% de reprovados; (d) uma foto do campus dentro de um link para a página de contato.

A2. Diferencie alt de figcaption: para quem cada um existe, e por que não devem ter o mesmo texto.

A3. Qual formato de imagem você usaria para: (a) foto de paisagem; (b) logotipo vetorial; (c) captura de tela com texto pequeno; (d) ícone que precisa escalar de 16 px a 512 px sem perder qualidade?

A4. O que faz loading="lazy" em uma <img>? Por que ele melhora a performance — e por que a imagem do topo da página não deve tê-lo?

A5. Por que um <button> sem type dentro de um formulário pode causar problemas? Cite os três valores possíveis de type e quando usar cada um.

A6. Para que serve o elemento <track> dentro de <video>? Qual a diferença entre kind="captions" e kind="subtitles"?

A7. Quando usar <ul>, <ol> e <dl>? Dê um exemplo real de cada, retirado do site do evento.

A8. Escreva uma lista de definição com três termos técnicos desta trilha (por exemplo: alt, srcset, WebVTT) e suas definições.

A9. Como se representa uma lista aninhada corretamente? Escreva o HTML de dois níveis e explique qual erro o validador do W3C acusa quando o aninhamento está errado.

A10. Por que um menu de navegação deve ser marcado como lista dentro de <nav>? Cite dois ganhos concretos.

A11. Um campo <input type="file"> está em um <form method="post"> sem enctype. O que chega ao servidor quando o usuário envia uma foto? Qual atributo corrige isso?

A12. Dado <meter value="38" min="0" max="40" low="20" high="35" optimum="0">, o que os atributos low, high e optimum comunicam ao navegador? Quando <progress> seria a escolha errada para esse mesmo dado?

Nível B — Aplicação

B1. Crie uma galeria de seis imagens usando <figure> e <figcaption>, com alt descritivo em todas, loading="lazy" nas três últimas e width/height declarados. Otimize as imagens no Squoosh e documente, em comentários HTML acima de cada <figure>, o peso original e o peso após a otimização.

Resultado esperado: seis figuras com legenda visível; o validador não acusa erros; na aba Network, ordenando por Size, nenhuma imagem passa de 150 KB; os comentários mostram redução em todas as seis.

Dica

Redimensione antes de comprimir: uma foto exibida em 400 px de largura sai do Squoosh com 800 px (para telas 2×), não com 4000. Só depois escolha WebP a 75 de qualidade. Para saber o peso original, veja a coluna Size no Network antes de trocar o arquivo, ou clique com o botão direito no arquivo e escolha Propriedades.

B2. Monte uma página de contato (para o seu projeto autoral) com: formulário (nome, e-mail, assunto via <select>, mensagem em <textarea>), telefone e e-mail clicáveis (tel: e mailto:), mapa incorporado por <iframe> com title, e horário de atendimento em uma tabela com caption, thead e th scope.

Resultado esperado: ao clicar no telefone em um celular, o discador abre; ao clicar no e-mail, o cliente de e-mail abre com o destinatário preenchido; o mapa mostra o local; a tabela tem cabeçalho de coluna; zero erros no validador.

Dica

<a href="tel:+5566999990000">(66) 99999-0000</a> — o número no href vai sem espaços, parênteses ou traços, com o código do país. Para mailto:, você pode pré-preencher o assunto: mailto:contato@evento.br?subject=Inscrição. Se o mapa ficar em branco, confira se copiou o src da opção "Incorporar um mapa", e não a URL da barra de endereço.

B3. Implemente uma página com um <picture> servindo AVIF, WebP e JPG, e uma <img> com srcset/sizes em três larguras (400, 800 e 1600 px). Na aba Network do DevTools, com Disable cache ligado, teste em três larguras de janela (400, 900 e 1400 px) e registre em uma tabela qual arquivo foi baixado em cada caso e por quê.

Resultado esperado: o <picture> baixa o .avif (ou .webp, dependendo do navegador); a <img> com srcset baixa arquivos diferentes conforme a largura; a tabela explica cada escolha com base em sizes e na densidade da tela.

Dica

Use o modo dispositivo do DevTools para fixar larguras exatas. Se em todas as larguras o navegador baixar sempre o mesmo arquivo, confira se o sizes está presente — sem ele, o navegador assume 100vw. Se ele baixar sempre o maior, você provavelmente está em uma tela com densidade 2× (o DevTools mostra o DPR no modo dispositivo).

B4. Monte uma página de cardápio usando <dl> para os pratos (termo = nome; definição = descrição e preço), <ul> para os acompanhamentos e <ol> para o modo de preparo de um dos pratos. Inclua uma foto por prato em <figure> com <figcaption>.

Resultado esperado: cada prato aparece como termo com descrição recuada; o modo de preparo está numerado; os acompanhamentos, com marcadores; cada foto tem alt que descreve o prato e legenda com o nome; zero erros no validador.

Dica

Um prato pode ter dois <dd>: um para a descrição, outro para o preço. Para colocar a <figure> junto ao prato, ela pode ficar dentro do <dd><dd> aceita conteúdo de fluxo, inclusive figuras.

B5. Amplie o formulário de inscrição da Aula 03 com um <fieldset> "Comprovante e pagamento" contendo: upload do comprovante de matrícula (accept para PDF e imagens), upload opcional de vários certificados (multiple), CPF com inputmode="numeric" e pattern, <select> de lote, <input type="number"> de quantidade e um <output> com o total. Acrescente um botão "Salvar rascunho" com formaction diferente e formnovalidate.

Resultado esperado: o <form> tem enctype="multipart/form-data"; o seletor de arquivos filtra os tipos permitidos; o botão "Enviar" exige os campos obrigatórios e o "Salvar rascunho" envia sem validar; zero erros no validador.

Dica

Teste o formnovalidate deixando um campo required vazio e clicando nos dois botões: só o "Enviar" deve bloquear. Para o <output>, o valor fica fixo por enquanto — ele só passará a calcular sozinho quando você aprender eventos, na Aula 13.

Nível C — Desafio

C1. Landing page multimídia. Crie a landing page de um produto ou evento contendo: vídeo de fundo (muted, loop, autoplay, com poster), galeria responsiva com <picture> em três formatos, player de áudio com depoimento, formulário de captação de contatos (nome, e-mail, telefone, interesse em <select>) e mapa em <iframe>. Sem CSS ainda. Otimize todas as mídias e apresente uma tabela comparando o peso de cada arquivo antes e depois, com o percentual de redução total da página.

Dica

O vídeo de fundo só toca sozinho se tiver muted — sem isso, o navegador bloqueia o autoplay. Para o percentual de redução, some os pesos de todos os arquivos antes e depois: (antes − depois) ÷ antes × 100. A aba Network dá os dois totais se você trocar os arquivos e recarregar com Disable cache.

🏆 Desafios

⭐ Caça aos bugs na galeria

htmlacessibilidadebug

Alguém entregou a galeria abaixo dizendo que "abre normal no navegador". Abre — mas o validador do W3C acusa erros, o leitor de tela lê nomes de arquivo e o vídeo nunca mostra legenda. Há oito problemas no trecho. Encontre todos sem rodar o validador primeiro; depois use o validador para conferir quantos você achou sozinho.

HTML
<section>
  <h2>Galeria</h2>
  <ul>
    <li><img src="img/foto1.jpg" alt="imagem"></li>
    <ul>
      <li><img src="img/foto2.jpg"></li>
    </ul>
    <li>
      <figure>
        <img src="img/foto3.jpg" alt="foto3.jpg" loading="lazy">
      </figure>
    </li>
  </ul>
  <video controls>
    <source src="midia/abertura.mp4">
    <track src="legendas/abertura.vtt" kind="captions">
  </video>
  <iframe src="https://www.youtube.com/watch?v=abc123" width="560" height="315"></iframe>
  <img src="img/logo.png" alt="logo da empresa" title="logo">
</section>

Critérios de pronto

  • Uma lista numerada com os oito problemas, cada um com: a linha, o que está errado, por que importa (validação, acessibilidade ou funcionamento) e a correção.
  • O trecho reescrito, validando com zero erros no W3C.
  • Pelo menos três dos problemas são de acessibilidade (não apenas de validação).
  • Um parágrafo explicando por que "abre normal no navegador" não é critério de qualidade.
Pistas
  1. Releia a seção 2.3 sobre o que pode ser filho direto de <ul>.
  2. Releia a tabela de alt da seção 3.2: dois alt estão presentes mas errados, e um está ausente.
  3. Um <source> sem type obriga o navegador a baixar o arquivo para descobrir se consegue tocá-lo; um <track> sem srclang e label não aparece no menu de legendas.
  4. A URL do YouTube que funciona em <iframe> contém /embed/, e todo <iframe> precisa de um atributo que o leitor de tela anuncia.
⭐⭐

⭐⭐ Galeria dez vezes mais leve

htmlperformancedevtoolsinvestigacao

Uma página de galeria com doze fotos de celular pesa em média 40 MB. Quanto tempo ela leva para abrir no 4G lento? Você vai medir. Monte uma página com doze fotos originais (sem tratamento), meça o peso e o tempo de carregamento com throttling Slow 4G e, depois, construa a mesma galeria com um pipeline completo de imagens responsivas — tamanhos, formatos e prioridade de carregamento — até reduzir o peso em pelo menos 90% sem que a qualidade visível caia.

Critérios de pronto

  • Duas versões da página (galeria-antes.html e galeria-depois.html) com as mesmas doze fotos.
  • Cada <img> da versão final usa srcset com três larguras, sizes coerente com o layout, e está dentro de <picture> com AVIF, WebP e JPG.
  • As duas ou três primeiras fotos têm fetchpriority="high"; as demais, loading="lazy".
  • Uma tabela em Markdown ou HTML com, para cada versão: peso total transferido, número de requisições e tempo até o evento Load (visível no rodapé do Network), medidos com Slow 4G e cache desabilitado.
  • Redução de peso igual ou superior a 90%.
Pistas
  1. No Squoosh, a aba Resize reduz dimensões; a aba Compress troca o formato. Faça as duas coisas: a maior economia vem de reduzir a dimensão.
  2. Em vez de tratar doze fotos uma a uma, procure o modo de linha de comando do Squoosh (@squoosh/cli) ou a ferramenta ImageMagick (magick foto.jpg -resize 800x foto-800.jpg) — vale a pena aprender agora.
  3. sizes deve refletir o espaço real: se as fotos ficarão em três colunas em telas largas, cada uma ocupa cerca de 33vw.
  4. Meça com Disable cache marcado e sempre na mesma largura de janela; senão os números não são comparáveis.

Para ir além: rode o Lighthouse (aba do DevTools) nas duas versões e compare a nota de Performance e a métrica Largest Contentful Paint.

⭐⭐⭐

⭐⭐⭐ Mídia acessível de ponta a ponta

htmlacessibilidadeinvestigacaoprojeto

A WCAG exige, para vídeo pré-gravado: legendas (critério 1.2.2), audiodescrição ou alternativa em texto (1.2.3) e, para áudio, transcrição (1.2.1). Quase nenhum site de evento cumpre isso — o seu vai cumprir. Pegue um vídeo de dois a três minutos com fala (pode ser uma palestra curta gravada por você ou um vídeo livre) e um áudio de um minuto, e construa uma página em que toda a informação da mídia esteja disponível para alguém que não ouve e para alguém que não vê.

Critérios de pronto

  • O <video> tem três <track>: captions em português com sons relevantes entre colchetes, chapters com pelo menos quatro capítulos, e descriptions descrevendo o que aparece na tela nos momentos em que a fala não explica.
  • Uma transcrição completa do vídeo em texto na própria página, dentro de <details> para não ocupar espaço, sincronizada com os capítulos por <h3>.
  • O <audio> tem, logo abaixo, a transcrição completa em texto.
  • Ao ligar as legendas nos controles do navegador, os três tipos de trilha aparecem no menu, com os rótulos corretos.
  • Um relatório de uma página: quanto tempo levou transcrever cada minuto de mídia, que ferramentas ajudaram (transcrição automática do YouTube, Whisper, transcrição do Google Docs) e o que precisou ser corrigido à mão.
Pistas
  1. Leia a especificação de WebVTT na MDN ("WebVTT API") — capítulos e descrições usam o mesmo formato de tempo das legendas, mudando só o kind do <track>.
  2. O YouTube gera legendas automáticas para vídeos enviados; é possível baixá-las em formato .vtt e corrigir os erros — muito mais rápido que transcrever do zero.
  3. Descrições (kind="descriptions") são lidas em voz alta pelo leitor de tela nos intervalos sem fala; escreva-as curtas o bastante para caber no silêncio disponível.
  4. Para ver a transcrição sincronizada por capítulos, cada <h3> da transcrição deve corresponder a um bloco do arquivo de capítulos — mesmo texto, mesma ordem.

Para ir além: publique a página e teste com o leitor de tela NVDA (Windows, gratuito) ou o VoiceOver (macOS): navegue só pelo teclado até o vídeo, dê play e ouça as descrições.

🐛 Erros comuns

Sintoma Causa Solução
A imagem aparece no VS Code mas não no Live Server (ícone quebrado) Caminho errado ou diferença de maiúsculas: Foto.JPG no disco e foto.jpg no src. Windows ignora a diferença; o servidor de deploy (Linux) não Use caminhos relativos à página, nomes em minúsculas, sem espaços nem acentos
Leitor de tela lê "DSC zero zero quatro dois ponto jpg" <img> sem alt Todo <img> tem alt; alt="" só em imagens decorativas
Element ul not allowed as child of element ul no validador Lista aninhada colocada entre dois <li> em vez de dentro de um O <ul> filho vai dentro do <li> pai
O vídeo de fundo não inicia e o console mostra play() failed because the user didn't interact with the document first autoplay sem muted Adicione muted (e playsinline para iPhone)
O botão CC não aparece nos controles do vídeo <track> com caminho errado, sem kind, ou arquivo .vtt sem a primeira linha WEBVTT Confira o caminho na aba Network (deve retornar 200) e a primeira linha do arquivo
O <iframe> fica em branco URL normal do site em vez da URL de embed, ou o site proíbe incorporação (X-Frame-Options: DENY) Use a URL de embed fornecida pelo serviço (YouTube: /embed/ID; Maps: "Incorporar um mapa")
A página "pula" quando as imagens carregam <img> sem width e height Declare as dimensões reais do arquivo; o CSS redimensiona depois
O srcset está lá, mas o navegador baixa sempre o maior arquivo Falta o sizes, então o navegador assume 100vw; ou a tela tem densidade 2× Escreva sizes coerente com o layout; teste com o DPR do modo dispositivo em 1
O servidor recebe só o nome do arquivo, não o conteúdo <form> com type="file" sem enctype="multipart/form-data" Adicione o enctype ao <form>
Peso da página não muda depois de otimizar as imagens Cache do navegador servindo os arquivos antigos Marque Disable cache na aba Network antes de recarregar

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulos sobre listas e imagens. TERUEL, E. C. HTML5 — Guia Prático, capítulo sobre APIs de mídia. Na MDN em pt-BR, leia "Imagens responsivas" e "Conteúdo de vídeo e áudio" (links em Para aprofundar).

Parte 2 — Produção (30 min). No seu projeto autoral, produza os exercícios B1 (galeria com a tabela de peso antes/depois) e B5 (formulário ampliado com upload, inputmode, <output> e botão de rascunho). Produza também as páginas de programação e de galeria do seu projeto, com todas as mídias e os três tipos de lista aplicados, validadas no W3C.

Critério de pronto: as páginas validam com zero erros; toda <img> tem alt, width e height; o vídeo tem <track> com arquivo .vtt funcionando; a página de galeria pesa menos de 2 MB na aba Network com cache desabilitado; o comentário HTML com o peso antes/depois está no topo do <body>.

Guarde no seu repositório: commit + push (ou a pasta do projeto, se você ainda não estiver usando Git).

Parte 3 — Registro (10 min). Em texto próprio (ou no fórum da turma, se houver), sob o título "Descrever imagens", escreva o alt de três imagens do seu próprio projeto — uma informativa, uma decorativa e uma dentro de link — e justifique cada escolha em duas linhas. Se puder, compare com o alt que outras pessoas escreveriam: ele entrega a mesma informação que a imagem entrega para quem vê?

✅ Checkpoint do projeto

Ao fim desta aula, o repositório do seu projeto autoral deve ter:

  • [ ] Página de programação (ou equivalente do seu domínio) com os horários em <time>, uma <ul> aninhada de dois níveis e uma <ol> usando pelo menos um dos atributos start, reversed ou type.
  • [ ] Menu de navegação como <ul> dentro de <nav aria-label>, com aria-current="page" na página atual.
  • [ ] Uma <dl> em uso (glossário, ficha de pessoa, cardápio — o que fizer sentido no seu domínio).
  • [ ] Página de galeria com seis <figure>/<figcaption>, alt descritivo, width/height e loading="lazy" nas imagens abaixo da dobra.
  • [ ] Banner em <picture> com AVIF, WebP e JPG, sem lazy.
  • [ ] Um <video> com controls, poster, preload="metadata" e <track kind="captions"> apontando para um .vtt válido.
  • [ ] Um <audio> com controls e fallback.
  • [ ] Um <iframe> com title e loading="lazy".
  • [ ] Formulário de inscrição com enctype, campo de upload com accept, inputmode no CPF e um <output>.
  • [ ] Zero erros no validador W3C em todas as páginas.
  • [ ] Comentário no topo do <body> com o peso da página antes e depois da otimização.

📚 Para aprofundar

Na próxima aula você dá ao site o esqueleto de layout com <header>, <main>, <section>, <article> e <aside> — e escreve as primeiras linhas de CSS.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Formulários: fechando o ciclo1.1 Upload de arquivos de verdade1.2 Mais de um valor em um campo só1.3 <output>, <progress> e <meter> — mostrar, não coletar1.4 Campos fora do <form> e botões que mudam o destino1.5 inputmode — o teclado certo sem mudar o tipo2. Listas2.1 Os três tipos2.2 Atributos da lista ordenada2.3 Listas aninhadas: o erro que o validador sempre pega2.4 Por que um menu é uma lista3. Imagens3.1 A <img> completa3.2 Escrevendo bons alt3.3 <figure> e <figcaption>3.4 Formatos de imagem3.5 Imagens responsivas: srcset e sizes3.6 Formatos alternativos com <picture>3.7 Prioridade de carregamento4. Áudio e vídeo4.1 <audio>4.2 <video>4.3 Legendas com <track> e WebVTT5. <iframe> — conteúdo externo6. Medindo o peso da página💻 Mão na massa — Mídias, listas e campos avançados nas páginas do eventoPasso 1 — Logo e navegação marcada nas cinco páginasPasso 2 — Banner responsivo na página inicialPasso 3 — Trilhas em lista aninhadaPasso 4 — As cinco mais procuradas, com <ol reversed>Passo 5 — Vídeo de abertura com legendaPasso 6 — Depoimento em áudio dentro de <figure>Passo 7 — Mapa do local em <iframe>Passo 8 — Fotos e elenco completo em palestrantes.htmlPasso 9 — Campos avançados em inscricao.htmlPasso 10 — Validação e mediçãoComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ Caça aos bugs na galeria⭐⭐ Galeria dez vezes mais leve⭐⭐⭐ Mídia acessível de ponta a ponta🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 1 · Arquitetura da Web e HTML3 aulas de 50 min + 1 h EAD

Aula 05 — Elementos HTML para layout e introdução ao CSS

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Estruturar uma página com os elementos de seccionamento do HTML5 (header, nav, main, section, article, aside, footer) e decidir quando div é a escolha certa.
  • Aplicar o padrão de contêiner centralizado que sustenta praticamente todo site profissional.
  • Explicar o que é CSS, como o navegador o aplica e o que são seletor, declaração e regra.
  • Usar as três formas de aplicação de estilo e justificar por que o CSS externo é a única aceita nos trabalhos desta trilha.
  • Calcular o tamanho de uma caixa no modelo de caixa e explicar por que todo projeto começa com box-sizing: border-box.
  • Diferenciar os valores de display (block, inline, inline-block, none) e seu efeito no fluxo do documento.
  • Reconhecer o colapso de margens e prever o espaço real entre dois elementos.

📋 Pré-requisitos

  • [ ] As cinco páginas do site do evento (index.html, programacao.html, inscricao.html, palestrantes.html, contato.html) validando com zero erros no W3C.
  • [ ] VS Code com Live Server e DevTools abertos na aba Elements — hoje você usará muito o painel Styles e a aba Computed.
  • [ ] Revisar da Aula 02 a estrutura mínima de um documento (<!DOCTYPE html>, lang, charset, viewport).

Na Aula 04 você terminou o conteúdo das páginas do evento: listas, imagens otimizadas, vídeo com legenda e mapa incorporado. O site está completo em informação — e cru em aparência. Antes de estilizar, porém, a página precisa de um esqueleto correto: é sobre ele que o CSS vai trabalhar. Hoje você organiza esse esqueleto com os elementos de seccionamento, escreve a primeira folha de estilo e entende a regra que resolve a maior parte dos problemas de layout: o modelo de caixa. Esta aula fecha a Unidade 1.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Elementos de seccionamento, section × article × div, padrão de contêiner centralizado, diagnóstico no console
2 50 min O que é CSS, sintaxe, três formas de aplicação, como o navegador monta a página, primeiras regras
3 50 min Box model, box-sizing, shorthand, colapso de margens, display; Mão na massa: esqueleto das cinco páginas e css/estilo.css

1. Elementos HTML para layout

Até a Aula 02 você usou <header>, <nav>, <main> e <footer> como "as partes da página". Hoje você entende o que cada um significa — e por que essa escolha muda o que o leitor de tela anuncia, o que o buscador indexa e como o CSS vai se apoiar na estrutura.

1.1 O esqueleto de uma página

index.html (esqueleto)

HTML
<body>
  <header id="topo">
    <img src="img/logo-sasi.svg" alt="" width="160" height="48">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <p>Três noites de outubro · Auditório Central</p>
    <nav aria-label="Principal">
      <ul>
        <li><a href="index.html" aria-current="page">Início</a></li>
        <li><a href="programacao.html">Programação</a></li>
        <li><a href="inscricao.html">Inscrição</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <section class="hero">
      <h2>Três noites de palestras, minicursos e maratona de programação</h2>
      <p>Participação gratuita, com certificado para quem comparecer.</p>
    </section>

    <section class="destaques">
      <h2>Destaques</h2>
      <article class="cartao">
        <h3>Minicurso de Git e GitHub</h3>
        <p>Do primeiro commit ao pull request, em duas horas.</p>
      </article>
      <article class="cartao">
        <h3>Maratona de programação</h3>
        <p>Equipes de três, problemas de lógica, premiação no encerramento.</p>
      </article>
    </section>
  </main>

  <aside>
    <h2>Edições anteriores</h2>
    <p>Veja a galeria e os vídeos das edições passadas.</p>
  </aside>

  <footer>
    <p>Realização: Comissão Organizadora da Semana Acadêmica de Sistemas de Informação.</p>
    <p>
      <a href="mailto:contato@semanasi.com.br">contato@semanasi.com.br</a> ·
      <a href="tel:+556635111000">(66) 3511-1000</a>
    </p>
    <p>&copy; Semana Acadêmica de Sistemas de Informação. Todos os direitos reservados.</p>
  </footer>
</body>

O <h1> continua no <header>, como na Aula 02: ele nomeia o site, é o mesmo nas cinco páginas, e o título de cada página é o <h2> do topo do <main>. Só na Aula 07, quando o site for redesenhado, o <h1> desce para dentro do <main>.

Elemento Papel Quantidade por página
<header> Cabeçalho da página ou de uma seção (logo, título, menu) Vários (um por contexto)
<nav> Bloco de navegação principal 1 a 3, sempre rotulados com aria-label
<main> Conteúdo único e central da página — o que muda de uma página para outra Exatamente 1
<section> Agrupamento temático com título Vários
<article> Conteúdo autocontido, que faz sentido isolado (notícia, cartão, comentário, produto) Vários
<aside> Conteúdo tangencial (barra lateral, links relacionados, propaganda) Vários
<footer> Rodapé da página ou de uma seção Vários
<div> Sem significado — só agrupa para estilo Quando nenhum outro serve

Esses elementos são chamados de marcos (landmarks): o leitor de tela oferece um atalho para pular direto entre eles ("ir para o conteúdo principal", "ir para a navegação"). Uma página feita só de <div> não tem marcos — a pessoa cega precisa ouvir tudo, do topo, sempre.

1.2 section × article × div: a pergunta certa

A dúvida mais comum desta aula é "isso é uma section ou um article?". Três perguntas resolvem:

  1. Faz sentido sozinho, fora desta página? Uma notícia, um cartão de palestrante, um comentário, um produto — se você conseguiria distribuí-lo por RSS ou compartilhar isoladamente, é <article>.
  2. É um agrupamento temático com título, que só faz sentido dentro desta página? "Destaques", "Programação do primeiro dia", "Perguntas frequentes" — é <section>. Uma <section> sem título é sinal de que você queria uma <div>.
  3. Não tem significado nenhum — existe só para agrupar por estilo? Um contêiner para centralizar, um invólucro para aplicar um fundo — é <div>.

💡 Dica div não é proibida — é a última opção. Precisa de um contêiner só para centralizar o conteúdo com max-width? div é a escolha certa. Precisa marcar o rodapé? Use footer. A pergunta a fazer é sempre: "existe um elemento que descreva o que isto é?" Se existe, use-o. Se não existe, div.

Um <article> pode ter seu próprio <header> e <footer> — e é comum:

HTML
<article class="cartao">
  <header>
    <h3>Minicurso de Git e GitHub</h3>
    <p><time datetime="19:30">19h30</time> · Laboratório 2 · 40 vagas</p>
  </header>
  <p>Do primeiro commit ao pull request, em duas horas.</p>
  <footer>
    <a href="inscricao.html">Inscrever-se</a>
  </footer>
</article>

O <header> de dentro do <article> não é o cabeçalho da página — é o cabeçalho daquele artigo. O leitor de tela entende a diferença pelo contexto.

1.3 Mais de um <nav>

Uma página pode ter navegação principal, um menu de rodapé e a "trilha de migalhas" (breadcrumb). Todos são <nav>; o que os diferencia é o aria-label:

HTML
<nav aria-label="Principal">
  <ul>
    <li><a href="index.html">Início</a></li>
    <li><a href="programacao.html">Programação</a></li>
  </ul>
</nav>

<nav aria-label="Você está em">
  <ol>
    <li><a href="index.html">Início</a></li>
    <li><a href="programacao.html">Programação</a></li>
    <li aria-current="page">Primeiro dia</li>
  </ol>
</nav>

<footer>
  <nav aria-label="Rodapé">
    <ul>
      <li><a href="contato.html">Contato</a></li>
      <li><a href="privacidade.html">Política de privacidade</a></li>
    </ul>
  </nav>
</footer>

Sem o rótulo, o leitor de tela anuncia três vezes "navegação" e a pessoa não sabe qual é qual.

1.4 O padrão de contêiner centralizado

Abra qualquer site profissional: o cabeçalho tem uma cor de fundo que vai de uma borda à outra da janela, mas o conteúdo (logo, menu) fica limitado a uma faixa central. Esse é o padrão de contêiner centralizado, e ele combina um elemento semântico de largura total com uma div interna que limita e centraliza:

index.html (trecho)

HTML
<header id="topo">
  <div class="container">
    <img src="img/logo-sasi.svg" alt="" width="160" height="48">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <nav aria-label="Principal">
      <ul>
        <li><a href="index.html" aria-current="page">Início</a></li>
        <li><a href="programacao.html">Programação</a></li>
      </ul>
    </nav>
  </div>
</header>

css/estilo.css (trecho)

CSS
.container {
  width: 100%;
  max-width: 1100px;
  margin-inline: auto;
  padding-inline: 1rem;
}

Leia a regra assim: "ocupe toda a largura disponível, mas nunca passe de 1100 px; as margens laterais automáticas dividem a sobra igualmente, centralizando; e sempre reserve 1 rem de respiro de cada lado". Em um monitor largo, o conteúdo para em 1100 px; em um celular, ocupa toda a largura menos o respiro. Esse número, 1100 px, é o do projeto e não muda mais — nas Aulas 06 e 07 ele vira a variável --largura-max. O elemento semântico (<header>) continua ocupando 100% — útil para a cor de fundo que sangra até as bordas.

Esse é o esqueleto de praticamente todo site que você já visitou. Guarde-o: você o usará em todas as páginas hoje e em todos os projetos da trilha.

🔬 Investigue Abra index.html no Live Server, pressione F12, vá à aba Console e cole o comando abaixo. Ele desenha um contorno vermelho translúcido em todos os elementos da página — você enxerga a caixa de cada um e percebe na hora onde há div demais, section sem título ou conteúdo fora do <main>.

js document.querySelectorAll("*").forEach(el => el.style.outline = "1px solid rgba(255, 0, 0, 0.3)" );

Depois, na aba Elements, clique com o botão direito no <body> e escolha Expand recursively: a árvore inteira se abre e você vê a hierarquia dos marcos. Recarregue a página para tirar os contornos.

🧠 Você sabia? Antes do HTML5, todo site era feito de <div id="a05-header">, <div id="a05-nav">, <div id="a05-footer">. Os nomes dos elementos de seccionamento não foram inventados por um comitê: o grupo de trabalho analisou mais de um bilhão de páginas indexadas pelo Google e descobriu quais eram os id e class mais usados pelos desenvolvedores. header, footer, nav, content e sidebar estavam no topo — e viraram <header>, <footer>, <nav>, <main> e <aside>. O HTML5 padronizou o que a comunidade já fazia por convenção.

2. O que é CSS

CSS (Cascading Style Sheets, folhas de estilo em cascata) é a linguagem que descreve como o conteúdo estruturado pelo HTML deve ser apresentado. O HTML responde "o que é isso?" (um título, uma lista, um formulário); o CSS responde "com que aparência?" (azul, 2 rem, centralizado, com sombra).

A separação é deliberada e vale ouro: o mesmo HTML pode ganhar dez aparências diferentes trocando só o CSS, e o mesmo CSS serve a cem páginas. É por isso que o Marco 1 fecha sem CSS e o Marco 2 estiliza o mesmo site: a estrutura e a apresentação são trabalhos diferentes.

2.1 Sintaxe

CSS
seletor {
  propriedade: valor;
  outra-propriedade: outro-valor;
}

Um exemplo real:

CSS
h1 {
  color: #0b3d5c;
  font-size: 2rem;
  margin-bottom: 16px;
}
Termo O que é No exemplo
Seletor Quem recebe o estilo h1 (todos os <h1> da página)
Declaração Um par propriedade: valor; color: #0b3d5c;
Bloco de declarações Tudo entre as chaves as três declarações
Regra Seletor + bloco o exemplo inteiro

Regras de escrita que evitam metade dos erros da aula:

  • Toda declaração termina com ;. Sem ele, o navegador junta a declaração com a próxima e ignora as duas.
  • Propriedades e valores são em minúsculas; nomes compostos usam hífen (font-size, não fontSize).
  • Comentários são /* assim */. Não existe comentário de uma linha (//) em CSS — se você escrever //, a regra seguinte quebra em silêncio.
  • Espaços e quebras de linha são livres: h1{color:red} e o bloco acima são idênticos para o navegador. Escreva para humanos.

2.2 Como o navegador aplica o CSS

🔎 Por baixo do capô Quando a página carrega, o navegador faz três coisas em sequência: (1) lê o HTML e monta a árvore DOM (a estrutura de elementos que você vê na aba Elements); (2) lê o CSS e monta a CSSOM, a árvore de regras; (3) combina as duas na árvore de renderização, calculando para cada elemento quais regras se aplicam e qual é o valor final de cada propriedade — é isso que a aba Computed do DevTools mostra. Só então ele calcula posições e tamanhos (layout) e pinta os pixels (paint). Por isso um CSS externo referenciado no <head> bloqueia a renderização até ser baixado: o navegador se recusa a mostrar a página com a aparência errada e depois "piscar" para a certa.

Você vai ver esse fluxo na prática na Aula 09, quando medir o custo de animações. Por ora, o que importa: o CSS não "pinta por cima" do HTML — ele participa da construção da página desde o início.

2.3 Um pouco de história

🧠 Você sabia? Nos primeiros anos da Web, quem decidia a aparência de uma página era o navegador, não o autor. Um <h1> era grande e em negrito porque o Mosaic decidiu assim. Quando o CSS foi proposto — por Håkon Wium Lie, que trabalhava com Tim Berners-Lee no CERN — a ideia central não era dar poder total ao autor, e sim criar uma cascata: o estilo final resulta da combinação entre o que o navegador sugere, o que o usuário prefere e o que o autor define. Essa ideia continua viva: se um usuário com baixa visão aumenta o tamanho da fonte no navegador, um site feito com rem respeita a escolha. Um site feito com px a ignora. O "C" de CSS existe para proteger o usuário.

3. As três formas de aplicar CSS

3.1 Inline — evite

HTML
<p style="color: red; font-size: 18px;">Texto</p>

Problemas: mistura conteúdo com apresentação, não é reaproveitável (cada parágrafo precisa repetir o estilo), tem prioridade altíssima (difícil de sobrescrever — você verá o porquê na Aula 06) e polui o HTML. Uso legítimo: quando o valor é gerado dinamicamente por JavaScript — uma barra de progresso cuja largura muda a cada segundo, por exemplo.

3.2 Interna (<style> no <head>) — só para testes

HTML
<head>
  <meta charset="UTF-8">
  <title>Protótipo</title>
  <style>
    body { font-family: sans-serif; }
    h1 { color: #0b3d5c; }
  </style>
</head>

Vale para protótipos rápidos, exemplos de aula e páginas únicas que nunca terão uma segunda. Não é reaproveitável entre páginas nem cacheável.

3.3 Externa — a forma correta

index.html (no <head>)

HTML
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semana Acadêmica de Sistemas de Informação</title>
  <link rel="stylesheet" href="css/estilo.css">
</head>

css/estilo.css

CSS
body {
  font-family: sans-serif;
}

h1 {
  color: #0b3d5c;
}

Vantagens:

  • Um arquivo serve o site inteiro. Mudou a cor do título? Mudou nas cinco páginas.
  • Cache. O navegador baixa estilo.css uma vez e reaproveita nas outras páginas — a segunda página abre mais rápido.
  • Separação de responsabilidades. HTML descreve; CSS apresenta; na Unidade 3, JavaScript comporta.
  • Trabalho em equipe. Uma pessoa mexe no conteúdo enquanto outra mexe no estilo, sem conflito.

Todos os trabalhos desta trilha usam CSS externo. O href segue as mesmas regras de caminho relativo dos links e imagens (Aula 02): css/estilo.css está na pasta css, ao lado do HTML.

⚠️ Atenção Uma página pode ter vários <link rel="stylesheet">. Eles são aplicados na ordem em que aparecem: quando duas regras iguais conflitam, a do arquivo que veio por último vence. Guarde isso — é a terceira etapa da cascata, que você formaliza na Aula 06.

4. O modelo de caixa (box model)

Todo elemento HTML é uma caixa retangular. Não importa se é um título, uma imagem, um link ou um <span> no meio de uma frase: para o navegador, é um retângulo com quatro camadas. Entender isso resolve 80% dos problemas de layout que você vai encontrar.

Texto
┌───────────────────────────────────────────────┐
│                    MARGIN                     │   espaço EXTERNO (transparente)
│   ┌───────────────────────────────────────┐   │
│   │                BORDER                 │   │   a borda
│   │   ┌───────────────────────────────┐   │   │
│   │   │            PADDING            │   │   │   espaço INTERNO (recebe o fundo)
│   │   │   ┌───────────────────────┐   │   │   │
│   │   │   │        CONTENT        │   │   │   │   width × height
│   │   │   │                       │   │   │   │
│   │   │   └───────────────────────┘   │   │   │
│   │   └───────────────────────────────┘   │   │
│   └───────────────────────────────────────┘   │
└───────────────────────────────────────────────┘
Camada O que é Recebe a cor de fundo?
Content O conteúdo em si: texto, imagem, filhos Sim
Padding Espaço entre o conteúdo e a borda Sim
Border A linha ao redor Tem cor própria
Margin Espaço entre esta caixa e as vizinhas Não (sempre transparente)
CSS
.caixa {
  width: 300px;
  height: 150px;
  padding: 20px;
  border: 5px solid #0b3d5c;
  margin: 30px;
  background-color: #f0f3f6;   /* pinta content + padding, não a margem */
}

4.1 box-sizing — a configuração que muda tudo

Pergunta: quantos pixels de largura a .caixa acima ocupa na tela?

Com o comportamento padrão, chamado content-box, width define só o conteúdo. O padding e a borda são somados por fora:

Texto
300 (width) + 20 + 20 (padding) + 5 + 5 (border) = 350 px de largura ocupada

Você pediu 300 px e recebeu 350. Em um layout de duas colunas de 50% cada, basta um padding para a segunda coluna não caber e cair para a linha de baixo. Isso quebra layouts em porcentagem constantemente.

Com border-box, width passa a incluir padding e borda: a caixa ocupa exatamente 300 px, e o conteúdo fica com o que sobra (250 px). É como você pensa intuitivamente — "quero uma caixa de 300" — e é assim que todo projeto profissional trabalha. Por isso todo CSS desta trilha começa com este reset:

css/estilo.css (topo do arquivo)

CSS
/* 1. Reset e box-sizing */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

Leia: "todo elemento (*), inclusive os pseudoelementos ::before e ::after (Aula 06), usa border-box e começa sem margem nem padding". O zerar de margens tira os espaçamentos padrão do navegador (os <h1> e <p> vêm com margens que variam entre navegadores) para que você decida cada espaço.

🔬 Investigue No DevTools, aba Elements, selecione qualquer elemento e olhe o painel Styles: role até o fim e você verá o diagrama do box model daquele elemento, com os valores de margin, border, padding e content. Passe o mouse sobre cada camada — o navegador destaca a área correspondente na página em cores diferentes (laranja para margem, verde para padding, azul para conteúdo). Agora, na aba Computed, procure box-sizing: aplique o reset acima e veja o valor mudar de content-box para border-box — e a largura ocupada cair de 350 para 300.

4.2 Notação abreviada (shorthand)

margin e padding aceitam de um a quatro valores, sempre no sentido horário a partir do topo:

CSS
margin: 10px;                    /* todos os lados */
margin: 10px 20px;               /* vertical | horizontal */
margin: 10px 20px 30px;          /* topo | horizontal | base */
margin: 10px 20px 30px 40px;     /* topo | direita | base | esquerda (sentido horário) */

margin: 0 auto;                  /* centraliza horizontalmente um bloco COM width definida */

padding-top: 10px;               /* um lado só */
margin-inline: auto;             /* esquerda + direita (equivale a margin-left + margin-right) */
margin-block: 2rem;              /* topo + base */

margin: 0 auto é o truque clássico de centralização: as margens laterais automáticas dividem igualmente o espaço que sobra. Só funciona quando o elemento é de bloco e tem largura menor que o pai — se ocupar 100%, não sobra nada para dividir. A versão moderna, margin-inline: auto, foi a que você usou no .container.

4.3 Colapso de margens

Margens verticais adjacentes entre elementos irmãos não se somam: prevalece a maior.

CSS
.a { margin-bottom: 30px; }
.b { margin-top: 20px; }
/* O espaço entre .a e .b é 30px, não 50px */

Isso é comportamento especificado, não bug — existe desde os primeiros dias do CSS para que parágrafos com margin: 1em 0 fiquem espaçados por 1em, e não por 2em. Três regras para não se surpreender:

  1. Só acontece com margens verticais (topo e base). Margens horizontais sempre se somam.
  2. Acontece também entre pai e primeiro/último filho: a margin-top do primeiro <p> dentro de uma <section> sem padding nem borda "vaza" para fora da seção.
  3. Não acontece em Flexbox nem em Grid (Aula 07) — o que é um dos motivos de esses modelos serem mais previsíveis.

4.4 Bordas

CSS
border: 2px solid #333;          /* largura | estilo | cor */
border-bottom: 3px dashed red;   /* um lado só */
border-radius: 8px;              /* cantos arredondados */
border-radius: 50%;              /* círculo perfeito, se a caixa for quadrada */

Estilos disponíveis: solid, dashed, dotted, double, none. A borda só aparece se os três valores estiverem presentes (uma borda sem estilo é invisível — erro frequente).

4.5 Outros ajustes da caixa

CSS
.foto-palestrante {
  width: 100%;          /* ocupa toda a largura do pai */
  max-width: 400px;     /* mas nunca passa de 400 px */
  height: auto;         /* mantém a proporção */
}

.resumo {
  max-width: 65ch;      /* no máximo 65 caracteres por linha: legibilidade */
  min-height: 120px;    /* nunca menor que isso, mesmo com pouco texto */
}

.cartao {
  overflow: hidden;     /* corta o que passar da caixa (auto: barra de rolagem; visible: vaza) */
}

max-width é mais útil que width na maior parte dos casos: define um teto e deixa o elemento encolher em telas pequenas. Você usará isso o tempo todo na Aula 08.

5. display: como a caixa se comporta no fluxo

O HTML renderiza os elementos em fluxo normal: de cima para baixo, da esquerda para a direita. A propriedade display define como cada caixa participa desse fluxo.

CSS
display: block;          /* ocupa toda a largura disponível, aceita width/height. Padrão de div, p, h1, section, ul */
display: inline;         /* ocupa só o necessário, na linha do texto. IGNORA width/height e margens verticais. Padrão de span, a, strong, em */
display: inline-block;   /* fica na linha do texto, MAS aceita width/height e margens verticais */
display: none;           /* remove do fluxo — não ocupa espaço, como se não existisse */
display: flex;           /* Aula 07 */
display: grid;           /* Aula 07 */
Valor Quebra linha? Aceita width/height? Exemplos padrão
block Sim, antes e depois Sim div, p, h1, ul, section
inline Não Não span, a, strong, em, code
inline-block Não Sim img, button, input, select
none Some <template>, elementos com hidden

O caso clássico: você quer um <a> que pareça um botão, com 200 px de largura e padding vertical. Como <a> é inline, width e margin-top são ignorados. Solução: display: inline-block — o link continua na linha (pode ficar ao lado de outro) e passa a aceitar dimensões.

CSS
.botao {
  display: inline-block;
  width: 200px;
  padding: 12px 0;
  text-align: center;
  background-color: #0b3d5c;
  color: #fff;
}

5.1 Três formas de "esconder", três resultados diferentes

Propriedade Ocupa espaço Clicável Lido por leitor de tela
display: none Não Não Não
visibility: hidden Sim Não Não
opacity: 0 Sim Sim Sim

Escolha pela intenção: display: none para remover de verdade (um menu fechado); visibility: hidden para reservar o lugar (um placeholder que vai aparecer); opacity: 0 para animações de aparecer/desaparecer (Aula 09) — porque um botão com opacity: 0 continua clicável e continua sendo lido, e isso é um bug de acessibilidade quando não é intencional. O atributo HTML hidden equivale a display: none.

6. Um gostinho do que vem: herança, estados e variáveis

Para o Laboratório de hoje você precisa de três ideias que a Aula 06 aprofunda. Aqui vai o mínimo para usá-las.

6.1 Algumas propriedades são herdadas

CSS
body {
  font-family: "Segoe UI", Roboto, Arial, sans-serif;
  color: #333;
  line-height: 1.6;
}
/* Todo o site herda a fonte, a cor e a altura de linha — escreva uma vez, vale para tudo */
Herdadas (passam de pai para filho) Não herdadas (cada elemento define a sua)
color, font-family, font-size, font-weight margin, padding, border
line-height, text-align, letter-spacing width, height, background
visibility, cursor, list-style display, position, overflow

A lógica: propriedades de texto são herdadas (faz sentido que o parágrafo tenha a fonte do body); propriedades de caixa não são (não faz sentido que todo filho de uma caixa com borda ganhe borda).

6.2 Estados com :hover e :focus

Um seletor pode mirar um estado do elemento — mouse por cima, foco pelo teclado — com as pseudoclasses:

CSS
.botao:hover {
  background-color: #1a7fb5;   /* mouse por cima */
}

.botao:focus-visible {
  outline: 3px solid #f0a500;  /* foco visível pelo teclado */
  outline-offset: 2px;
}

⚠️ Atenção Nunca escreva outline: none sem colocar outro indicador no lugar. Quem navega pelo teclado (por deficiência motora, por preferência ou porque o mouse quebrou) depende do contorno de foco para saber onde está. Um site sem foco visível é inutilizável por teclado — não importa quão bonito ele pareça.

6.3 Variáveis CSS

Em vez de repetir #0b3d5c em vinte lugares, declare uma vez e reutilize:

CSS
:root {
  --cor-primaria: #0b3d5c;
  --cor-secundaria: #1a7fb5;
  --espaco: 16px;
}

.botao {
  background-color: var(--cor-primaria);
  padding: var(--espaco);
}

.botao:hover {
  background-color: var(--cor-secundaria);
}

:root é o <html>; declarar ali torna a variável visível na página inteira. Mudou a cor da marca? Uma linha. Na Aula 06 você monta um sistema completo de variáveis; hoje basta usar duas ou três.

6.4 Por que id não é para estilo e !important não é solução

Você verá na Aula 06 que o navegador decide conflitos entre regras por especificidade: um #id vence qualquer quantidade de .classes, e !important vence tudo. Parece prático — até que você precisa sobrescrever e não consegue. Regra desta trilha desde hoje: estilize com classes (reutilizáveis e fáceis de sobrescrever), reserve id para âncoras e JavaScript, e não use !important — se precisou dele, há um problema de organização no CSS, e o remédio é reorganizar, não escalar a guerra.

💻 Mão na massa — Esqueleto semântico das cinco páginas e a primeira folha de estilo

Hoje as cinco páginas do site do evento — todas já existentes desde as Aulas 02 e 03 — ganham o esqueleto semântico correto, contato.html é reestruturada (o formulário do B5 da Aula 03 continua lá) e nasce o arquivo css/estilo.css, que crescerá a cada aula da Unidade 2.

Passo 1 — criar css/estilo.css com reset, contêiner e base

Crie a pasta css e, dentro dela, o arquivo:

css/estilo.css

CSS
/* ==========================================================
   Semana Acadêmica de Sistemas de Informação — folha de estilo
   Ordem: 1. reset · 2. variáveis · 3. base · 4. layout · 5. componentes
   ========================================================== */

/* 1. Reset e box-sizing */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* 2. Variáveis */
:root {
  --cor-primaria: #0b3d5c;
  --cor-secundaria: #1a7fb5;
  --cor-texto: #333333;
  --cor-fundo: #f7f9fb;
  --espaco: 16px;
}

/* 3. Base */
body {
  font-family: "Segoe UI", Roboto, Arial, sans-serif;
  line-height: 1.6;
  color: var(--cor-texto);
  background-color: var(--cor-fundo);
}

img,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

/* 4. Layout */
.container {
  width: 100%;
  max-width: 1100px;               /* largura do projeto, fixada de vez */
  margin-inline: auto;
  padding-inline: 1rem;
}

header {
  background-color: var(--cor-primaria);
  color: #ffffff;
  padding: var(--espaco) 0;
}

header a {
  color: #ffffff;
}

main {
  padding: calc(var(--espaco) * 2) 0;
}

main > .container > section {
  margin-bottom: calc(var(--espaco) * 2);
}

aside {
  padding: var(--espaco) 0;
  background-color: #e8eef3;
}

footer {
  background-color: var(--cor-primaria);
  color: #ffffff;
  padding: var(--espaco) 0;
  text-align: center;
}

footer a {
  color: #ffffff;
}

/* 5. Componentes */
.cartao {
  background-color: #ffffff;
  border: 1px solid #d9e0e7;
  border-radius: 8px;
  padding: var(--espaco);
  margin-bottom: var(--espaco);
}

.botao {
  display: inline-block;
  padding: 12px 24px;
  background-color: var(--cor-primaria);
  color: #ffffff;
  text-decoration: none;
  border-radius: 8px;
}

.botao:hover {
  background-color: var(--cor-secundaria);
}

.botao:focus-visible {
  outline: 3px solid #f0a500;
  outline-offset: 2px;
}

A regra img, video { max-width: 100%; height: auto; display: block; } merece atenção: sem ela, uma foto de 1600 px vaza para fora do contêiner em telas menores. display: block remove o pequeno espaço que aparece embaixo de imagens inline (elas ficam alinhadas à linha de base do texto, como se fossem letras).

Passo 2 — ligar a folha de estilo nas cinco páginas

Em cada página, dentro do <head>, depois do <title>:

index.html, programacao.html, inscricao.html, palestrantes.html, contato.html (no <head>)

HTML
<link rel="stylesheet" href="css/estilo.css">

Salve e olhe o Live Server: o cabeçalho fica azul-escuro, a fonte muda, as margens padrão somem. Se nada mudou, veja a aba Network: o estilo.css deve aparecer com status 200. Um 404 significa caminho errado.

Passo 3 — reestruturar index.html

A página inicial já tem conteúdo desde a Aula 02 (as seções "Sobre o evento", "Como participar" e "Glossário") e o banner responsivo da Aula 04. Nada disso sai. O que muda é o esqueleto em volta: cada região ganha o seu <div class="container">, nasce uma seção de abertura (.hero), nascem os cartões de destaque, e o conteúdo tangencial vai para um <aside>.

index.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Semana Acadêmica de Sistemas de Informação: três dias de palestras, minicursos e oficinas para estudantes e profissionais de tecnologia.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Início — Semana Acadêmica de Sistemas de Informação</title>
  <link rel="stylesheet" href="css/estilo.css">
</head>
<body>
  <header id="topo">
    <div class="container">
      <img src="img/logo-sasi.svg" alt="" width="160" height="48">
      <h1>Semana Acadêmica de Sistemas de Informação</h1>
      <p>Três noites de outubro · Auditório Central</p>
      <nav aria-label="Principal">
        <ul>
          <li><a href="index.html" aria-current="page">Início</a></li>
          <li><a href="programacao.html">Programação</a></li>
          <li><a href="inscricao.html">Inscrição</a></li>
          <li><a href="palestrantes.html">Palestrantes</a></li>
          <li><a href="contato.html">Contato</a></li>
        </ul>
      </nav>
    </div>
  </header>

  <main>
    <div class="container">
      <section class="hero">
        <h2>Três noites de palestras, minicursos e maratona de programação</h2>
        <p>Participação gratuita, com certificado de 20 horas para quem comparecer a pelo menos 75% das atividades.</p>
        <a href="inscricao.html" class="botao">Inscreva-se</a>
      </section>

      <!-- O <picture> do banner (Aula 04) e as seções "Sobre o evento",
           "Como participar" e "Glossário" (Aula 02) continuam aqui,
           exatamente como estavam. -->

      <section class="destaques">
        <h2>Destaques desta edição</h2>

        <article class="cartao">
          <header>
            <h3>Minicurso: Git e GitHub do zero</h3>
            <p><time datetime="20:00">20h00</time> · Laboratório 2 · 30 vagas</p>
          </header>
          <p>Do primeiro commit ao pull request, em duas horas de prática.</p>
          <footer>
            <a href="inscricao.html">Garantir vaga</a>
          </footer>
        </article>

        <article class="cartao">
          <header>
            <h3>Maratona de programação</h3>
            <p><time datetime="18:30">18h30</time> · Laboratórios 1 e 2 · equipes de três</p>
          </header>
          <p>Problemas de lógica, ranking em tempo real e premiação no encerramento.</p>
          <footer>
            <a href="inscricao.html">Formar equipe</a>
          </footer>
        </article>

        <article class="cartao">
          <header>
            <h3>Mesa-redonda: mercado de trabalho em Sinop</h3>
            <p><time datetime="20:00">20h00</time> · Sala 105</p>
          </header>
          <p>Egressos do curso contam como conseguiram o primeiro emprego na região.</p>
          <footer>
            <a href="programacao.html">Ver programação</a>
          </footer>
        </article>
      </section>

      <section class="numeros">
        <h2>O evento em números</h2>
        <dl>
          <dt>Edições realizadas</dt>
          <dd>7</dd>
          <dt>Participantes na última edição</dt>
          <dd>180</dd>
          <dt>Horas de atividade</dt>
          <dd>21</dd>
        </dl>
      </section>
    </div>
  </main>

  <aside>
    <div class="container">
      <h2>Edições anteriores</h2>
      <p>Veja a <a href="palestrantes.html">galeria de palestrantes</a> e o <a href="programacao.html">vídeo de abertura</a> da edição passada.</p>
    </div>
  </aside>

  <footer>
    <div class="container">
      <p>Realização: Comissão Organizadora da Semana Acadêmica de Sistemas de Informação.</p>
      <p>
        <a href="mailto:contato@semanasi.com.br">contato@semanasi.com.br</a> ·
        <a href="tel:+556635111000">(66) 3511-1000</a>
      </p>
      <p>&copy; Semana Acadêmica de Sistemas de Informação. Todos os direitos reservados.</p>
      <nav aria-label="Rodapé">
        <ul>
          <li><a href="contato.html">Contato</a></li>
          <li><a href="https://www.w3.org/WAI/" target="_blank" rel="noopener noreferrer">Acessibilidade (W3C)</a></li>
        </ul>
      </nav>
    </div>
  </footer>
</body>
</html>

Três observações sobre o que não mudou: o <h1> continua no <header> (ele nomeia o site, não a página), o rodapé continua com os três parágrafos da Aula 02 — agora com um <nav> a mais — e as seções que a Aula 02 escreveu continuam no <main>, só que dentro do contêiner.

Passo 4 — reestruturar contato.html

A quinta página existe desde a Aula 02 (esqueleto) e recebeu o formulário de mensagem no exercício B5 da Aula 03 — com nome, e-mail, assunto, mensagem e a escolha da forma de resposta em rádios. Esse formulário não é reescrito: ele é transplantado para dentro do novo esqueleto, ganhando apenas o <div class="container"> em volta e a classe .botao no botão de envio. O resto da página junta o que você aprendeu nas Aulas 02 a 04: tabela, links tel:/mailto: e mapa em <iframe>.

contato.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Fale com a organização da Semana Acadêmica de Sistemas de Informação: telefone, e-mail, formulário e localização.">
  <title>Contato — Semana Acadêmica de Sistemas de Informação</title>
  <link rel="stylesheet" href="css/estilo.css">
</head>
<body>
  <header id="topo">
    <div class="container">
      <img src="img/logo-sasi.svg" alt="" width="160" height="48">
      <h1>Semana Acadêmica de Sistemas de Informação</h1>
      <p>Três noites de outubro · Auditório Central</p>
      <nav aria-label="Principal">
        <ul>
          <li><a href="index.html">Início</a></li>
          <li><a href="programacao.html">Programação</a></li>
          <li><a href="inscricao.html">Inscrição</a></li>
          <li><a href="palestrantes.html">Palestrantes</a></li>
          <li><a href="contato.html" aria-current="page">Contato</a></li>
        </ul>
      </nav>
    </div>
  </header>

  <main>
    <div class="container">
      <h2>Contato</h2>

      <section>
        <h3>Fale com a organização</h3>
        <address>
          <p>Telefone: <a href="tel:+5566999990000">(66) 99999-0000</a></p>
          <p>E-mail: <a href="mailto:sasi@semanasi.com.br?subject=Contato%20pelo%20site">sasi@semanasi.com.br</a></p>
          <p>Sala 12 do Auditório Central, em Sinop</p>
        </address>
      </section>

      <section>
        <h3>Horário de atendimento</h3>
        <table>
          <caption>Atendimento presencial na sala da organização</caption>
          <thead>
            <tr>
              <th scope="col">Dia</th>
              <th scope="col">Manhã</th>
              <th scope="col">Noite</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <th scope="row">Segunda a quinta</th>
              <td>8h às 11h</td>
              <td>19h às 21h</td>
            </tr>
            <tr>
              <th scope="row">Sexta</th>
              <td>8h às 11h</td>
              <td>Fechado</td>
            </tr>
          </tbody>
        </table>
      </section>

      <section>
        <h3>Envie uma mensagem</h3>
        <form action="/contato" method="post">
          <p>
            <label for="nome">Nome completo</label>
            <input type="text" id="nome" name="nome" required minlength="5" autocomplete="name">
          </p>
          <p>
            <label for="email">E-mail</label>
            <input type="email" id="email" name="email" required autocomplete="email">
          </p>
          <p>
            <label for="assunto">Assunto</label>
            <select id="assunto" name="assunto" required>
              <option value="">Selecione</option>
              <option value="inscricao">Dúvida sobre inscrição</option>
              <option value="certificado">Certificado</option>
              <option value="patrocinio">Patrocínio</option>
              <option value="outro">Outro</option>
            </select>
          </p>
          <p>
            <label for="mensagem">Mensagem</label>
            <textarea id="mensagem" name="mensagem" rows="6" maxlength="600" required></textarea>
          </p>

          <fieldset>
            <legend>Como prefere receber a resposta?</legend>
            <p>
              <input type="radio" id="resposta-email" name="resposta" value="email" checked>
              <label for="resposta-email">Por e-mail</label>
            </p>
            <p>
              <input type="radio" id="resposta-telefone" name="resposta" value="telefone">
              <label for="resposta-telefone">Por telefone</label>
            </p>
          </fieldset>

          <p>
            <label for="telefone">Telefone (opcional, necessário se escolher resposta por telefone)</label>
            <input type="tel" id="telefone" name="telefone" autocomplete="tel"
                   placeholder="(66) 99999-0000">
          </p>

          <button type="submit" class="botao">Enviar mensagem</button>
        </form>
      </section>

      <section>
        <h3>Como chegar</h3>
        <iframe src="https://www.google.com/maps/embed?pb=CODIGO_GERADO_PELO_MAPS"
                title="Mapa: Auditório Central, em Sinop"
                width="600" height="450"
                loading="lazy"
                referrerpolicy="no-referrer-when-downgrade">
        </iframe>
      </section>
    </div>
  </main>

  <footer>
    <div class="container">
      <p>Semana Acadêmica de Sistemas de Informação</p>
    </div>
  </footer>
</body>
</html>

Repare no <address>: é o elemento semântico para informações de contato do autor ou da organização responsável pela página — exatamente este caso.

Passo 5 — aplicar o contêiner nas outras três páginas

Em programacao.html, inscricao.html e palestrantes.html, envolva o conteúdo de <header>, <main> e <footer> com <div class="container">, como no Passo 3. O conteúdo em si não muda. Esse é um trabalho mecânico de cinco minutos — use a busca e substituição do VS Code (Ctrl+H) para o cabeçalho e o rodapé, que são idênticos em todas.

Aproveite para conferir, em cada página: há um único <h1> (o do <header>)? O título da página é o <h2> no topo do <main>? Em palestrantes.html, cada convidado já é um <article> desde a Aula 02 — acrescente a ele a classe cartao, que a folha de estilo do Passo 1 acabou de definir, e o <h3> continua sendo o nome da pessoa. Em programacao.html, a tabela permanece como está; ela ganha estilo na Aula 06.

Passo 6 — diagnóstico e validação

  1. Cole o comando de contorno da seção 1 no console de cada página e procure: conteúdo fora de <main>, <section> sem título, <div> que poderia ser um elemento semântico.
  2. Na aba Elements, selecione o .container do <main> e confira no painel Styles que width resolve para no máximo 1100 px e que as margens laterais são iguais.
  3. Redimensione a janela até 400 px: o conteúdo deve manter 1 rem de respiro em cada lado, sem barra de rolagem horizontal.
  4. Valide as cinco páginas no W3C. Meta: zero erros.

Como testar

  • As cinco páginas abrem com o mesmo cabeçalho azul-escuro, a mesma fonte e o mesmo rodapé — sinal de que o CSS externo está ligado em todas.
  • O conteúdo fica centralizado em monitores largos e ocupa a largura toda (menos o respiro) em janelas estreitas.
  • O botão "Inscreva-se" da página inicial muda de cor ao passar o mouse e mostra um contorno amarelo ao ser alcançado com Tab.
  • Em contato.html, o telefone e o e-mail são clicáveis, a tabela tem cabeçalhos, o formulário valida e o mapa aparece.
  • Nenhuma imagem vaza para fora do contêiner em 400 px de largura.
  • O DevTools mostra box-sizing: border-box em qualquer elemento que você selecionar.
  • Zero erros no validador nas cinco páginas.

🧪 Laboratório

Nível A — Fixação

A1. Quais são as três formas de aplicar CSS? Qual delas deve ser usada em projetos reais e por quê? Cite um caso legítimo para cada uma das outras duas.

A2. Cite quatro propriedades herdadas e quatro não herdadas. Explique a lógica que separa os dois grupos.

A3. Um elemento tem width: 200px, padding: 15px e border: 5px. Qual a largura total ocupada com box-sizing: content-box? E com border-box? Em qual dos dois o conteúdo fica com menos espaço?

A4. Explique o colapso de margens com um exemplo numérico. Diga em que situação ele não ocorre.

A5. Diferencie display: none, visibility: hidden e opacity: 0 quanto a espaço ocupado, clicabilidade e leitura por leitor de tela. Para cada um, dê um uso adequado.

A6. O que faz margin: 0 auto? Que condição o elemento precisa satisfazer para funcionar? Qual a alternativa moderna com a mesma função?

A7. Por que !important deve ser evitado? O que geralmente indica quando alguém sente necessidade de usá-lo?

A8. Cite três elementos de seccionamento do HTML5 e o papel de cada um. O que são "marcos" para um leitor de tela?

A9. Quantos <main> uma página pode ter? Por quê? E quantos <header>?

A10. Qual a diferença entre <section> e <article>? Dê um exemplo de cada, retirado do site do evento.

A11. Quando o uso de <div> é a escolha correta? Dê o exemplo usado nesta aula.

A12. Escreva o esqueleto semântico completo de uma página institucional (sem conteúdo, só os elementos), incluindo o padrão de contêiner centralizado em cada região.

A13. Um <a> recebe width: 200px e margin-top: 20px, mas nada muda na tela. Por quê? Como corrigir sem tirar o link da linha do texto?

A14. Preveja o que acontece com a regra abaixo e explique o motivo:

CSS
h1 {
  color: #0b3d5c
  font-size: 2rem;
}

Nível B — Aplicação

B1. Crie três cartões (article.cartao) com: fundo branco, border-radius, box-shadow, padding interno, título, parágrafo e um link com classe .botao. No :hover, o cartão deve mudar de sombra e o botão de cor. Use apenas variáveis CSS para todas as cores e espaçamentos.

Resultado esperado: três cartões idênticos em estrutura, com sombra suave; ao passar o mouse, a sombra fica mais forte e o botão muda de cor; nenhum valor de cor ou espaçamento aparece fora de :root; o foco do botão via Tab é visível.

Dica

box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) é uma sombra discreta; no :hover, troque por 0 6px 16px rgba(0, 0, 0, 0.2). Declare as duas como variáveis (--sombra-cartao e --sombra-cartao-hover). O :hover do cartão é .cartao:hover; o do botão dentro do cartão em hover é .cartao:hover .botao.

B2. Crie um menu de navegação horizontal usando display: inline-block nos itens (Flexbox só na Aula 07), com espaçamento entre eles, :hover com mudança de cor de fundo, :focus-visible visível e o item ativo destacado por uma classe .ativo (ou pelo atributo aria-current).

Resultado esperado: os cinco links ficam lado a lado na mesma linha, com espaço entre eles; passar o mouse muda o fundo do item; navegar por Tab mostra o contorno; o item da página atual tem aparência distinta sem depender do mouse.

Dica

li { display: inline-block; } coloca os itens na linha; o espaço entre eles vem de margin-right no li ou de padding no a. Para o item ativo sem criar classe, use o seletor de atributo a[aria-current="page"] — você já colocou esse atributo no HTML. Um pequeno espaço em branco entre os itens aparece por causa das quebras de linha do HTML: por enquanto, aceite; na Aula 07 o Flexbox resolve.

B3. Monte o esqueleto semântico das cinco páginas do seu projeto autoral, aplicando o padrão de contêiner centralizado em header, main e footer, ligando uma folha de estilo externa com o reset e as variáveis base, e valide todas no W3C.

Resultado esperado: cinco páginas com um único <h1> cada, <main> único, <nav aria-label>, conteúdo centralizado com respiro nas laterais; o mesmo css/estilo.css ligado em todas; zero erros no validador.

Dica

Comece pela página mais simples e copie o <head>, o <header> e o <footer> para as outras. Cole o comando de contorno no console de cada uma e procure conteúdo fora de <main>. Se o validador acusar "Element h1 not allowed" ou "Section lacks heading", releia a seção 1.2.

B4. Pegue a página abaixo, feita só com <div>, e reescreva-a usando os elementos de seccionamento adequados. Documente, em comentários HTML, cada troca e o motivo.

HTML
<div id="topo">
  <div class="logo"><img src="logo.png"></div>
  <div class="menu">
    <a href="index.html">Início</a>
    <a href="noticias.html">Notícias</a>
  </div>
</div>
<div id="conteudo">
  <div class="titulo">Últimas notícias</div>
  <div class="noticia">
    <div class="titulo-noticia">Inscrições abertas</div>
    <div class="data">segunda-feira</div>
    <div class="texto">As inscrições começaram hoje.</div>
  </div>
  <div class="noticia">
    <div class="titulo-noticia">Palestrante confirmada</div>
    <div class="data">terça-feira</div>
    <div class="texto">Ana Souza falará sobre HTML semântico.</div>
  </div>
</div>
<div id="lateral">
  <div class="titulo">Links úteis</div>
  <a href="https://www.wikipedia.org">Wikipédia</a>
</div>
<div id="rodape">Todos os direitos reservados</div>

Resultado esperado: zero <div> com significado disfarçado; títulos como <h1>/<h2>/<h3>; cada notícia é um <article> com <time>; o menu é uma lista dentro de <nav>; a imagem tem alt; zero erros no validador; pelo menos oito comentários explicando as trocas.

Dica

Faça a pergunta da seção 1.2 para cada div: "existe um elemento que descreva o que isto é?". topo é um cabeçalho; menu é navegação; conteudo é o principal; noticia faz sentido sozinha; lateral é tangencial; rodape é rodapé. Os "títulos" são cabeçalhos de nível 1, 2 e 3. A div que sobra, se sobrar, é o contêiner centralizado.

Nível C — Desafio

C1. Engenharia reversa de um site real. Escolha a página inicial de um site institucional de verdade (o portal da sua universidade ou escola, o site de uma prefeitura, o gov.br). Com o DevTools, mapeie a estrutura de marcos dela: quantos <header>, <nav>, <main>, <section>, <article>, <aside> e <footer> existem, e quantas <div>. Depois reconstrua o esqueleto dessa página (sem copiar conteúdo — use textos próprios) em HTML semântico, com o contêiner centralizado, e escreva um parágrafo comparando: o site original usa os marcos corretamente? O que você faria diferente?

Dica

No console, document.querySelectorAll("div").length conta as div; troque por main, nav, article para os outros. Para ver os marcos como um leitor de tela vê, instale a extensão Accessibility Insights ou use a aba Accessibility do DevTools (painel Elements → Accessibility → Accessibility Tree). Sites com dezenas de div e nenhum main são comuns — e é exatamente isso que vale a pena registrar.

🏆 Desafios

⭐ A folha de estilo que não funciona

cssbugdevtools

Alguém passou uma hora tentando descobrir por que "o CSS não pega". O arquivo está abaixo, e o HTML o referencia com <link rel="stylesheet" href="estilo.css"> — mas o arquivo está salvo em css/estilo.css. Além desse, há seis erros no CSS. Encontre todos os sete usando só o DevTools (painel Styles mostra declarações inválidas riscadas; a aba Network mostra o 404) antes de recorrer a qualquer validador.

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: Arial, sans-serif
  color: #333;
}

// Cabeçalho
header {
  background-color: darkblu;
  padding: 16px;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
}

nav a {
  width: 120px;
  margin-top: 8px;
  color: white;
}

.cartao {
  border: 2px #0b3d5c;
  padding: 16px;
}

.aviso {
  display: inline;
  width: 100%;
  background-color: #fff3cd;
}

Critérios de pronto

  • Lista numerada com os sete problemas: linha, sintoma observável na página ou no DevTools, causa e correção.
  • O CSS corrigido, com as regras que dependiam de display funcionando (a largura dos links e do aviso aplicada de fato).
  • Uma frase explicando o que o painel Styles mostra quando uma declaração é inválida, e o que a aba Network mostra quando o arquivo não é encontrado.
Pistas
  1. Dois erros são de sintaxe pura: um ; que falta e um comentário no formato errado. Releia a seção 2.1 sobre o que acontece com a declaração seguinte em cada caso.
  2. Um nome de cor está escrito errado; o DevTools risca a declaração inválida e mostra um triângulo amarelo ao lado.
  3. Uma borda precisa de três valores para aparecer (seção 4.4).
  4. Dois blocos aplicam width e margin-top a elementos que, por padrão, ignoram essas propriedades (seção 5).
⭐⭐

⭐⭐ Reproduza o cartão de repositório do GitHub

csslayoutinvestigacao

Abra o perfil de qualquer pessoa no GitHub e olhe os cartões de "repositórios fixados" (pinned): ícone, nome em azul, descrição em cinza, etiqueta da linguagem com uma bolinha colorida, contador de estrelas. Parece simples — e é o tipo de componente que você fará dezenas de vezes na carreira. Reproduza um cartão idêntico usando só o que esta aula ensinou: box model, display: inline-block, bordas, cores e variáveis. Sem Flexbox, sem Grid, sem position. Use o DevTools para descobrir os valores reais de padding, borda, raio e cores do cartão original.

Critérios de pronto

  • O cartão tem borda de 1 px, cantos arredondados, padding interno e largura máxima iguais aos do original (medidos no DevTools, com tolerância de 2 px).
  • O nome do repositório é um link; a bolinha da linguagem é um elemento inline-block de 12×12 px com border-radius: 50%.
  • Todas as cores estão em variáveis em :root, com os valores extraídos do original.
  • Três cartões lado a lado (inline-block) cabem em 1200 px sem quebrar; em 400 px, ficam um embaixo do outro.
  • Um comentário no CSS lista os valores originais que você mediu e onde os encontrou no DevTools (painel Styles ou Computed).
Pistas
  1. No DevTools, selecione o cartão original e leia a aba Computed: ela mostra o valor final de padding, border-radius, border-color e background-color já resolvidos.
  2. Elementos inline-block lado a lado ganham um espaço em branco entre si por causa das quebras de linha no HTML; reduza-o escrevendo as tags coladas ou aceite-o por enquanto.
  3. A bolinha e o nome da linguagem ficam na mesma linha porque ambos são inline/inline-block; vertical-align: middle alinha a bolinha ao texto.
  4. Para os três cartões caberem em 1200 px, calcule: com border-box, três caixas de 32% mais o espaço em branco entre elas passam de 100%? Ajuste até caber.
⭐⭐⭐

⭐⭐⭐ Laboratório interativo do box model

csslayoutdevtoolsinvestigacao

Explicar o box model para outra pessoa é a melhor forma de descobrir se você entendeu. Construa uma página didática, box-model.html, que demonstre visualmente cada conceito desta aula com exemplos vivos: a mesma caixa em content-box e border-box lado a lado, com as medidas escritas; três casos de colapso de margem (irmãos, pai e filho, sem colapso dentro de um elemento com padding); block, inline e inline-block recebendo a mesma width e reagindo de formas diferentes; e as três formas de esconder (display: none, visibility: hidden, opacity: 0) com um botão em cada uma para o leitor testar o clique. Tudo com HTML semântico e CSS externo — nada de JavaScript.

Critérios de pronto

  • Cada demonstração é uma <section> com título, um parágrafo explicando o que observar e o exemplo vivo ao lado do código-fonte correspondente em <pre><code>.
  • As medidas reais de cada caixa (largura ocupada, espaço entre elementos) aparecem escritas na página e conferem com o que o DevTools mostra.
  • As caixas usam cores diferentes para content, padding e border, visíveis a olho nu.
  • Alguém que nunca viu CSS consegue, lendo só a página, responder às questões A3, A4, A5 e A13 deste Laboratório — teste com uma pessoa nessas condições e registre as respostas.
  • A página valida no W3C e usa o padrão de contêiner centralizado.
Pistas
  1. Para mostrar o código-fonte na página, escreva o HTML dentro de <pre><code> trocando < por < — senão o navegador renderiza em vez de exibir.
  2. Para tornar visível o padding, use background-clip: content-box em um elemento e compare com o padrão border-box: a cor de fundo mostra onde termina o conteúdo.
  3. O caso "pai e filho" do colapso fica evidente se você der um fundo à <section> pai: a margem do primeiro filho aparece fora do fundo. Adicione padding-top: 1px ao pai e veja a margem voltar para dentro.
  4. Para os botões de "esconder", coloque três <a href="#a05-clicou"> com cada técnica e peça ao leitor para tentar clicar e usar Tab: só o de opacity: 0 responde.

Para ir além: publique a página (Aula 15 ou trilha Deploy) e use-a como material de revisão sempre que precisar retomar o assunto.

🔥

🔥 Boss — Auditoria e reconstrução de um site real

htmlacessibilidadeprojetoinvestigacao

Sites institucionais brasileiros — de prefeituras, secretarias, campi — costumam ter dezenas de <div>, imagens sem alt, formulários sem <label> e tabelas usadas para layout. Você agora sabe reconhecer tudo isso. Escolha um site institucional real (que não seja o mesmo usado no C1), audite as suas quatro páginas principais com tudo o que a Unidade 1 ensinou e reconstrua essas páginas em HTML semântico, acessível e válido, com conteúdo próprio inspirado no original. É o mini-projeto que fecha a unidade: arquitetura da Web, estrutura de documento, textos, links, tabelas, formulários, mídias, listas e seccionamento — tudo junto.

Critérios de pronto

  • Relatório de auditoria (uma a duas páginas) com, para cada uma das quatro páginas originais: número de <div> e de marcos semânticos, erros do validador W3C, imagens sem alt, campos sem <label>, tabelas usadas para layout, peso total na aba Network e nota de Acessibilidade no Lighthouse.
  • Quatro páginas reconstruídas, interligadas por menu em <nav>, com: <main> único, um <h1> por página, hierarquia de títulos correta, os três tipos de lista, uma tabela de dados com caption/thead/th scope, um formulário com no mínimo seis campos e label em todos, uma imagem em <figure> com alt adequado, um vídeo ou áudio com controls e um <iframe> com title.
  • Folha de estilo externa única com reset, box-sizing: border-box, variáveis base e o contêiner centralizado — e nada além disso (a estilização completa é assunto da Unidade 2).
  • Zero erros no validador nas quatro páginas; nota de Acessibilidade no Lighthouse igual ou superior a 90.
  • Tabela comparativa final: original × reconstrução, nas mesmas métricas do relatório.
  • README no repositório com o link do site original, as decisões de estrutura e o que você aprendeu.
Pistas
  1. Para a auditoria, o console resolve as contagens: document.querySelectorAll("div").length, document.querySelectorAll("img:not([alt])").length, document.querySelectorAll("input:not([id])").length.
  2. O Lighthouse está na aba homônima do DevTools; rode só a categoria Accessibility para ser rápido, e leia os itens reprovados — eles apontam o elemento exato.
  3. Comece a reconstrução pelo esqueleto (Passo 3 da Mão na massa) e só depois preencha o conteúdo; um esqueleto validado evita retrabalho.
  4. Conteúdo "inspirado no original" significa mesma estrutura de informação (seções, tipos de dado) com textos seus — não copie parágrafos nem imagens sem licença.

Para ir além: este Boss pode virar parte do seu Marco 1 — mostre-o junto com o restante do projeto.

🐛 Erros comuns

Sintoma Causa Solução
O CSS "não pega" em nenhuma página; a aba Network mostra estilo.css com status 404 Caminho errado no href do <link> (arquivo em css/estilo.css, link apontando para estilo.css) Corrija o caminho relativo à página; confira maiúsculas e a extensão
Uma regra inteira é ignorada e a seguinte também Faltou ; ao fim de uma declaração — o navegador junta as duas e descarta Todo propriedade: valor termina com ;; o DevTools risca a declaração inválida
Tudo abaixo de um comentário para de funcionar Comentário escrito com // — não existe em CSS Use /* comentário */
p .classe não estiliza o parágrafo com a classe Espaço acidental: p .classe seleciona descendentes com a classe dentro de <p> Sem espaço (p.classe) para "parágrafo que tem a classe"
Duas colunas de 50% não cabem lado a lado e a segunda cai box-sizing: content-box somando padding e borda por fora Reset com box-sizing: border-box no topo do CSS
margin: 0 auto não centraliza Elemento sem width/max-width (ocupa 100%, não sobra espaço) ou elemento inline Defina uma largura e garanta display: block
width e margin-top em um <a> ou <span> não fazem nada Elemento inline ignora dimensões e margens verticais display: inline-block (ou block)
A borda não aparece border: 2px #333 sem o estilo (solid) border: 2px solid #333 — os três valores
O espaço entre dois blocos é menor que a soma das margens Colapso de margens verticais Comportamento esperado; use só uma das margens ou padding no pai
O foco do teclado sumiu dos links outline: none no :focus Substitua por um indicador visível (outline colorido ou box-shadow)
A imagem vaza para fora do contêiner em telas estreitas <img> sem max-width: 100% Regra base img { max-width: 100%; height: auto; }
Tudo em px e o usuário que aumentou a fonte do navegador não é atendido Unidades absolutas para tipografia rem para texto (Aula 06)

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). SILVA, M. S. Criando sites com HTML, capítulos sobre estrutura de página e introdução ao CSS. Na MDN em pt-BR: "Estrutura de documento e site" e "O modelo de caixa" (links em Para aprofundar).

Parte 2 — Produção (30 min). Exercício B3: o esqueleto semântico das cinco páginas do seu projeto autoral, validado no W3C, mais a primeira folha de estilo (css/estilo.css) com reset, box-sizing: border-box global, variáveis de cores base e o contêiner centralizado aplicado nas cinco páginas.

Critério de pronto: as cinco páginas abrem com o mesmo cabeçalho e rodapé estilizados; document.querySelectorAll("main").length retorna 1 em cada página; o DevTools mostra box-sizing: border-box em qualquer elemento; zero erros no validador.

Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: publique o tema do seu projeto e o wireframe das páginas nas três larguras (celular, tablet, desktop) — pode ser desenhado à mão e fotografado. Os três marcos da trilha acompanham este mesmo projeto, cada um acrescentando uma camada (HTML no Marco 1, CSS no Marco 2, JavaScript no Marco 3). Se puder, compare o wireframe com o de outra pessoa, apontando um problema de hierarquia visual.

✅ Checkpoint do projeto

Ao fim desta aula — e da Unidade 1 — o repositório do seu projeto autoral deve ter:

  • [ ] Cinco páginas interligadas por um menu em <nav aria-label> com aria-current na página atual.
  • [ ] Em cada página: <header>, <main> único, <footer>, um único <h1> e seções com título.
  • [ ] <article> para cada conteúdo autocontido (cartões, palestrantes, notícias, produtos do seu domínio).
  • [ ] Padrão de contêiner centralizado (<div class="container">) em header, main e footer.
  • [ ] css/estilo.css ligado em todas as páginas, começando pelo reset com box-sizing: border-box.
  • [ ] Bloco :root com pelo menos três variáveis (cor primária, cor secundária, espaçamento).
  • [ ] Regra base img, video { max-width: 100%; height: auto; display: block; }.
  • [ ] Um .botao com :hover e :focus-visible visíveis.
  • [ ] Página de contato com <address>, links tel:/mailto:, tabela com caption e formulário com label em todos os campos.
  • [ ] Zero erros no validador W3C nas cinco páginas.
  • [ ] Tema e wireframe do projeto autoral registrados (no fórum da turma ou nas suas próprias anotações).

Isso encerra a Unidade 1.

📚 Para aprofundar

Na próxima aula você entra no CSS de verdade — seletores, cascata, especificidade, cores, unidades, tipografia e variáveis — e é também o dia do Marco 1: o site em HTML puro, com tudo o que a Unidade 1 ensinou. As instruções completas estão na Aula 06, e o escopo são os itens não-CSS do Checkpoint acima.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Elementos HTML para layout1.1 O esqueleto de uma página1.2 section × article × div: a pergunta certa1.3 Mais de um <nav>1.4 O padrão de contêiner centralizado2. O que é CSS2.1 Sintaxe2.2 Como o navegador aplica o CSS2.3 Um pouco de história3. As três formas de aplicar CSS3.1 Inline — evite3.2 Interna (<style> no <head>) — só para testes3.3 Externa — a forma correta4. O modelo de caixa (box model)4.1 box-sizing — a configuração que muda tudo4.2 Notação abreviada (shorthand)4.3 Colapso de margens4.4 Bordas4.5 Outros ajustes da caixa5. display: como a caixa se comporta no fluxo5.1 Três formas de "esconder", três resultados diferentes6. Um gostinho do que vem: herança, estados e variáveis6.1 Algumas propriedades são herdadas6.2 Estados com :hover e :focus6.3 Variáveis CSS6.4 Por que id não é para estilo e !important não é solução💻 Mão na massa — Esqueleto semântico das cinco páginas e a primeira folha de estiloPasso 1 — criar css/estilo.css com reset, contêiner e basePasso 2 — ligar a folha de estilo nas cinco páginasPasso 3 — reestruturar index.htmlPasso 4 — reestruturar contato.htmlPasso 5 — aplicar o contêiner nas outras três páginasPasso 6 — diagnóstico e validaçãoComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ A folha de estilo que não funciona⭐⭐ Reproduza o cartão de repositório do GitHub⭐⭐⭐ Laboratório interativo do box model🔥 Boss — Auditoria e reconstrução de um site real🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 2 · CSS: estilo, layout e responsividade3 aulas de 50 min + 1 h EADFecha a unidade · Marco 1

Aula 06 — CSS: sintaxe, seletores, classes, atributos e valores

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Escrever seletores precisos usando tipo, classe, id, atributo, combinadores, pseudoclasses e pseudoelementos — e explicar o que cada um alcança.
  • Prever qual regra vence um conflito, calculando a especificidade e aplicando as três etapas da cascata.
  • Distinguir propriedades herdadas de não herdadas e usar a herança para escrever menos CSS.
  • Escolher e escrever cores nas notações hexadecimal, rgb() e hsl(), e verificar o contraste segundo a WCAG.
  • Justificar o uso de unidades relativas (rem, em, %, vw, ch) em vez de px e evitar a armadilha do em acumulado.
  • Definir uma escala tipográfica legível: pilha de fontes, tamanhos, line-height e largura de linha.
  • Montar um sistema de design em variáveis CSS e organizar a folha de estilo em seções previsíveis.

📋 Pré-requisitos

  • [ ] Pasta site-evento/ com as cinco páginas em HTML semântico validadas no W3C e css/estilo.css ligado em todas elas (Aula 05).
  • [ ] VS Code com Live Server e um navegador com DevTools. Hoje o painel Styles e a aba Computed são as ferramentas principais.
  • [ ] Revisar da Aula 05: sintaxe de uma regra CSS (seletor, declaração, bloco), o modelo de caixa, box-sizing: border-box, os valores de display e o gostinho de herança, :hover e variáveis da seção 6.
  • [ ] Marco 1 pronto — o site em HTML puro. As instruções completas estão no fim desta aula.

Na aula passada você deu ao site do evento um esqueleto semântico correto e escreveu a primeira folha de estilo: um reset com box-sizing: border-box, três variáveis, uma tipografia base e alguns componentes. Aquela folha funciona, mas não escala: as cores estão espalhadas, os seletores são genéricos demais e, quando duas regras brigam, você ainda não sabe explicar quem ganha. Hoje isso muda — e o site ganha um sistema de design de verdade. Esta aula abre a Unidade 2 e é também o dia do Marco 1.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Seletores completos: tipo, classe, id, atributo, combinadores, pseudoclasses e pseudoelementos
2 50 min Cascata, especificidade e herança; cores e contraste; unidades relativas; tipografia
3 50 min Variáveis CSS e organização da folha; Mão na massa: o sistema de design do site; Marco 1 do projeto

1. Seletores

Um seletor responde a uma pergunta: quais elementos desta página recebem este estilo? Quanto mais preciso o seletor, menos você luta contra o próprio CSS depois.

1.1 Os quatro seletores básicos

CSS
*           { }   /* universal: todos os elementos */
p           { }   /* tipo (ou elemento): todos os <p> */
.destaque   { }   /* classe: todos os elementos com class="destaque" */
#cabecalho  { }   /* id: o único elemento com id="cabecalho" */
  • Universal (*) existe para o reset. Fora dele, quase nunca é a ferramenta certa.
  • Tipo serve para estilos base: como deve ser todo parágrafo, todo link, toda tabela deste site.
  • Classe é o seletor do dia a dia. Reutilizável, aplicável a qualquer elemento, fácil de sobrescrever.
  • id identifica um elemento único na página. Para estilo, evite: ele tem especificidade altíssima e não é reutilizável.

💡 Dica A regra desta trilha, que você já conhece desde a Aula 05: estilize com classes; reserve id para âncoras (#topo), para o atributo for dos rótulos e para o JavaScript da Unidade 3. Se um estilo aparece uma vez hoje, quase certamente vai aparecer duas amanhã — e uma classe já estará pronta.

1.2 Agrupamento

Uma vírgula aplica a mesma regra a vários seletores:

CSS
h1, h2, h3 {
  font-family: var(--fonte-base);
  line-height: 1.2;
}

Cuidado com o efeito colateral: se um dos seletores da lista estiver errado, alguns navegadores antigos descartavam a regra inteira. Hoje isso não acontece mais com seletores válidos, mas continua valendo a disciplina de agrupar apenas o que é realmente comum.

1.3 Combinadores: seletores que descrevem relações

CSS
article p   { }   /* descendente: todo <p> dentro de <article>, em qualquer nível */
article > p { }   /* filho direto: só os <p> filhos imediatos de <article> */
h2 + p      { }   /* irmão adjacente: o <p> imediatamente depois de um <h2> */
h2 ~ p      { }   /* irmãos gerais: todos os <p> depois de um <h2>, no mesmo pai */
Texto
<article>            article p   pega os 3 parágrafos
  <p>A</p>           article > p pega só A e C
  <div>              h2 + p      pega só o parágrafo logo após o h2
    <p>B</p>         h2 ~ p      pega todos os parágrafos irmãos depois do h2
  </div>
  <h2>Título</h2>
  <p>C</p>
</article>

O combinador de irmão adjacente resolve um problema clássico de tipografia: dar uma margem maior ao parágrafo que abre uma seção, sem tocar nos outros.

CSS
h2 + p {
  font-size: 1.125rem;   /* o parágrafo de abertura é um pouco maior */
}

1.4 Um espaço muda tudo

CSS
p.destaque   { }   /* um <p> QUE TEM a classe destaque */
p .destaque  { }   /* qualquer elemento com classe destaque DENTRO de um <p> */
.cartao.ativo  { }   /* um elemento que tem AMBAS as classes */
.cartao .ativo { }   /* um elemento com classe ativo dentro de um .cartao */

Esse espaço é o erro de digitação mais caro do CSS iniciante: o estilo "simplesmente não pega" e não há mensagem de erro nenhuma. Quando isso acontecer, o primeiro passo é ler o seletor em voz alta, palavra por palavra: "pê ponto destaque" (o mesmo elemento) ou "pê espaço ponto destaque" (um dentro do outro)?

🔬 Investigue Abra index.html no Live Server e o DevTools. No painel Elements, selecione um <h2> e olhe a aba Styles: à direita de cada regra aparece o arquivo e a linha de origem; regras riscadas foram vencidas por outras. Agora, na aba Console, digite document.querySelectorAll("main p").length e depois document.querySelectorAll("main > p").length. Os números são diferentes — e a diferença é exatamente o que o combinador > exclui. Repita com .cartao p, .cartao > p e h2 + p. O querySelectorAll aceita os mesmos seletores do CSS, e é a maneira mais rápida de conferir se um seletor pega o que você acha que pega.

1.5 Seletores de atributo

CSS
a[target]              { }   /* tem o atributo, qualquer que seja o valor */
a[target="_blank"]     { }   /* valor exato */
a[href^="https"]       { }   /* COMEÇA com (^ = começo) */
a[href$=".pdf"]        { }   /* TERMINA com ($ = fim) */
a[href*="wikipedia"]   { }   /* CONTÉM em qualquer posição */
input[type="checkbox"] { }   /* muito usado em formulários */

Eles são poderosos justamente porque estilizam pelo significado do HTML, sem exigir uma classe extra. Dois usos que você vai aplicar hoje:

CSS
/* Avisa visualmente que o link sai do site */
a[href^="http"]::after {
  content: " ↗";
}

/* Avisa que o link baixa um arquivo pesado */
a[href$=".pdf"]::after {
  content: " (PDF)";
  font-size: .875rem;
}

No site do evento, o menu vai usar a[aria-current="page"] para destacar a página atual — o mesmo atributo que você aprendeu na Aula 04 por acessibilidade vira, de graça, um seletor de estilo. Essa é a recompensa de escrever HTML semântico: o CSS fica mais curto.

1.6 Pseudoclasses: o estado do elemento

Uma pseudoclasse (um : simples) mira um elemento em determinado estado.

CSS
a:link           { }   /* link ainda não visitado */
a:visited        { }   /* já visitado */
a:hover          { }   /* mouse por cima */
a:active         { }   /* no instante do clique */
a:focus          { }   /* com foco (teclado ou clique) */
a:focus-visible  { }   /* com foco E o navegador julga útil mostrar o anel */

input:required   { }
input:checked    { }
input:disabled   { }
input:valid      { }
input:invalid    { }
input:placeholder-shown { }

⚠️ Atenção Ordem obrigatória das pseudoclasses de link: LVHA:link, :visited, :hover, :active. Como todas têm a mesma especificidade, quem vem depois vence; escrever :hover antes de :visited faz o :hover "não funcionar" em links já visitados. O mnemônico clássico é LoVe HAte.

:focus ou :focus-visible? Use :focus-visible. Ele mostra o anel de foco quando a navegação é por teclado e o esconde quando o foco veio de um clique de mouse — exatamente o comportamento que as pessoas esperam. E nunca escreva outline: none sem colocar outro indicador visível no lugar: um site sem foco visível é inutilizável por teclado, ponto.

1.7 Pseudoclasses estruturais

CSS
li:first-child      { }   /* o primeiro filho */
li:last-child       { }   /* o último filho */
li:only-child       { }   /* quando é o único */
tr:nth-child(3)     { }   /* o terceiro */
tr:nth-child(odd)   { }   /* ímpares: 1, 3, 5… */
tr:nth-child(even)  { }   /* pares: 2, 4, 6… */
li:nth-child(3n)    { }   /* de 3 em 3: 3, 6, 9… */
li:nth-child(3n+1)  { }   /* 1, 4, 7… */
p:not(.destaque)    { }   /* todos os <p> que NÃO têm a classe destaque */

:nth-child(even) é o que dá as linhas alternadas de uma tabela — o famoso "zebrado" que você vai aplicar na programação do evento. Já :not() evita regras duplicadas: em vez de escrever um estilo para todos os parágrafos e depois desfazê-lo em alguns, você exclui de uma vez.

📌 Vale gravar :nth-child(n) conta todos os irmãos, não só os do mesmo tipo. Em <div><h2></h2><p>A</p><p>B</p></div>, o parágrafo A é o segundo filho — p:nth-child(1) não pega nada. Quando você quer contar só os do mesmo tipo, existe :nth-of-type(n). Essa diferença é pegadinha clássica de entrevista e bug certo na vida real.

1.8 Pseudoelementos: partes que não existem no HTML

Um pseudoelemento (dois-pontos duplos, ::) cria ou alcança uma parte virtual do elemento.

CSS
p::first-line   { font-weight: 600; }
p::first-letter { font-size: 3rem; line-height: 1; }

.aviso::before  { content: "⚠ "; }
.externo::after { content: " ↗"; }

li::marker      { color: var(--cor-secundaria); }   /* o marcador da lista */
::selection     { background: #0b3d5c; color: #fff; }   /* texto selecionado */

A propriedade content é obrigatória em ::before e ::after: sem ela, o pseudoelemento não é criado — nem mesmo com content: "" esquecido. Esse é o motivo número um de "meu ::after não aparece".

⚠️ Atenção Conteúdo criado por content é decoração, não informação. Ele pode não ser lido por leitores de tela, não é copiado de forma confiável e some se o CSS não carregar. Use ::before/::after para ícones, setas e aspas decorativas — nunca para texto que a pessoa precisa ler para entender a página. A seta de link externo é aceitável porque a informação essencial ("este link sai do site") também deve estar no texto ou no title do link.

🧠 Você sabia? A sintaxe de dois-pontos duplos (::before) só apareceu no CSS3, justamente para distinguir pseudoelementos (partes virtuais) de pseudoclasses (estados). Antes disso, tudo usava um dois-pontos só, e os navegadores aceitam :before até hoje por compatibilidade retroativa. Nunca deixaram de aceitar — porque remover algo da Web quebra páginas que ninguém pode mais consertar. É a mesma lógica que mantém <form> funcionando desde 1993, e o motivo pelo qual o CSS tem tantos "dois jeitos de fazer a mesma coisa". Escreva sempre com ::; leia : sem estranhar.

2. A cascata: como o navegador decide

Duas regras diferentes atingem o mesmo <h2> e dizem cores diferentes. Quem ganha? O navegador decide em três etapas, nesta ordem.

2.1 Etapa 1 — origem e importância

Da menor para a maior prioridade:

  1. Estilos padrão do navegador (é por isso que um <h1> já nasce grande e em negrito).
  2. Estilos do usuário (folha de estilo pessoal, configurações de acessibilidade).
  3. Estilos do autor — você, no estilo.css.
  4. Declarações !important do autor.
  5. Declarações !important do usuário — a última palavra é sempre de quem lê a página.

Repare na inversão do topo: o !important do usuário vence o do autor. Isso é proposital. Alguém que precisa de fonte gigante ou de altíssimo contraste para conseguir ler tem prioridade sobre o seu design.

⚠️ Atenção !important resolve o conflito de agora e cria um problema permanente: para sobrescrevê-lo, só com outro !important mais específico — e a folha vira uma escada de gritos. Regra desta trilha: !important é proibido nos trabalhos, salvo justificativa escrita (a única exceção que você vai encontrar é dentro do bloco prefers-reduced-motion da Aula 09). Se precisou dele, o problema é de arquitetura, e o remédio é reorganizar os seletores.

2.2 Etapa 2 — especificidade

Se duas regras têm a mesma origem, vence a mais específica. Conte cada seletor como um trio (A, B, C):

Peso O que conta Exemplos
A id #menu
B Classes, atributos e pseudoclasses .cartao, [type="text"], :hover
C Elementos e pseudoelementos p, div, ::before

O universal (*) e os combinadores (>, +, ~) valem zero. :not() não conta em si, mas o que está dentro dele conta.

Seletor A, B, C Leitura
p 0, 0, 1 0-0-1
.destaque 0, 1, 0 0-1-0
p.destaque 0, 1, 1 0-1-1
nav ul li a 0, 0, 4 0-0-4
.menu a 0, 1, 1 0-1-1
#menu 1, 0, 0 1-0-0
#menu li a:hover 1, 1, 2 1-1-2
style="…" (inline) Vence tudo, exceto !important

Compara-se A primeiro; só em caso de empate compara-se B; e, empatando de novo, C. Um id vence qualquer quantidade de classes: #menu (1-0-0) ganha de .nav .lista .item .link (0-4-0). É exatamente por isso que estilizar com id é uma armadilha — você constrói uma parede que só um id maior derruba.

Um caso real que você vai encontrar na Aula 07. Um menu com um link em formato de botão:

CSS
.menu a      { color: var(--cor-texto); }   /* 0-1-1 */
.menu__cta   { color: #ffffff; }            /* 0-1-0 — PERDE */
.menu a.menu__cta { color: #ffffff; }       /* 0-2-1 — ganha */

O botão ficaria com texto escuro sobre fundo escuro, e nada no console avisaria. Quando um estilo "não pega", a especificidade é a primeira suspeita — e o DevTools mostra a regra vencida riscada.

2.3 Etapa 3 — ordem no arquivo

Empatou em especificidade? Vence a última regra escrita.

CSS
.botao { background-color: #0b3d5c; }
.botao { background-color: #1a7fb5; }   /* esta vence: veio depois */

Isso vale também entre arquivos: se a página tem dois <link rel="stylesheet">, o segundo sobrescreve o primeiro em caso de empate. E é por isso que a ordem das seções dentro da folha (seção 7 desta aula) importa: base primeiro, componentes depois, utilitários por último.

2.4 Herança

Algumas propriedades passam automaticamente de pai para filho. Você já viu isso na Aula 05; agora vale a tabela completa e a lógica por trás.

Herdadas (passam para os filhos) Não herdadas (cada elemento define a sua)
color, font-family, font-size, font-weight margin, padding, border
line-height, text-align, letter-spacing width, height, background
visibility, cursor, list-style display, position, overflow

A lógica: propriedades de texto são herdadas (faz sentido que o parágrafo use a fonte do body); propriedades de caixa não são (não faz sentido que todo filho de uma caixa com borda ganhe borda também).

A herança é o que permite escrever pouco CSS:

CSS
body {
  font-family: var(--fonte-base);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--cor-texto);
}
/* O site inteiro herda estas quatro declarações. */

Quatro palavras-chave funcionam em qualquer propriedade e controlam a herança na mão:

CSS
.filho {
  border: inherit;   /* força a herança de uma propriedade que não é herdada */
  color: initial;    /* volta ao valor padrão da especificação (preto, no caso) */
  padding: unset;    /* herda se for herdável; volta ao inicial se não for */
  margin: revert;    /* volta ao valor que o NAVEGADOR daria */
}

revert é a mais útil das quatro: ela desfaz o seu CSS sem desfazer o do navegador. Útil quando um reset agressivo tirou algo que você quer de volta em um caso específico.

3. Cores

3.1 As notações

CSS
color: red;                       /* 1. nome — 147 nomes padronizados */
color: #0b3d5c;                   /* 2. hexadecimal: #RRGGBB */
color: #f0a;                      /* 3. hex abreviado = #ff00aa */
color: #0b3d5c80;                 /* 4. hex com alfa (últimos dois dígitos) */
color: rgb(11, 61, 92);           /* 5. RGB decimal, 0 a 255 */
color: rgba(11, 61, 92, 0.5);     /* 6. RGB com transparência, 0 a 1 */
color: hsl(203, 79%, 20%);        /* 7. matiz, saturação, luminosidade */
color: hsla(203, 79%, 20%, 0.5);  /* 8. HSL com transparência */

O hexadecimal é a notação mais comum porque é a que as ferramentas de design cospem. Mas ele é ilegível para humanos: olhando #0b3d5c você não sabe se é claro ou escuro, nem qual é o "primo mais claro" dessa cor.

3.2 HSL: a notação que serve para pensar

hsl() descreve a cor como uma pessoa descreveria:

  • H (hue, matiz): a cor em si, em graus de 0 a 360 no círculo cromático (0 vermelho, 120 verde, 240 azul).
  • S (saturation, saturação): quanto de cor, de 0% (cinza) a 100% (vibrante).
  • L (lightness, luminosidade): de 0% (preto) a 100% (branco); 50% é a cor "pura".

Isso torna trivial construir uma paleta coerente: mantenha o H, varie o L.

CSS
:root {
  --azul-900: hsl(203, 79%, 20%);   /* mais escuro  — texto sobre claro */
  --azul-700: hsl(203, 79%, 32%);
  --azul-500: hsl(203, 75%, 40%);   /* a cor "da marca" */
  --azul-300: hsl(203, 60%, 70%);
  --azul-100: hsl(203, 60%, 94%);   /* mais claro — fundos suaves */
}

Cinco tons que combinam entre si, escritos em cinco linhas, sem nenhuma ferramenta. Tente fazer isso adivinhando hexadecimais.

💡 Dica No DevTools, clique no quadradinho de cor ao lado de qualquer declaração de color ou background: abre o seletor de cores. Clique no rótulo do formato (embaixo, à direita do valor) e ele converte entre hex, rgb e hsl na hora. É a maneira mais rápida de descobrir o hsl() equivalente a um hexadecimal que veio de um layout.

E o oklch()? Existe uma notação mais nova, oklch(luminosidade croma matiz) — por exemplo, oklch(45% 0.09 240). Ela resolve um defeito do HSL: dois tons com o mesmo L em HSL podem parecer bem diferentes em brilho para o olho humano (compare um amarelo e um azul com L: 50%). O oklch é perceptualmente uniforme, ou seja, mesma luminosidade significa mesmo brilho aparente. Já funciona em todos os navegadores atuais e vale conhecer; nesta trilha continuamos com hsl() e hexadecimal, que é o que você vai encontrar em 99% do código existente.

3.3 Fundos

CSS
background-color: #f5f5f5;
background-image: url("../img/textura.png");
background-repeat: no-repeat;
background-position: center;
background-size: cover;      /* cobre todo o espaço, cortando o excesso */
background-size: contain;    /* cabe inteira, podendo sobrar espaço */

/* Forma abreviada: cor, imagem, repetição, posição / tamanho */
background: #f5f5f5 url("../img/fundo.jpg") no-repeat center / cover;

/* Gradientes são imagens, não cores */
background-image: linear-gradient(to right, #0b3d5c, #1a7fb5);
background-image: linear-gradient(160deg, #0b3d5c 0%, #1a7fb5 100%);
background-image: radial-gradient(circle, #ffffff, #cccccc);

Repare no caminho url("../img/textura.png"): dentro de css/estilo.css, o ../ sobe para a raiz do site antes de entrar em img/. Caminhos em CSS são relativos ao arquivo CSS, não ao HTML — é a causa mais comum de imagem de fundo que não aparece.

3.4 Contraste é requisito, não gosto

A WCAG exige 4,5:1 de contraste entre texto e fundo (3:1 para texto grande — 24 px, ou 18,5 px em negrito). Texto cinza-claro sobre branco reprova; texto branco sobre amarelo reprova com folga.

Verifique em https://webaim.org/resources/contrastchecker/ — dois campos, um resultado, dez segundos. As cores do site do evento:

Par (texto sobre fundo) Razão Serve para
#0b3d5c sobre #ffffff 11,4:1 Texto, títulos, links — passa AAA
#ffffff sobre #0b3d5c 11,4:1 Cabeçalho, rodapé e botão primário
#333333 sobre #f7f9fb 12,0:1 Corpo de texto do site inteiro
#ffffff sobre #1a7fb5 4,4:1 Só texto grande e bordas — reprova em texto normal

Essa última linha é o tipo de descoberta que o verificador entrega e o olho não: #1a7fb5 parece perfeitamente legível, e falha por pouco. Por isso --cor-secundaria é usada, no site do evento, em bordas, ícones e estado :hover — nunca como fundo de texto pequeno.

⚠️ Atenção Nunca comunique uma informação por cor. Campo inválido com borda vermelha e mais nada exclui quem tem daltonismo (cerca de 8% dos homens). Acrescente sempre um segundo sinal: um ícone, um texto, uma mudança de espessura de borda. Isso entra no checklist de qualidade dos Marcos 2 e 3.

4. Unidades

4.1 Absolutas

Unidade Uso recomendado
px Pixel de referência. Previsível — bom para bordas, sombras e raios
pt, cm, mm, in Impressão. Não use para tela

4.2 Relativas — prefira sempre

Unidade Relativa a Observação
% Uma medida do elemento pai Ótima para largura
em font-size do próprio elemento (ou do pai, quando aplicada ao próprio font-size) Acumula em aninhamentos
rem font-size da raiz (<html>) Previsível; padrão para tipografia
vw / vh 1% da largura / altura da janela 100vh é a altura total da tela
vmin / vmax Menor / maior dimensão da janela Útil em elementos quadrados
ch Largura do caractere "0" da fonte atual Ideal para limitar a linha de texto
CSS
html  { font-size: 100%; }        /* respeita o padrão do navegador: 16px */
h1    { font-size: 2rem; }        /* 32px */
p     { font-size: 1rem; }        /* 16px */
small { font-size: 0.875rem; }    /* 14px */

⚠️ Atenção A armadilha do em. Se .pai { font-size: 1.5em } e, dentro dele, .filho { font-size: 1.5em }, o filho fica com 2,25× o tamanho base. Em três níveis, 3,4×. Com rem isso não acontece, porque a referência é sempre a raiz — a fonte de um item de menu não muda porque alguém aninhou mais uma <div>. Use rem para tipografia e em só quando quiser essa proporcionalidade local (o padding de um botão que cresce junto com o texto dele, por exemplo).

🧠 Você sabia? Nunca escreva html { font-size: 14px } para "deixar tudo menor". Essa linha é uma das piores coisas que se pode fazer com acessibilidade: uma pessoa com baixa visão que configurou o navegador para fonte 20 px acabou de ter a escolha dela anulada — e não faz ideia do porquê. Todo o valor do rem está em ele ser relativo a uma raiz que o usuário controla. Se você precisa de tudo menor, reduza os valores em rem das suas regras, não a raiz. html { font-size: 100% } é a única declaração de tamanho de raiz aceitável nesta trilha.

4.3 Fazendo contas: calc(), min(), max() e clamp()

CSS
width:  calc(100% - 40px);        /* largura total menos um espaço fixo */
height: calc(100vh - 80px);       /* tela inteira menos o cabeçalho */

width: min(1100px, 100% - 2rem);  /* o MENOR dos dois: nunca estoura a tela */
width: max(300px, 30%);           /* o MAIOR dos dois: nunca fica estreito demais */

font-size: clamp(1rem, 2.5vw, 1.5rem);  /* mínimo, ideal fluido, máximo */

Os espaços em volta dos operadores de calc() são obrigatórios: calc(100%-40px) não funciona, e o navegador não avisa. Motivo histórico: sem espaço, -40px seria interpretado como um número negativo, não como uma subtração.

clamp() é a peça central da tipografia fluida da Aula 08 — por ora, guarde a leitura: "no mínimo 1rem, idealmente 2,5% da largura da janela, no máximo 1.5rem".

5. Tipografia

Tipografia é 90% da aparência de um site. Antes de escolher cor, escolha bem a fonte, o tamanho e o espaçamento.

5.1 A pilha de fontes

CSS
body {
  font-family: "Inter", Arial, sans-serif;
}

A pilha é lida da esquerda para a direita: o navegador usa a primeira que existir no sistema do usuário. Sempre termine com uma família genérica (sans-serif, serif, monospace) — é a garantia de que existe um fim de linha.

Nomes com espaço vão entre aspas ("Segoe UI"). Uma pilha "de sistema" — que usa a fonte nativa de cada sistema operacional e não baixa nada — é uma escolha profissional e rápida:

CSS
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;

5.2 As propriedades

CSS
body {
  font-family: var(--fonte-base);
  font-size: 1rem;
  font-weight: 400;         /* 100 a 900; 400 = normal, 700 = negrito */
  font-style: normal;       /* normal | italic | oblique */
  line-height: 1.6;         /* SEM unidade = múltiplo do font-size. Recomendado */
  letter-spacing: 0.01em;
  color: var(--cor-texto);
}

h1 {
  font-size: 2rem;
  line-height: 1.2;         /* títulos pedem entrelinha menor */
  text-transform: none;     /* uppercase | lowercase | capitalize */
  text-align: left;         /* center | right | justify */
}

a {
  text-decoration: underline;
  text-underline-offset: 2px;   /* afasta o sublinhado da base das letras */
}

💡 Dica line-height sem unidade (1.6) é sempre a escolha certa. Com unidade (line-height: 24px ou 1.6em), o valor calculado é herdado pelos filhos — então um título de 2rem dentro de um body com line-height: 24px herda 24 px de entrelinha e as letras se sobrepõem. Sem unidade, o que se herda é o fator, e cada elemento calcula a própria entrelinha a partir do próprio tamanho.

5.3 Uma escala tipográfica

Não escolha tamanhos no chute. Parta de 1rem e multiplique por uma razão constante (1,25 é uma boa escolha para telas):

Nome Valor Uso
2.5rem 40 px Título da página inicial
2rem 32 px h1
1.5rem 24 px h2
1.25rem 20 px h3
1rem 16 px Corpo de texto
0.875rem 14 px Legendas, metadados, rodapé

Seis tamanhos bastam para um site inteiro. Se você precisou de um sétimo, provavelmente precisava era de outro peso (font-weight) ou de outra cor.

5.4 Fontes externas (Google Fonts)

HTML
<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@400;600;700&display=swap"
      rel="stylesheet">
CSS
body { font-family: "Inter", Arial, sans-serif; }
  • preconnect abre a conexão com o servidor de fontes antes de ela ser necessária — economiza cerca de 100 ms.
  • wght@400;600;700 importa três pesos. Cada peso é um arquivo baixado: importar oito "por precaução" pode custar centenas de kilobytes e atrasar a primeira pintura da página.
  • display=swap mostra o texto imediatamente com a fonte de reserva e troca quando a externa chegar. Sem ele, alguns navegadores escondem o texto por até três segundos.

🔎 Por baixo do capô A troca de fonte tem dois nomes que aparecem em toda auditoria de performance: FOUT (flash of unstyled text, o texto aparece com a fonte de reserva e "pula" quando a definitiva carrega) e FOIT (flash of invisible text, o texto simplesmente não aparece até a fonte chegar). display=swap escolhe deliberadamente o FOUT — porque texto visível com a fonte errada é melhor do que página em branco. Para reduzir o "pulo", escolha uma fonte de reserva com largura parecida (Arial para Inter, Georgia para uma serifada) — é o motivo real de a pilha de fontes ter mais de um nome.

5.5 Legibilidade: quatro números que resolvem

  • Corpo de texto entre 1rem e 1.125rem. Menor que isso, cansa.
  • line-height entre 1.5 e 1.7 para texto corrido.
  • Largura de linha entre 45 e 75 caracteres — em CSS, max-width: 65ch. Linha muito longa faz o olho perder o começo da seguinte.
  • Evite text-align: justify na web: sem hifenização, ele abre "rios" de espaço em branco no meio do parágrafo.

6. Variáveis CSS (custom properties)

6.1 Declarar e usar

CSS
:root {
  --cor-primaria: #0b3d5c;
  --cor-secundaria: #1a7fb5;
  --espaco-medio: 16px;
  --raio-borda: 8px;
}

.botao {
  background-color: var(--cor-primaria);
  padding: var(--espaco-medio);
  border-radius: var(--raio-borda);
}

.botao:hover {
  background-color: var(--cor-secundaria);
}

O nome precisa começar com dois hífens (--) e diferencia maiúsculas de minúsculas (--Cor e --cor são variáveis diferentes). :root é o seletor do elemento <html>: declarar ali torna a variável visível na página inteira.

6.2 Escopo e herança

Variáveis são herdadas, e é isso que as torna poderosas: você pode redefinir uma variável em qualquer escopo e tudo abaixo dela muda.

CSS
:root       { --cor-fundo-cartao: #ffffff; }
.destaque   { --cor-fundo-cartao: #fff8e1; }   /* só dentro de .destaque */

.cartao { background-color: var(--cor-fundo-cartao); }

Um .cartao dentro de .destaque fica amarelado; os outros continuam brancos — e a regra do .cartao não foi tocada. É esse mecanismo que permite temas claro e escuro trocando poucas linhas (assunto da Aula 09 e requisito do projeto final).

6.3 Valor de reserva

CSS
.cartao { box-shadow: var(--sombra-cartao, 0 1px 4px rgba(0, 0, 0, 0.15)); }

O segundo argumento de var() é usado quando a variável não existe. Útil em componentes que podem ser copiados para outro projeto.

6.4 O que vira variável e o que não vira

Vire variável tudo que se repete e pode mudar junto: cores da marca, espaçamentos da escala, raios de borda, sombras, a pilha de fontes. Não vire variável valores que aparecem uma vez só, nem valores que por acaso são iguais mas não têm relação (o padding de um botão e a margin de um parágrafo podem ser 16 px hoje e nada obriga que continuem iguais).

E cuidado com o nome: --azul-escuro é um nome ruim, porque no dia em que a identidade virar verde, a variável passa a mentir. --cor-primaria continua verdadeira.

7. Organizando a folha de estilo

7.1 As sete seções

Uma folha de estilo cresce todo dia. Sem uma ordem combinada, em duas semanas ninguém acha nada — e regras duplicadas começam a brigar. A ordem que você vai usar da Aula 06 até a Aula 09:

CSS
/* 1. Reset e box-sizing        */
/* 2. Variáveis em :root        */
/* 3. Base (body, tipografia, links, imagens) */
/* 4. Layout (header, main, footer, contêineres) */
/* 5. Componentes (botões, cartões, tabelas, formulários) */
/* 6. Utilitários (.texto-centro, .oculto) */
/* 7. Media queries (Aula 08)   */

A ordem não é arbitrária: ela vai do mais genérico ao mais específico, e a etapa 3 da cascata (ordem no arquivo) faz o resto. Um utilitário escrito na seção 6 sobrescreve um componente da seção 5 sem precisar de especificidade extra — que é exatamente o que se espera de um utilitário.

7.2 Nomes de classe que não mentem

O nome da classe deve descrever a função, não a aparência:

Ruim Bom Por quê
.texto-vermelho .mensagem-erro Se mudar para laranja, o nome não mente
.caixa-esquerda .barra-lateral Se mudar de lado, o nome continua válido
.f18b .titulo-secao Legibilidade para quem lê depois (inclusive você)
.azul-grande .botao--primario Descreve o papel, não os pixels

O padrão bloco__elemento--modificador (.cartao, .cartao__titulo, .cartao--destaque) tem nome: BEM. Você não é obrigado a segui-lo à risca nesta trilha, mas vai encontrá-lo na Aula 07 e em quase todo projeto profissional — e ele resolve um problema real: um nome de classe longo e específico nunca colide com o de outro componente.

💻 Mão na massa — O sistema de design do site do evento

Hoje você reescreve css/estilo.css inteiro. A folha da Aula 05 tinha 5 seções e 3 variáveis; a nova terá 7 seções, um sistema de design em 10 variáveis, uma escala tipográfica, botões com todos os estados e a tabela de programação estilizada.

Passo 1 — Importar a fonte nas cinco páginas

Antes do <link rel="stylesheet" href="css/estilo.css"> de cada página, acrescente os três links da fonte:

site-evento/index.html (no <head>, antes do link do CSS)

HTML
  <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@400;600;700&display=swap"
        rel="stylesheet">
  <link rel="stylesheet" href="css/estilo.css">

Repita nas cinco páginas. A ordem importa: a fonte precisa estar disponível quando o seu CSS pedir por ela.

Passo 2 — Simplificar o <main> das cinco páginas

Na Aula 05, todas as regiões receberam um <div class="container">. O <main> não precisa dele: como o conteúdo principal não tem fundo colorido sangrando até as bordas da tela, o próprio <main> pode assumir a largura máxima do projeto. Em cada página, remova o <div class="container"> de dentro do <main> (e o </div> correspondente), promovendo as seções um nível:

site-evento/index.html (trecho do <main>, depois da mudança)

HTML
  <main>
    <section class="hero">
      <h2>Três noites de palestras, minicursos e maratona de programação</h2>
      <p>Participação gratuita, com certificado de 20 horas para quem comparecer a pelo menos 75% das atividades.</p>
      <a href="inscricao.html" class="botao">Inscreva-se</a>
    </section>
  </main>

O <div class="container"> continua no <header> e no <footer>, onde ele é necessário: essas duas regiões têm fundo azul de ponta a ponta da tela, e só o conteúdo delas fica limitado a 1100 px — o mesmo 1100 px que a Aula 05 fixou.

💡 Dica Guarde esse número. Na Aula 07 ele vira a variável --largura-max, e a classe .container volta ao <main> — não mais como uma <div> interna, mas aplicada ao próprio <main> ou à <section> de topo. A largura nunca muda; o que muda é quem carrega a classe.

Passo 3 — Reset e variáveis

Abra css/estilo.css e substitua o arquivo inteiro. Comece pelas duas primeiras seções:

site-evento/css/estilo.css — seções 1 e 2

CSS
/* ==========================================================
   Semana Acadêmica de Sistemas de Informação — folha de estilo
   1. Reset · 2. Variáveis · 3. Base · 4. Layout
   5. Componentes · 6. Utilitários · 7. Media queries
   ========================================================== */

/* 1. Reset e box-sizing */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* 2. Variáveis */
:root {
  --cor-primaria: #0b3d5c;
  --cor-secundaria: #1a7fb5;
  --cor-texto: #333333;
  --cor-fundo: #f7f9fb;
  --fonte-base: "Inter", Arial, sans-serif;
  --espaco-pequeno: 8px;
  --espaco-medio: 16px;
  --espaco-grande: 32px;
  --raio-borda: 8px;
  --sombra-cartao: 0 2px 8px rgba(0, 0, 0, 0.1);
}

Dez variáveis: quatro cores, uma fonte, três espaçamentos, um raio e uma sombra. É o sistema de design do projeto — a partir de agora, nenhum valor de cor ou de espaçamento é escrito solto no meio da folha. Na Aula 07 você vai acrescentar mais três.

💡 Dica Três espaçamentos parecem pouco, e são suficientes. Uma escala curta (8 / 16 / 32) força consistência: o olho percebe imediatamente quando um espaço "não é nenhum dos três". Quando você precisar de um valor intermediário, quase sempre o certo é usar um dos três, não inventar o quarto.

site-evento/css/estilo.css — seção 3

CSS
/* 3. Base */
html {
  font-size: 100%;              /* respeita o tamanho escolhido pelo usuário */
  scroll-behavior: smooth;      /* rolagem suave nas âncoras "Voltar ao topo" */
}

body {
  font-family: var(--fonte-base);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--cor-texto);
  background-color: var(--cor-fundo);
}

h1,
h2,
h3 {
  line-height: 1.2;
  color: var(--cor-primaria);
  margin-bottom: var(--espaco-medio);
}

h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }

p,
ul,
ol,
dl,
table {
  margin-bottom: var(--espaco-medio);
}

p {
  max-width: 65ch;              /* linha de leitura confortável */
}

ul,
ol {
  padding-left: var(--espaco-grande);   /* devolve o recuo que o reset tirou */
}

a {
  color: var(--cor-primaria);
  text-decoration: underline;
  text-underline-offset: 2px;
}

a:hover {
  color: var(--cor-secundaria);
}

/* Sinaliza links que saem do site */
a[href^="http"]::after {
  content: " ↗";
  font-size: .875rem;
}

/* Foco visível em TUDO que é focável */
:focus-visible {
  outline: 3px solid var(--cor-secundaria);
  outline-offset: 2px;
}

img,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

Quatro decisões que merecem explicação:

  • ul, ol { padding-left: var(--espaco-grande) } devolve o recuo que o reset universal apagou. Sem essa linha, os marcadores das listas ficam para fora da caixa e somem no corte.
  • p { max-width: 65ch } limita a linha de leitura mesmo antes de existir qualquer layout. É o ajuste de legibilidade mais barato que existe.
  • :focus-visible sem seletor de elemento aplica-se a tudo que recebe foco: links, botões, campos. Uma regra, o site inteiro acessível pelo teclado.
  • a[href^="http"]::after usa um seletor de atributo para marcar links externos. Como todos os links internos do site são relativos (programacao.html), só os externos começam com http.

Passo 5 — Layout: cabeçalho, conteúdo e rodapé

site-evento/css/estilo.css — seção 4

CSS
/* 4. Layout */
.container {
  max-width: 1100px;
  margin: 0 auto;
  padding-inline: var(--espaco-medio);
}

header {
  background-color: var(--cor-primaria);
  color: #ffffff;
  padding-block: var(--espaco-grande);
}

header h1,
header h2 {
  color: #ffffff;
}

header a {
  color: #ffffff;
}

header nav ul {
  padding-left: 0;
  list-style: none;
}

header nav li {
  display: inline-block;              /* menu na horizontal, sem Flexbox ainda */
  margin-right: var(--espaco-medio);
}

main {
  max-width: var(--largura-max, 1100px);
  margin: 0 auto;
  padding: var(--espaco-grande) var(--espaco-medio);
}

main section {
  margin-bottom: var(--espaco-grande);
}

main section > h2 {
  border-bottom: 3px solid var(--cor-secundaria);
  padding-bottom: var(--espaco-pequeno);
}

footer {
  background-color: var(--cor-primaria);
  color: #ffffff;
  text-align: center;
  padding-block: var(--espaco-grande);
}

footer a {
  color: #ffffff;
}

footer p {
  max-width: none;                    /* o rodapé é centralizado, não é leitura corrida */
  margin-inline: auto;
}

O menu horizontal com display: inline-block é um paliativo consciente: na Aula 07 ele será refeito com Flexbox, que resolve alinhamento e espaçamento de forma muito mais limpa. Por hoje, funciona e não atrapalha.

⚠️ Atenção As regras header a { color: #ffffff } e footer a { color: #ffffff } têm especificidade 0-0-2 e vencem a { color: var(--cor-primaria) } (0-0-1) — que é o que queremos, porque azul-escuro sobre azul-escuro seria ilegível. Se você tivesse escrito .container a, a especificidade seria 0-1-1 e o resultado seria o mesmo; mas aí o main também seria afetado, porque ele também estava dentro de um .container até o Passo 2. É por isso que seletores mais precisos são mais fáceis de manter.

Passo 6 — Componentes: botão, cartão e tabela

site-evento/css/estilo.css — seção 5, botões

CSS
/* 5. Componentes */
.botao {
  display: inline-block;
  padding: 12px 24px;
  background-color: var(--cor-primaria);
  color: #ffffff;
  font-weight: 600;
  text-decoration: none;
  border: 2px solid var(--cor-primaria);
  border-radius: var(--raio-borda);
  cursor: pointer;
}

.botao:hover,
.botao:focus-visible {
  background-color: var(--cor-secundaria);
  border-color: var(--cor-secundaria);
}

.botao:active {
  background-color: #072a40;          /* um tom mais escuro no instante do clique */
}

.botao--contorno {
  background-color: transparent;
  color: var(--cor-primaria);
}

.botao--contorno:hover,
.botao--contorno:focus-visible {
  background-color: var(--cor-primaria);
  color: #ffffff;
}

.botao:disabled,
.botao[aria-disabled="true"] {
  background-color: #9aa8b2;
  border-color: #9aa8b2;
  cursor: not-allowed;
}

Quatro estados em um componente só: repouso, :hover/:focus-visible, :active e desabilitado. Um botão que não muda de aparência ao receber foco é um botão quebrado para quem usa teclado.

site-evento/css/estilo.css — seção 5, cartão

CSS
.cartao {
  background-color: #ffffff;
  border: 1px solid #dfe6ec;
  border-radius: var(--raio-borda);
  padding: var(--espaco-medio);
  margin-bottom: var(--espaco-medio);
  box-shadow: var(--sombra-cartao);
}

.cartao h3 {
  margin-bottom: var(--espaco-pequeno);
}

.cartao p:last-child {
  margin-bottom: 0;                   /* sem espaço sobrando no fim do cartão */
}

site-evento/css/estilo.css — seção 5, tabela da programação

CSS
table {
  width: 100%;
  border-collapse: collapse;          /* junta as bordas duplas em uma só */
}

caption {
  text-align: left;
  font-weight: 600;
  color: var(--cor-primaria);
  padding-bottom: var(--espaco-pequeno);
}

th,
td {
  padding: var(--espaco-pequeno) var(--espaco-medio);
  border-bottom: 1px solid #dfe6ec;
  text-align: left;
  vertical-align: top;
}

thead th {
  background-color: var(--cor-primaria);
  color: #ffffff;
}

tbody th[scope="colgroup"] {
  background-color: #e8eef3;
  color: var(--cor-primaria);
}

tbody tr:nth-child(even) {
  background-color: #ffffff;          /* zebrado: linhas pares em branco */
}

tbody tr:hover {
  background-color: #eef4f8;
}

tfoot td {
  font-size: .875rem;
  font-style: italic;
}

Três seletores desta tabela vieram direto da teoria de hoje: tbody tr:nth-child(even) (pseudoclasse estrutural), tbody th[scope="colgroup"] (seletor de atributo, aproveitando a semântica que você escreveu na Aula 02) e tbody tr:hover (pseudoclasse de estado). Nenhuma classe nova foi necessária — o HTML bem marcado já dizia tudo.

Duas tabelas do site usam essas regras: a da programação, escrita na Aula 02, e a de horário de atendimento em contato.html, escrita na Aula 05. Na Aula 07 a programação será redesenhada em cartões; a folha de estilo da tabela continua valendo para a de contato.html, então nada aqui é desperdício.

⚠️ Atenção border-collapse: collapse é o que transforma a tabela padrão (com bordas duplas e um espaço entre as células) em uma tabela de aparência profissional. Ela precisa ir no elemento table, não nas células — e, com collapse ativo, border-radius na tabela deixa de ter efeito visível nas células das pontas.

Passo 7 — Utilitários e o espaço das media queries

site-evento/css/estilo.css — seções 6 e 7

CSS
/* 6. Utilitários */
.texto-centro {
  text-align: center;
}

.sem-margem {
  margin: 0;
}

/* Some da tela, continua existindo para leitores de tela */
.oculto-visualmente {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* 7. Media queries */
/* Reservado para a Aula 08: mobile first e breakpoints. */

.oculto-visualmente é o utilitário mais importante da lista. Ele resolve um caso frequente: um rótulo que precisa existir para o leitor de tela mas ficaria redundante na tela (um campo de busca com ícone de lupa, por exemplo). Note que não é display: none: isso removeria o elemento da árvore de acessibilidade, e ele deixaria de ser lido — o oposto da intenção.

Passo 8 — Verificar contraste e revisar

  1. Abra https://webaim.org/resources/contrastchecker/ e teste os quatro pares da tabela da seção 3.4.
  2. Crie, na raiz do projeto, um arquivo contraste.md anotando cada par (texto × fundo), a razão obtida e onde ele é usado. Esse arquivo faz parte do Marco 2 (Aula 10).
  3. No DevTools, selecione qualquer texto e abra a aba Computed: confira font-size (16 px no corpo), line-height (25,6 px = 1,6 × 16) e color.

Como testar

  1. As cinco páginas abrem no Live Server com a fonte Inter, cabeçalho e rodapé azul-escuros e conteúdo centralizado com no máximo 1100 px.
  2. Cada <h2> de seção tem a linha azul-clara embaixo; nenhum parágrafo passa de cerca de 65 caracteres por linha.
  3. Os links externos (LinkedIn e GitHub, na página de palestrantes) mostram a seta ; os internos, não.
  4. Pressionando Tab, cada link e cada botão ganha um contorno azul de 3 px visível — inclusive dentro do cabeçalho azul.
  5. Em programacao.html, a tabela ocupa a largura toda, o cabeçalho é branco sobre azul-escuro, as linhas alternam de cor e a linha sob o cursor muda de fundo.
  6. Passando o mouse sobre o botão "Inscreva-se", ele fica azul-claro; segurando o clique, escurece.
  7. No DevTools, mudar --cor-primaria no bloco :root muda cabeçalho, rodapé, títulos, links, botões e cabeçalho da tabela ao mesmo tempo. Esse é o teste definitivo de que o sistema de design está funcionando: faça, é impressionante.

Resultado esperado: o mesmo site da Aula 05, agora com identidade visual coerente, tipografia legível, botões com quatro estados, tabela zebrada e uma folha de estilo em que qualquer alteração de marca custa uma linha. O layout ainda é uma pilha de blocos — isso é a Aula 07.

🧪 Laboratório

Nível A — Fixação

A1. Escreva o seletor CSS para: (a) todos os parágrafos; (b) elementos com a classe aviso; (c) o elemento com id="a06-topo"; (d) parágrafos dentro de article; (e) apenas os li filhos diretos de uma ul; (f) links que abrem em nova aba.

A2. Qual é a diferença entre p.destaque e p .destaque? Escreva um trecho de HTML em que o primeiro pega um elemento e o segundo não pega nenhum.

A3. Calcule a especificidade (A, B, C) de: (a) p; (b) .cartao; (c) #menu a; (d) nav ul li a:hover; (e) .cartao .titulo span; (f) article > p.intro.

A4. Dado o CSS e o HTML abaixo, de que cor fica o parágrafo? Justifique com o cálculo das quatro especificidades.

CSS
p           { color: blue; }
.texto      { color: green; }
#principal p { color: red; }
p.texto     { color: orange; }
HTML
<div id="principal"><p class="texto">Qual cor?</p></div>

A5. Converta #1a7fb5 para rgb(). Depois escreva a mesma cor com 40% de transparência, nas duas notações que você conhece.

A6. Se html { font-size: 100% } e o navegador está no padrão, quantos pixels equivalem a 1.5rem, 0.75rem e 2.5rem?

A7. Explique a diferença entre em e rem e descreva, com um exemplo de três níveis aninhados, por que rem é preferível para tipografia.

A8. Escreva a ordem correta das pseudoclasses de link e explique o que acontece com o :hover se ela for invertida.

A9. Declare uma variável CSS --cor-destaque em :root e use-a em duas regras diferentes, uma delas com valor de reserva.

A10. Explique o que faz cada um destes seletores de atributo: a[href^="mailto:"], a[href$=".zip"], input[type="radio"], li[data-dia].

A11. Por que line-height: 1.6 é melhor que line-height: 24px? Dê um exemplo em que a segunda forma quebra o layout.

A12. Diferencie :nth-child(2) de :nth-of-type(2) usando o HTML <div><h2>T</h2><p>A</p><p>B</p></div>. Qual elemento cada um pega?

Nível B — Aplicação

B1. Estilize uma tabela de notas em exercicios/aula06/notas.html: border-collapse: collapse, cabeçalho com fundo escuro e texto claro, linhas pares com fundo cinza-claro (:nth-child(even)), destaque no :hover da linha, colunas numéricas alinhadas à direita e caption estilizado acima da tabela.

Resultado esperado: a tabela ocupa 100% da largura do contêiner; as bordas não são duplas; as linhas alternam visivelmente; passar o mouse muda o fundo da linha inteira; as notas ficam alinhadas à direita e os nomes à esquerda.

Dica

Para alinhar só a coluna de notas, use um seletor de posição (td:nth-child(3)) ou uma classe nas células numéricas. A segunda opção é mais robusta: se você acrescentar uma coluna, o número muda e o nth-child mira a coluna errada.

B2. Construa uma paleta documentada em exercicios/aula06/paleta.html: dez quadrados coloridos, cada um exibindo o nome da cor, o hexadecimal e o hsl(). Todas as cores declaradas como variáveis em :root. A paleta deve ser coerente: uma primária, uma secundária, uma de destaque, três neutros e as quatro cores semânticas (sucesso, alerta, erro, informação).

Resultado esperado: dez blocos, cada um com o nome do papel da cor (não o nome da cor), os dois valores e a razão de contraste com preto e com branco; as cores da mesma família compartilham o matiz (H) e variam a luminosidade (L).

Dica

Escolha o matiz primeiro (um número de 0 a 360) e construa a família variando só o L. Para as semânticas, os matizes convencionais são: sucesso perto de 140, alerta perto de 40, erro perto de 0 e informação perto de 210.

B3. Estilize completamente o formulário de inscrição da Aula 03. Em css/estilo.css, na seção 5, acrescente um bloco de formulário: campos com padding, borda e border-radius; :focus-visible com borda colorida; :invalid com borda vermelha e um segundo sinal que não seja cor; :valid discreto; fieldset com borda suave e legend destacada; rótulos em bloco acima dos campos.

Resultado esperado: a inscricao.html fica legível e agradável sem uma linha de JavaScript; navegando por Tab, cada campo mostra claramente que está focado; um campo obrigatório vazio, depois de tocado, indica o problema por borda e por outro sinal visual.

Dica

input:invalid marca o campo como inválido desde o carregamento da página, o que assusta o usuário antes de ele digitar qualquer coisa. Combine com :not(:placeholder-shown) para só sinalizar depois que a pessoa começou a preencher. Para o segundo sinal, ::after no rótulo ou um border-left mais grosso funcionam bem.

B4. Experimento de especificidade em exercicios/aula06/especificidade.html: uma página com um único parágrafo e seis regras diferentes que o atingem, cada uma com uma cor distinta. Preveja no papel qual cor vencerá, teste no navegador e escreva um relatório explicando o resultado com a tabela (A, B, C) de cada seletor.

Resultado esperado: as seis regras, a previsão escrita antes do teste, uma captura de tela do painel Styles mostrando as cinco regras riscadas, e a explicação de por que a vencedora venceu (etapa 2 ou etapa 3 da cascata).

Dica

Inclua pelo menos um empate proposital — duas regras com a mesma especificidade — para que a etapa 3 (ordem no arquivo) apareça no relatório. E inclua um seletor com :not() para testar se você entendeu que o conteúdo dele conta.

B5. Refatoração. Reescreva o CSS abaixo eliminando toda a repetição por meio de variáveis, agrupamento de seletores e herança. Comente cada mudança e conte as linhas antes e depois.

CSS
.header  { background-color: #2c3e50; padding: 20px; font-family: Arial; }
.footer  { background-color: #2c3e50; padding: 20px; font-family: Arial; }
.sidebar { background-color: #34495e; padding: 20px; font-family: Arial; }
.header h1  { color: #ecf0f1; font-family: Arial; }
.footer p   { color: #ecf0f1; font-family: Arial; }
.sidebar h2 { color: #ecf0f1; font-family: Arial; }

Resultado esperado: a versão refatorada tem menos da metade das linhas, nenhuma cor repetida em literal, font-family declarada uma única vez (no body, aproveitando a herança) e um comentário por decisão tomada.

Dica

font-family é herdada: declarar no body elimina as seis repetições de uma vez. As três cores viram duas variáveis (--cor-superficie-escura e --cor-texto-claro) mais uma variação. E .header, .footer agrupados resolvem as duas primeiras regras em uma.

Nível C — Desafio

C1. Sistema de design documentado. Crie design-system.css e design-system.html no seu projeto autoral, documentando visualmente o seu sistema: paleta completa em variáveis (com o papel de cada cor), escala tipográfica (seis tamanhos, cada um com nome e valor em rem), escala de espaçamentos (três a cinco níveis), botões em quatro variações (primário, secundário, contorno, desabilitado) com os quatro estados cada, campos de formulário em todos os estados, um cartão e um alerta em quatro cores semânticas. Todos os contrastes precisam passar no WCAG AA — apresente a tabela de verificação.

Dica

Construa a página como um catálogo: uma seção por família de componente, com o exemplo visual à esquerda e o código que o produz à direita (dentro de <pre><code>). Essa página é o seu material de consulta pelo resto da trilha e entra bem no Marco 2 — e é exatamente assim que sistemas de design profissionais são documentados.

🏆 Desafios

⭐ Detetive de especificidade

cssinvestigacaodevtools

Existe um estilo que "não pega" em praticamente todo projeto de CSS — e quase sempre a culpa é da especificidade. Neste desafio você vai treinar o olho: dada uma lista de conflitos reais, prever quem vence antes de abrir o navegador e depois conferir. Quem acerta dez de dez raramente volta a perder tempo caçando esse tipo de bug.

Critérios de pronto

  • Um arquivo detetive.html com dez pares de regras conflitantes, cada par atingindo um elemento diferente da página.
  • Uma tabela em previsoes.md com, para cada par: os dois seletores, a especificidade calculada de cada um, a sua previsão e o resultado observado.
  • Pelo menos dois pares em que a decisão é tomada pela etapa 3 (ordem no arquivo), não pela especificidade.
  • Pelo menos um par envolvendo :not() e um envolvendo um seletor de atributo.
  • Uma captura de tela do painel Styles de um dos casos, com a regra vencida riscada, e uma frase explicando o que o DevTools está mostrando.

Pistas

  1. Comece pelos casos fáceis (p × .classe) e vá subindo até #id × muitas classes — a surpresa está sempre nos empates.
  2. :not(.a) não conta como pseudoclasse, mas o .a de dentro conta. Teste e confirme.
  3. Um style="" inline vence qualquer seletor: inclua um par com ele e explique por que isso torna o atributo style uma má ideia em código de produção.
  4. No painel Styles, as regras aparecem da mais específica para a menos específica, de cima para baixo. Use isso para conferir sua tabela em segundos.
⭐⭐

⭐⭐ A paleta que ninguém consegue ler

cssacessibilidadeinvestigacao

Escolha três sites reais que você usa toda semana — um portal público, uma loja e uma rede social — e audite o contraste deles. Você vai encontrar texto cinza-claro sobre branco, botão desabilitado ilegível e link que só se distingue do texto pela cor (o que também reprova, por outro critério da WCAG). Depois, conserte: reescreva a paleta de um deles mantendo a identidade visual e passando em AA.

Critérios de pronto

  • Uma tabela com pelo menos doze pares de cor auditados (texto × fundo), a razão de contraste medida e o veredito (AA, AAA, reprova).
  • Pelo menos três reprovações encontradas, com captura de tela da tela original.
  • Uma paleta corrigida de um dos sites, em variáveis CSS, com cada cor ajustada apenas na luminosidade (o matiz da marca precisa ser preservado).
  • Uma página antes-depois.html mostrando os dois estados lado a lado, com conteúdo próprio.
  • Uma frase por correção explicando o que mudou e por quanto passou a razão de contraste.

Pistas

  1. O verificador do WebAIM aceita colar os dois hexadecimais; para pegar os hexadecimais do site, use o conta-gotas do seletor de cores do DevTools.
  2. O Lighthouse (aba do DevTools) tem uma auditoria de contraste que lista os elementos reprovados de uma vez — use como ponto de partida, não como conclusão.
  3. Para corrigir mantendo a marca: converta a cor para hsl(), deixe H e S intactos e baixe (ou suba) o L de 5 em 5 pontos até passar.
  4. Texto grande tem exigência menor (3:1). Antes de escurecer um título gigante, confira se ele já não passa.
⭐⭐

⭐⭐ Reconstruindo um componente só com seletores

csshtmlrefatoracaoacessibilidade

Pegue este HTML sujo, cheio de classes que descrevem aparência, e reconstrua o mesmo visual sem tocar no HTML — usando apenas seletores de tipo, de atributo, combinadores e pseudoclasses. Depois reescreva o HTML como ele deveria ser e compare as duas folhas de estilo. O objetivo é sentir na prática o que um HTML semântico economiza de CSS.

componente-sujo.html

HTML
<div class="caixa-branca-borda-cinza">
  <div class="texto-azul-grande-negrito">Minicurso de Git e GitHub</div>
  <div class="texto-cinza-pequeno">Dia 2 · 19h · Laboratório 2</div>
  <div class="texto-normal">Do primeiro commit ao primeiro pull request, em duas horas.</div>
  <div class="linha-botoes">
    <span class="botao-azul" onclick="inscrever()">Inscrever-se</span>
    <span class="botao-branco" onclick="detalhes()">Ver detalhes</span>
  </div>
</div>

Critérios de pronto

  • Uma folha versao-a.css que reproduz o visual do componente sem alterar uma vírgula do HTML acima.
  • Um arquivo componente-limpo.html com a mesma informação em HTML semântico (article, h3, time, p, a ou button) e sem nenhuma classe que descreva aparência.
  • Uma folha versao-b.css para a versão limpa, com o mesmo resultado visual.
  • Uma comparação: número de linhas de cada folha, número de classes de cada HTML e três problemas de acessibilidade que a versão suja tinha e a limpa não tem.
  • Os dois componentes precisam ser utilizáveis por teclado — o que, na versão suja, exige explicar por que <span onclick> não é um botão.

Pistas

  1. Na versão A, div:first-child, div:nth-child(2) e .linha-botoes span:first-child fazem quase todo o trabalho — e você vai perceber como isso é frágil.
  2. Na versão B, article h3, article time e article .botao bastam, e o CSS sobrevive a uma reordenação do conteúdo.
  3. Um <span> não é focável nem anunciado como botão. Liste isso, junto com a ausência de rótulo e a dependência de JavaScript inline.
  4. Meça o tempo: quanto você demorou para escrever cada folha? Esse número também é um resultado.
⭐⭐⭐

⭐⭐⭐ Clone visual de uma identidade real

cssinvestigacaoprojetoacessibilidade

Escolha a página inicial de um site cuja identidade visual você admira e reproduza a identidade — paleta, tipografia, escala de espaçamentos, estilo de botões, tratamento de links — em uma página com conteúdo totalmente diferente e seu. Não é copiar o layout (isso é a Aula 07): é extrair o sistema por trás da aparência e escrevê-lo em variáveis. Ao terminar, você vai olhar para qualquer site e enxergar as decisões, não os pixels.

Critérios de pronto

  • Um arquivo identidade.css com todo o sistema em variáveis: paleta com o papel de cada cor, escala tipográfica completa, escala de espaçamentos, raios e sombras.
  • Uma página clone.html com conteúdo próprio (pode ser sobre o seu projeto autoral) que usa apenas as variáveis — nenhum valor de cor ou espaçamento escrito solto.
  • Um documento de meia página analisando as decisões do site original: por que essas cores, por que essa escala, qual é a razão entre os tamanhos de fonte, quantos espaçamentos diferentes ele usa de verdade.
  • Uma tabela de contraste provando que a sua versão passa em AA em todos os pares de texto — inclusive se o original não passar (e diga se não passa).
  • A troca de tema: mudando apenas os valores dentro do :root, a página inteira assume outra identidade coerente. Entregue as duas capturas de tela.

Pistas

  1. No DevTools, a aba Computed de um elemento mostra os valores finais; anote font-size, line-height, padding e color de uns dez elementos e procure os padrões. Você vai descobrir que quase todo site usa cinco ou seis valores, repetidos.
  2. Muitos sites profissionais já expõem o sistema deles: procure por -- no painel Styles do elemento <html> e veja as variáveis do próprio site.
  3. Para a escala tipográfica, divida cada tamanho pelo anterior: se der sempre perto de 1,25 ou de 1,333, você encontrou a razão.
  4. O teste final do sistema é o item 5 dos critérios: se trocar o :root não muda tudo, é porque ainda há valores soltos. Procure-os com Ctrl+F por # no seu CSS.

🐛 Erros comuns

Sintoma Causa Solução
A regra seguinte é ignorada em silêncio Falta o ; no fim da declaração anterior, ou foi usado // como comentário Toda declaração termina com ;; em CSS o comentário é /* assim */
O estilo "não pega" e não há erro nenhum Espaço acidental no seletor: p .classe em vez de p.classe Leia o seletor em voz alta e confira no console com document.querySelectorAll("…")
Uma regra com #id não consegue ser sobrescrita por classes id foi usado para estilizar; 1-0-0 vence qualquer 0-n-0 Troque o id por uma classe; reserve id para âncoras, for e JavaScript
O :hover do link não funciona em links já visitados Ordem LVHA quebrada: :hover escrito antes de :visited Escreva sempre :link, :visited, :hover, :active nessa ordem
O ::after simplesmente não aparece Falta a propriedade content content é obrigatória em ::before/::after; use content: "" se for só decoração
Layouts em porcentagem estouram a caixa box-sizing: border-box não foi aplicado; padding e border somam à largura Mantenha o reset *, *::before, *::after { box-sizing: border-box; }
A fonte fica gigante em elementos aninhados Uso de em para font-size em vários níveis, acumulando Use rem para tipografia; deixe em para espaçamentos internos de um componente
A imagem de fundo não aparece, e a aba Network mostra 404 Caminho relativo ao HTML em vez de relativo ao arquivo CSS Dentro de css/estilo.css, use url("../img/arquivo.png")
calc(100%-40px) não tem efeito nenhum Faltam os espaços em volta do operador Escreva calc(100% - 40px); os espaços são obrigatórios
Texto cinza-claro sobre branco reprova na auditoria Contraste abaixo de 4,5:1 Verifique no WebAIM e escureça baixando o L do hsl() até passar
Todo o site perdeu o contorno de foco Alguém escreveu outline: none para "ficar mais limpo" Nunca remova sem substituir; use :focus-visible com outline e outline-offset
As bordas da tabela ficam duplas e com espaço entre células border-collapse continua no valor padrão separate table { border-collapse: collapse; }
Um !important novo foi preciso para vencer outro !important Escalada de importância causada por má arquitetura de seletores Remova os dois e resolva por especificidade ou por ordem na folha

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). SILVA, Maurício Samy. Criando sites com HTML, capítulos de seletores e cascata. Na MDN em pt-BR: "Seletores CSS" e "Cascata, especificidade e herança" (links em Para aprofundar). Anote um seletor que apareceu na leitura e não nesta aula.

Parte 2 — Produção (30 min). Hoje fecha o Marco 1 (instruções completas logo abaixo). Além dele, produza o exercício B5 (refatoração com variáveis) aplicado ao seu projeto autoral: o bloco :root completo do seu sistema de design, com no mínimo dez variáveis, e a folha de estilo reorganizada nas sete seções.

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um conflito de estilo real que você enfrentou (no site do evento ou no seu projeto), o cálculo de especificidade dos seletores envolvidos e como resolveu sem !important. Se puder, compare a solução com a de outra pessoa.

Critério de pronto: o css/estilo.css do seu projeto abre com o comentário das sete seções, tem um :root com dez ou mais variáveis, nenhum valor de cor escrito solto fora do :root, nenhum !important e nenhum id usado como seletor de estilo.

Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).

✅ Checkpoint do projeto

Ao fim desta aula, o seu projeto autoral deve ter:

  • [ ] css/estilo.css organizado nas sete seções, com o comentário-cabeçalho listando-as.
  • [ ] Bloco :root com no mínimo dez variáveis: cores, fonte, três espaçamentos, raio e sombra.
  • [ ] Nenhum valor de cor escrito diretamente nas regras — tudo por var(--…), salvo #ffffff e o preto de sombras.
  • [ ] Tipografia base no body (fonte, font-size: 1rem, line-height: 1.6, cor) e escala de títulos em rem.
  • [ ] html { font-size: 100% } — nunca um valor fixo em pixels.
  • [ ] Parágrafos com largura de leitura limitada (max-width: 65ch ou equivalente).
  • [ ] Regra :focus-visible global com outline visível, e nenhum outline: none sem substituto.
  • [ ] Pelo menos um seletor de atributo em uso (por exemplo, a[href^="http"]::after).
  • [ ] Pelo menos uma pseudoclasse estrutural em uso (:nth-child(even) na tabela, por exemplo).
  • [ ] Um .botao com os quatro estados: repouso, :hover, :focus-visible e :active.
  • [ ] Uma tabela com border-collapse: collapse, caption estilizado e linhas alternadas.
  • [ ] Utilitário .oculto-visualmente disponível na seção 6.
  • [ ] Arquivo contraste.md na raiz, com os pares de cor verificados no WebAIM.
  • [ ] Nenhum !important e nenhum id usado como seletor de estilo.

🎓 Marco do projeto — Unidade 1

Escopo. Ao fim da Unidade 1, o seu projeto autoral precisa ser um site em HTML puro, sem CSS, sobre o tema que você definiu na Aula 01. O escopo são os itens não-CSS do Checkpoint da Aula 05: as cinco páginas, o seccionamento semântico, a hierarquia de títulos, os <article>, a página de contato e a validação no W3C. Os itens de CSS daquele Checkpoint — css/estilo.css ligado nas páginas, o bloco :root, a regra base de imagens e o .botao com :hover/:focus-visiblenão entram neste marco; eles fecham o Marco 2, sobre este mesmo site. A página vai parecer crua, e isso é intencional: o que este marco mede é a estrutura. A estilização é o Marco 2, e a interatividade é o Marco 3 — mesmo site, três camadas.

Requisitos.

# Requisito Onde foi estudado
1 Mínimo de 5 páginas interligadas por caminhos relativos, sem link quebrado Aula 02
2 Estrutura HTML5 válida em todas as páginas: <!DOCTYPE>, lang="pt-BR", charset, viewport, description e title próprios Aula 02
3 Seccionamento semântico: header, nav, main (um por página), section, article, aside, footer Aulas 02 e 05
4 Hierarquia de títulos correta, com um único <h1> por página e sem pular níveis Aula 02
5 Elementos de texto aplicados com significado: strong, em, blockquote com cite, time e q (o <abbr> é opcional e conta como item extra) Aula 02
6 Os três tipos de lista em uso: ul, ol e dl Aulas 02 e 04
7 Menu de navegação como <ul> dentro de <nav aria-label>, com aria-current="page" na página atual Aula 04
8 Uma tabela de dados com caption, thead, tbody, tfoot e th scope Aula 02
9 Um formulário completo: mínimo de 10 campos de tipos diferentes, <label> associado a cada um, fieldset/legend, <select>, <textarea> e validação nativa Aulas 03 e 04
10 Imagens com alt adequado a cada situação, ao menos uma em figure/figcaption, com width/height e loading="lazy" abaixo da dobra Aula 04
11 Um vídeo ou áudio incorporado com controls e fallback; se vídeo, com <track kind="captions"> Aula 04
12 Ao menos um link externo com target="_blank" e rel="noopener noreferrer" Aula 02
13 Zero erros no validador do W3C, em todas as páginas Aula 02

Checklist de qualidade. O que separa um projeto pronto de um feito às pressas na última hora:

  • Validação W3C sem erros em nenhuma das cinco páginas.
  • Seccionamento semântico coerente e hierarquia de títulos sem saltos.
  • Formulário completo e acessível: todo campo com rótulo associado, agrupamento em fieldset, validação nativa funcionando.
  • Tabela, listas e elementos de texto usados pelo que significam, não só pela aparência.
  • Mídias com alt que descreve o que a imagem comunica (não o nome do arquivo), legendas de vídeo funcionando, width/height e loading="lazy" presentes.
  • Navegação funcional e idêntica entre todas as páginas, sem link quebrado.
  • Arquivos e pastas em minúsculas, sem espaço nem acento; nomes que dizem o que cada arquivo é.
  • Nenhum <link rel="stylesheet">, <style> ou atributo style — este marco é só estrutura.

Como saber que está pronto.

  • Rode cada página no validador do W3C (upload de arquivo ou por URL, se já estiver publicada) e confira "Document checking completed. No errors or warnings to show."
  • No Console do DevTools, document.querySelectorAll("main").length deve retornar 1 em cada página.
  • Navegue pelo site inteiro só com Tab e Enter: todo link e todo campo do formulário precisa ser alcançável e visível em foco.
  • Peça para alguém (colega, familiar, ou você mesmo depois de um dia) abrir as cinco páginas sem explicação prévia e apontar qual link parece quebrado ou qual página parece fora do lugar.
  • Use IA para entender um erro do validador ou revisar uma dúvida de sintaxe — não para gerar o site inteiro no seu lugar. Se você não conseguir explicar uma linha do seu próprio HTML, ela ainda não é sua.

📚 Para aprofundar

Na próxima aula o site deixa de ser uma pilha de blocos: você vai aprender posicionamento, Flexbox e CSS Grid, decidir qual dos dois cada problema pede e construir o esqueleto de layout definitivo do site do evento, com um menu de navegação fixo, acessível e com link de salto.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Seletores1.1 Os quatro seletores básicos1.2 Agrupamento1.3 Combinadores: seletores que descrevem relações1.4 Um espaço muda tudo1.5 Seletores de atributo1.6 Pseudoclasses: o estado do elemento1.7 Pseudoclasses estruturais1.8 Pseudoelementos: partes que não existem no HTML2. A cascata: como o navegador decide2.1 Etapa 1 — origem e importância2.2 Etapa 2 — especificidade2.3 Etapa 3 — ordem no arquivo2.4 Herança3. Cores3.1 As notações3.2 HSL: a notação que serve para pensar3.3 Fundos3.4 Contraste é requisito, não gosto4. Unidades4.1 Absolutas4.2 Relativas — prefira sempre4.3 Fazendo contas: calc(), min(), max() e clamp()5. Tipografia5.1 A pilha de fontes5.2 As propriedades5.3 Uma escala tipográfica5.4 Fontes externas (Google Fonts)5.5 Legibilidade: quatro números que resolvem6. Variáveis CSS (custom properties)6.1 Declarar e usar6.2 Escopo e herança6.3 Valor de reserva6.4 O que vira variável e o que não vira7. Organizando a folha de estilo7.1 As sete seções7.2 Nomes de classe que não mentem💻 Mão na massa — O sistema de design do site do eventoPasso 1 — Importar a fonte nas cinco páginasPasso 2 — Simplificar o <main> das cinco páginasPasso 3 — Reset e variáveisPasso 4 — Estilos base: tipografia, links e imagensPasso 5 — Layout: cabeçalho, conteúdo e rodapéPasso 6 — Componentes: botão, cartão e tabelaPasso 7 — Utilitários e o espaço das media queriesPasso 8 — Verificar contraste e revisarComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ Detetive de especificidade⭐⭐ A paleta que ninguém consegue ler⭐⭐ Reconstruindo um componente só com seletores⭐⭐⭐ Clone visual de uma identidade real🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto🎓 Marco do projeto — Unidade 1📚 Para aprofundar
Nível 1Unidade 2 · CSS: estilo, layout e responsividade3 aulas de 50 min + 1 h EAD

Aula 07 — Formatando o layout de um website e o menu

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Explicar o fluxo normal do navegador e prever onde um elemento vai parar com cada valor de position (static, relative, absolute, fixed, sticky).
  • Controlar sobreposição com z-index e transbordamento com overflow, e explicar por que float não serve mais para layout.
  • Construir layouts em uma dimensão com Flexbox: eixos, alinhamento, distribuição de espaço, gap, flex-grow, flex-shrink e flex-basis.
  • Construir layouts em duas dimensões com CSS Grid: unidade fr, repeat(), minmax(), auto-fit e áreas nomeadas.
  • Decidir, diante de um componente ou de uma página, se o problema pede Flexbox, Grid ou os dois combinados.
  • Implementar um menu de navegação horizontal, fixo no topo e acessível (lista semântica, aria-label, aria-current, link de salto).
  • Inspecionar e depurar um layout com o overlay de Flexbox e Grid do DevTools.

📋 Pré-requisitos

  • [ ] Pasta site-evento/ com as cinco páginas em HTML semântico da Unidade 1 (index.html, programacao.html, inscricao.html, palestrantes.html, contato.html), validadas no W3C.
  • [ ] site-evento/css/estilo.css criado na Aula 06, com o reset box-sizing: border-box, o bloco :root de variáveis e a folha organizada na ordem recomendada (reset → variáveis → base → layout → componentes → utilitários → media queries).
  • [ ] VS Code com a extensão Live Server e um navegador com DevTools (Chrome ou Firefox).
  • [ ] Revisar da Aula 06: especificidade de seletores, display: block versus display: inline, o modelo de caixa (margin, border, padding) e var(--nome).

Na aula passada você estilizou o site do evento: sistema de design em variáveis, tipografia, cores, botões e tabela. Mas as páginas ainda são uma pilha de blocos de cima para baixo — o cabeçalho não fica ao lado do logo, os cartões de palestra não formam uma grade e o rodapé sobe quando a página tem pouco conteúdo. Hoje você aprende as duas ferramentas que resolvem isso — Flexbox e Grid — e constrói o esqueleto de layout definitivo do site, com um menu de navegação que qualquer pessoa consegue usar.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Fluxo normal; position e seus cinco valores; z-index; float como legado; overflow; experimentos no DevTools
2 50 min Flexbox completo: eixos, propriedades do contêiner e dos itens, os quatro padrões de layout que resolvem 80% dos casos
3 50 min CSS Grid: fr, repeat, minmax, áreas nomeadas; Flexbox ou Grid?; menu acessível; Mão na massa no site do evento

1. Fluxo normal e posicionamento

1.1 O que o navegador faz quando você não diz nada

Antes de qualquer CSS de layout, o navegador já tem um plano: o fluxo normal. Elementos de bloco (div, p, section, h1) são empilhados de cima para baixo, cada um ocupando toda a largura disponível. Elementos em linha (a, strong, span, img) são dispostos lado a lado, da esquerda para a direita, quebrando linha quando falta espaço — exatamente como as palavras de um parágrafo.

Pense no fluxo normal como a água escorrendo por uma calha: cada caixa cai na primeira posição livre. Tudo o que você vai aprender hoje — position, Flexbox, Grid — é uma forma de desviar a água: tirar um elemento do fluxo ou trocar as regras de como os filhos de um contêiner se organizam.

🧠 Você sabia? Toda vez que o navegador precisa recalcular a posição e o tamanho das caixas, ele faz um reflow (ou layout). Mudar a largura de um elemento no topo da página pode obrigar o navegador a recalcular a posição de todos os elementos abaixo dele — em uma página grande, isso custa milissegundos que se transformam em travadas visíveis. É por isso que, na Aula 09, você vai aprender a animar só propriedades que não disparam reflow. Por enquanto, guarde a ideia: layout é a etapa cara do desenho de uma página.

1.2 position: cinco maneiras de dizer "fique aqui"

A propriedade position define em relação a quê um elemento é posicionado e se ele ainda ocupa espaço no fluxo. As propriedades top, right, bottom e left só têm efeito quando position é diferente de static.

Exemplo — os cinco valores lado a lado

CSS
.a { position: static; }                          /* padrão; ignora top/right/bottom/left */
.b { position: relative; top: 10px; left: 20px; } /* desloca-se da própria posição original */
.c { position: absolute; top: 0; right: 0; }      /* canto do ancestral posicionado mais próximo */
.d { position: fixed; bottom: 20px; right: 20px; } /* preso à janela; não rola com a página */
.e { position: sticky; top: 0; }                  /* normal até encostar no topo; então gruda */
Valor Referência de posicionamento Ocupa espaço no fluxo?
static Nenhuma — segue o fluxo normal Sim
relative A própria posição original Sim (o espaço original é mantido, mesmo deslocado)
absolute O ancestral posicionado mais próximo (ou o <html>) Não — sai do fluxo
fixed A janela do navegador (viewport) Não — sai do fluxo
sticky Fluxo normal até atingir o limite (top, bottom); depois, o contêiner de rolagem Sim

A regra de ouro do absolute. Um elemento absolute procura, subindo na árvore, o ancestral mais próximo cujo position seja diferente de static. Se não encontrar nenhum, ele se posiciona em relação à página inteira — e vai parar no canto do documento, longe de onde você queria. Por isso a receita clássica é: position: relative no pai (que não se move, porque sem top/left o relative não desloca nada) e position: absolute no filho.

No site do evento, é assim que o selo "Esgotado" fica preso no canto do cartão de um minicurso:

site-evento/css/estilo.css — trecho

CSS
.cartao {
  position: relative;   /* vira a referência do selo, sem se mover */
}

.cartao__selo {
  position: absolute;   /* sai do fluxo e ancora no cartão */
  top: 8px;
  right: 8px;
}

sticky merece atenção especial. Ele é o híbrido: o elemento rola normalmente com a página até que a distância definida (top: 0) seja atingida; a partir daí, ele "gruda" e a página continua rolando por baixo. É o comportamento perfeito para um cabeçalho: não rouba espaço no topo quando a página carrega, mas fica sempre à mão. Duas condições para funcionar: declarar top (ou bottom) e nenhum ancestral com overflow diferente de visible — você vai reencontrar essa segunda condição na tabela de erros comuns.

💡 Dica fixed e sticky parecem iguais no cabeçalho, mas diferem no resto: fixed sai do fluxo (o conteúdo abaixo sobe e fica escondido atrás do cabeçalho, obrigando você a compensar com padding-top no body); sticky continua ocupando o espaço original. Prefira sticky para cabeçalhos.

1.3 z-index e a pilha de elementos

Quando dois elementos posicionados se sobrepõem, quem fica na frente? Por padrão, o que vem depois no HTML. z-index muda isso: valores maiores ficam à frente. Só funciona em elementos com position diferente de static (e em itens de Flexbox e Grid).

CSS
.modal   { position: fixed; z-index: 1000; }
.overlay { position: fixed; z-index: 900; }

⚠️ Atenção Evite z-index: 9999. Quando cada componente "grita mais alto" que o anterior, ninguém sabe mais quem está na frente de quem. Defina uma escala do projeto e documente-a na folha de estilo. No site do evento: 100 para o cabeçalho fixo, 900 para sobreposições escuras, 1000 para modais e para o link de salto.

🔎 Por baixo do capô z-index não é uma escala global. Todo elemento posicionado com z-index diferente de auto cria um contexto de empilhamento: os filhos dele só competem entre si, e o grupo inteiro é comparado com os vizinhos usando o z-index do pai. É por isso que, às vezes, um z-index: 9999 "não funciona": o elemento está dentro de um contexto com z-index: 1, e o contexto inteiro está atrás de outro com z-index: 2. opacity menor que 1, transform e filter também criam contextos de empilhamento — lembre-se disso na Aula 09.

1.4 float — o legado que você precisa reconhecer

float foi criado com um único propósito: fazer texto envolver uma imagem, como em uma revista. Como durante mais de dez anos não existia nada melhor, a comunidade usou float para construir colunas, menus e páginas inteiras — com gambiarras como clearfix e contêineres que colapsavam. Hoje não use float para layout: Flexbox e Grid resolvem tudo isso de forma previsível. Ainda é válido para o caso original:

CSS
figure { float: left; margin: 0 16px 16px 0; }  /* texto envolve a figura */
.limpa { clear: both; }                          /* próximo elemento não envolve nada */

Você vai encontrar float em código antigo, em tutoriais desatualizados e em respostas de fórum. Saiba ler; não escreva.

1.5 overflow: o que acontece quando o conteúdo não cabe

CSS
overflow: visible;  /* padrão: o conteúdo transborda e aparece fora da caixa */
overflow: hidden;   /* corta o excesso */
overflow: auto;     /* barra de rolagem só quando precisar */
overflow: scroll;   /* barra sempre visível */
overflow-x: auto;   /* controle por eixo */
overflow-y: hidden;

Um uso legítimo no site do evento: uma tabela de programação larga, dentro de um contêiner com overflow-x: auto, rola horizontalmente no celular em vez de estourar a página inteira.

🔬 Investigue Abra index.html com o Live Server e o DevTools (F12). No painel Elements, selecione o <header> e, na aba Styles, adicione position: sticky; top: 0; clicando no bloco element.style. Role a página: o cabeçalho gruda. Agora troque para position: fixed: veja o conteúdo "pular" para cima, porque o cabeçalho saiu do fluxo e deixou de ocupar espaço. Por fim, adicione overflow-x: hidden ao <body> com o sticky de volta — e observe que o cabeçalho para de grudar. Você acabou de encontrar o bug de sticky mais comum da web.

2. Flexbox — layout em uma dimensão

2.1 O que muda com display: flex

Flexbox distribui os filhos de um contêiner ao longo de um eixo — uma linha ou uma coluna — e resolve, com poucas propriedades, o que antes exigia truques: alinhar verticalmente, distribuir espaço sobrando, fazer itens crescerem ou encolherem, mudar a ordem visual.

CSS
.contêiner {
  display: flex;
}

Só essa linha já transforma todos os filhos diretos em flex items e os coloca lado a lado. Os netos não são afetados — Flexbox age em um nível só. Isso importa: se você quiser um Flexbox dentro de um item, o item precisa ser, ele mesmo, um contêiner display: flex.

2.2 Os dois eixos

Tudo em Flexbox gira em torno de dois eixos. O eixo principal (main axis) segue flex-direction; o eixo transversal (cross axis) é perpendicular a ele.

Texto
flex-direction: row (padrão)

   eixo principal (main axis) ─────────────────────────>
   ┌─────┐ ┌─────┐ ┌─────┐                          │
   │  1  │ │  2  │ │  3  │                          │ eixo transversal
   └─────┘ └─────┘ └─────┘                          │ (cross axis)
                                                    ▼

flex-direction: column

   eixo transversal ────────────>
   ┌─────┐   │
   │  1  │   │
   └─────┘   │ eixo principal
   ┌─────┐   │
   │  2  │   │
   └─────┘   ▼

Com flex-direction: column, os eixos trocam de papel: o principal passa a ser vertical. Todas as propriedades a seguir são definidas em relação a esses eixos, não em relação a "horizontal" e "vertical". Essa é a fonte da confusão mais comum com Flexbox — e a razão de justify-content alinhar horizontalmente em uma linha, mas verticalmente em uma coluna.

2.3 Propriedades do contêiner

CSS
.contêiner {
  display: flex;

  flex-direction: row;          /* row | row-reverse | column | column-reverse */

  flex-wrap: wrap;              /* nowrap (padrão) | wrap | wrap-reverse */

  justify-content: center;      /* distribui no EIXO PRINCIPAL */
  /* flex-start | flex-end | center | space-between | space-around | space-evenly */

  align-items: center;          /* alinha no EIXO TRANSVERSAL */
  /* stretch (padrão) | flex-start | flex-end | center | baseline */

  align-content: center;        /* alinha as LINHAS entre si (só com wrap e várias linhas) */

  gap: 16px;                    /* espaço entre itens — use isto, não margin */
  row-gap: 16px;
  column-gap: 24px;
}
Valor de justify-content Resultado
flex-start Todos encostados no início do eixo (padrão)
flex-end Todos encostados no fim
center Agrupados no centro
space-between Primeiro na ponta, último na ponta, o resto distribuído por igual
space-around Espaço igual em volta de cada item (as bordas ficam com metade)
space-evenly Espaços exatamente iguais, inclusive nas bordas

Repare em align-items: stretch, o padrão: os itens esticam para ocupar toda a altura do contêiner. É por isso que, em uma linha de cartões flex, todos ficam com a mesma altura sem nenhum esforço — algo que era quase impossível com float.

A centralização perfeita. Centralizar um elemento nos dois eixos foi, por vinte anos, um dos problemas mais frustrantes do CSS. Hoje são três linhas:

CSS
.centro {
  display: flex;
  justify-content: center;   /* eixo principal */
  align-items: center;       /* eixo transversal */
}

🧠 Você sabia? O primeiro rascunho do Flexbox é de 2009 — mas ele só se tornou utilizável em todos os navegadores por volta de 2015. Foram três sintaxes diferentes no caminho (display: box, depois display: flexbox, por fim display: flex), cada uma com prefixos de fornecedor (-webkit-, -ms-), e um Internet Explorer que implementou a versão intermediária e nunca atualizou. Se você encontrar display: -webkit-box em código antigo, é um fóssil dessa história. A lição: uma especificação só vira ferramenta quando todos os navegadores concordam — e isso leva tempo.

2.4 Propriedades dos itens

As propriedades do contêiner organizam o conjunto; as dos itens dizem como cada um reage ao espaço disponível.

CSS
.item {
  flex-grow: 1;       /* fator de crescimento quando sobra espaço (padrão 0: não cresce) */
  flex-shrink: 1;     /* fator de encolhimento quando falta espaço (padrão 1: encolhe) */
  flex-basis: 200px;  /* tamanho inicial antes de distribuir (padrão auto: o tamanho do conteúdo) */

  flex: 1;            /* atalho = flex: 1 1 0 — todos os itens com o mesmo tamanho */
  flex: 0 0 250px;    /* largura fixa: não cresce, não encolhe */
  flex: 1 1 280px;    /* começa com 280px, cresce e encolhe conforme o espaço */

  align-self: flex-end;   /* sobrescreve align-items só neste item */
  order: 2;               /* muda a ordem visual (padrão 0; menores vêm primeiro) */
}

Como ler flex: 1 1 280px: "comece com 280 px; se sobrar espaço, cresça na proporção 1; se faltar, encolha na proporção 1". Quando todos os irmãos têm flex: 1, o espaço é dividido igualmente. Quando um tem flex: 2 e os outros flex: 1, ele recebe o dobro do espaço sobrando — não o dobro do tamanho total.

⚠️ Atenção order muda apenas a ordem visual, não a ordem do DOM. Quem navega com Tab ou com um leitor de tela continua seguindo a ordem do HTML — e, se a tela mostra "Contato" antes de "Início" mas o foco vai na ordem contrária, a experiência fica confusa. Use order com parcimônia e nunca para inverter a sequência lógica de navegação.

2.5 Os quatro padrões que resolvem quase tudo

site-evento/css/estilo.css — trechos de exemplo

CSS
/* 1. Barra de navegação: logo à esquerda, menu à direita, tudo alinhado no centro */
.cabecalho__interno {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 32px;
}

/* 2. Menu horizontal com espaçamento uniforme */
.menu {
  display: flex;
  gap: 24px;
  list-style: none;
}

/* 3. Linha de cartões que quebra sozinha quando falta espaço */
.cartoes-flex {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
}
.cartoes-flex .cartao {
  flex: 1 1 280px;   /* mínimo 280px; cresce para preencher; quebra linha quando não couber */
}

/* 4. Rodapé sempre no fim da tela, mesmo com pouco conteúdo */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
main {
  flex: 1;           /* o main absorve todo o espaço sobrando; o rodapé é empurrado para baixo */
}

O padrão 4 resolve um problema clássico do site do evento: a página contato.html tem pouco conteúdo, e o rodapé ficava flutuando no meio da tela. Com body em coluna e main { flex: 1 }, o rodapé vai para o fim da janela — e, quando a página cresce, ele desce junto.

🔬 Investigue No DevTools, painel Elements, todo elemento com display: flex ganha um selo cinza escrito flex ao lado da tag. Clique nele: o navegador desenha o eixo principal, o contorno de cada item e os espaços de gap. Agora abra a aba Layout (Chrome) ou o painel Flexbox (Firefox): você pode ligar o overlay de vários contêineres ao mesmo tempo e ver, ao vivo, o que justify-content: space-between está fazendo com o espaço. Troque o valor na aba Styles e observe o desenho mudar.

3. CSS Grid — layout em duas dimensões

3.1 Linhas e colunas ao mesmo tempo

Flexbox pensa em uma fila. Grid pensa em uma tabela invisível: você declara colunas e linhas, e os filhos ocupam as células. É a ferramenta certa para a estrutura geral de uma página — cabeçalho, lateral, conteúdo, rodapé — e para qualquer coisa que precise alinhar nos dois eixos, como uma galeria.

CSS
.grade {
  display: grid;
  grid-template-columns: 200px 1fr 200px;   /* três colunas: fixa, elástica, fixa */
  grid-template-rows: auto 1fr auto;        /* três linhas: conteúdo, elástica, conteúdo */
  gap: 20px;
}

A unidade fr (fraction) representa uma fração do espaço que sobra depois de descontar tamanhos fixos e gaps. Em um contêiner de 1000 px com 200px 1fr 200px e gap: 20px, sobram 1000 − 200 − 200 − 40 = 560 px para a coluna do meio. Com 1fr 2fr, o espaço livre é dividido em três partes: uma para a primeira coluna, duas para a segunda.

3.2 Definindo colunas

CSS
grid-template-columns: 200px 200px 200px;                     /* três fixas */
grid-template-columns: 1fr 1fr 1fr;                           /* três iguais e elásticas */
grid-template-columns: repeat(3, 1fr);                        /* idem, abreviado */
grid-template-columns: 250px 1fr;                             /* lateral fixa + conteúdo elástico */
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));  /* responsivo sem media query */

💡 Dica A linha repeat(auto-fit, minmax(250px, 1fr)) é uma das mais poderosas do CSS moderno. Leia-a assim: "crie quantas colunas couberem (auto-fit), cada uma com no mínimo 250 px e no máximo uma fração igual do espaço (minmax(250px, 1fr))". Em uma janela de 1100 px cabem quatro colunas; em 800 px, três; em um celular, uma. A grade se reorganiza sozinha — sem nenhuma media query. É assim que os cartões de programação e de palestrantes do site do evento vão se comportar.

auto-fit ou auto-fill? Os dois criam quantas colunas couberem. A diferença aparece quando há poucos itens: auto-fill mantém as colunas vazias (três cartões em uma grade de cinco colunas ficam encolhidos à esquerda); auto-fit colapsa as faixas vazias e deixa os itens existentes esticarem para ocupar a largura toda. Para cartões, quase sempre você quer auto-fit.

3.3 Posicionando itens na grade

Grid numera as linhas-guia (as divisórias), não as células. Uma grade de três colunas tem quatro linhas-guia verticais: 1, 2, 3 e 4.

CSS
.item {
  grid-column: 1 / 3;          /* da linha-guia 1 até a 3 — ocupa duas colunas */
  grid-column: span 2;         /* ocupa duas colunas a partir de onde estiver */
  grid-row: 2 / 4;             /* da linha-guia 2 até a 4 — ocupa duas linhas */
  grid-area: 2 / 1 / 4 / 3;    /* linha-início / coluna-início / linha-fim / coluna-fim */
}

Itens sem posição explícita são colocados automaticamente na próxima célula livre, na ordem do HTML. Isso é o que faz auto-fit funcionar sem que você precise posicionar cartão por cartão.

3.4 Áreas nomeadas — o layout desenhado no código

Números de linha-guia funcionam, mas ninguém lê grid-area: 2 / 1 / 4 / 3 e visualiza a página. Áreas nomeadas resolvem isso: você desenha o layout com palavras.

CSS
.layout {
  display: grid;
  min-height: 100vh;
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "cabecalho cabecalho"
    "lateral   conteudo"
    "rodape    rodape";
  gap: 16px;
}

.cabecalho { grid-area: cabecalho; }
.lateral   { grid-area: lateral; }
.conteudo  { grid-area: conteudo; }
.rodape    { grid-area: rodape; }

Cada string é uma linha da grade; cada palavra, uma célula. Repetir o nome em células vizinhas faz a área se estender (o cabeçalho ocupa as duas colunas). Um ponto (.) marca uma célula vazia. Para reorganizar tudo no celular, basta redesenhar as áreas dentro de uma media query — uma coluna só, cada área em uma linha — sem tocar no HTML. Você fará exatamente isso na Aula 08.

3.5 Alinhamento no Grid

As propriedades de alinhamento têm os mesmos nomes do Flexbox, mas com uma camada a mais: alinhar os itens dentro das células e alinhar a grade inteira dentro do contêiner.

CSS
.grade {
  justify-items: center;    /* cada item, horizontalmente, dentro da sua célula */
  align-items: center;      /* cada item, verticalmente, dentro da sua célula */
  place-items: center;      /* atalho: align-items + justify-items */

  justify-content: center;  /* a grade inteira, horizontalmente, no contêiner */
  align-content: center;    /* a grade inteira, verticalmente, no contêiner */
}

📌 Vale gravar Em Grid, justify-* sempre se refere ao eixo horizontal (colunas) e align-* ao vertical (linhas). Em Flexbox, justify-content segue o eixo principal, que muda com flex-direction. Essa diferença cai no dia a dia — vale gravar.

🔬 Investigue Selecione no DevTools qualquer elemento com display: grid; ele ganha um selo grid. Clique nele e vá à aba Layout: ative "Show line numbers" e "Show area names". O navegador desenha as linhas-guia numeradas e escreve o nome de cada área por cima da página. Redimensione a janela com o overlay ligado e veja auto-fit criando e removendo colunas. Esse overlay vai economizar horas de tentativa e erro — use-o sempre que um item cair na célula errada.

4. Flexbox ou Grid?

A pergunta certa não é "qual é melhor", e sim "quantas dimensões o problema tem".

Use Flexbox quando Use Grid quando
O layout é uma linha ou uma coluna Você precisa controlar linhas e colunas
O tamanho dos itens (conteúdo) define a distribuição O contêiner define a estrutura, e o conteúdo se encaixa
Barra de navegação, grupo de botões, cartões em linha, campo de busca com botão Estrutura da página, galerias, dashboards, formulários em colunas
Alinhar coisas dentro de um componente Posicionar os componentes na página

Eles não competem — se combinam. A arquitetura padrão de um site hoje é: Grid para a estrutura da página (onde ficam cabeçalho, lateral, conteúdo, rodapé) e Flexbox para o interior de cada componente (o que há dentro do cabeçalho, de um cartão, de um formulário). É assim que o site do evento vai ficar ao fim desta aula.

Dois testes rápidos para decidir:

  1. Você consegue descrever o layout com uma frase que tenha "ao lado" ou "abaixo", mas não os dois? Flexbox. ("Logo ao lado do menu"; "campos empilhados abaixo do outro".)
  2. Você precisa que a segunda linha se alinhe com a primeira? Grid. (Uma galeria em que as colunas precisam bater; um formulário em que os rótulos alinham nas duas linhas.)

5. Construindo o menu de navegação

O menu é o componente que mais concentra erros de acessibilidade em projetos reais. Ele também é a primeira coisa que todo visitante toca. Vale construí-lo com cuidado.

5.1 Marcação

site-evento/index.html — trecho do cabeçalho

HTML
<header class="cabecalho">
  <div class="container cabecalho__interno">
    <a href="index.html" class="logo">
      <img src="img/logo-sasi.svg" alt="Página inicial — Semana Acadêmica de Sistemas de Informação">
    </a>

    <nav aria-label="Principal">
      <ul class="menu">
        <li><a href="index.html" aria-current="page">Início</a></li>
        <li><a href="programacao.html">Programação</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
        <li><a href="inscricao.html" class="menu__cta">Inscreva-se</a></li>
      </ul>
    </nav>
  </div>
</header>

⚠️ Atenção Este cabeçalho substitui o <header id="a07-topo"> das Aulas 02 a 06 (logo solto, <h1> do site, parágrafo de local e <nav> sem classe). Apague o bloco antigo inteiro nas cinco páginas e cole este no lugar. Junto com ele vão embora duas coisas: o parágrafo "Três noites de outubro · Auditório Central", que passa a viver no herói da página inicial e no rodapé, e o <h1> do cabeçalho — a partir de hoje o <h1> é o título da página, dentro do <main> (o que era <h2>Programação</h2> vira <h1>Programação</h1>, e assim por diante nas cinco páginas). O nome do evento continua acessível: ele está no alt do logo e no <title>.

Três pontos que valem nota:

  1. O menu é uma lista (ul/li). Leitores de tela anunciam "lista com 5 itens" ao entrar nela — o usuário sabe o tamanho da navegação antes de percorrê-la. Uma sequência de <a> soltos não dá essa informação.
  2. <nav aria-label="Principal"> distingue esta navegação de outras que a página pode ter (a do rodapé, uma trilha de "você está aqui", os filtros da programação). Sem o rótulo, o leitor de tela anuncia só "navegação", e o usuário não sabe qual.
  3. aria-current="page" marca a página atual semanticamente, não só com cor. Quem não enxerga a cor (por daltonismo ou por usar leitor de tela) recebe a mesma informação. Em cada página do site, o atributo vai no link correspondente — e o CSS usa o atributo como seletor, então o destaque visual vem de graça.

O alt do logo diz para onde o link leva ("Página inicial") e o que a imagem é, porque um logo dentro de um link é, antes de tudo, um link. Na Aula 04 esse mesmo logo tinha alt="", porque o <h1> ao lado já dizia o nome do evento; agora que o logo virou link e o <h1> saiu do cabeçalho, o alt volta a carregar informação. O alt correto depende do contexto, não do arquivo.

Uma mudança de ordem, declarada. O menu passa de cinco itens em ordem de "índice" (Início, Programação, Inscrição, Palestrantes, Contato) para quatro itens de navegação mais um botão de ação no fim: "Inscreva-se". A decisão é de experiência de uso — a ação que o site quer que a pessoa faça deve estar no fim da linha de leitura e visualmente distinta do resto, não perdida no meio do menu. O rótulo muda de "Inscrição" (substantivo, nome de página) para "Inscreva-se" (verbo, chamada para ação) pelo mesmo motivo. O arquivo continua sendo inscricao.html.

5.2 Estilo

site-evento/css/estilo.css — seção 4 (layout)

CSS
.cabecalho {
  background: var(--cor-superficie);
  border-bottom: 1px solid var(--cor-borda);
  position: sticky;
  top: 0;
  z-index: 100;
}

.cabecalho__interno {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espaco-grande);
  padding-block: var(--espaco-medio);
}

.logo img {
  display: block;   /* remove o espaço fantasma abaixo da imagem em linha */
  height: 40px;
  width: auto;
}

.menu {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  list-style: none;
}

.menu a {
  position: relative;      /* referência para o sublinhado (::after) */
  display: block;
  padding: .5rem 0;
  color: var(--cor-texto);
  text-decoration: none;
  font-weight: 500;
}

/* Sublinhado que cresce — a Aula 09 explica transform e transition em detalhe */
.menu a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  background: var(--cor-primaria);
  transform: scaleX(0);
  transition: transform 200ms ease;
}

.menu a:hover::after,
.menu a:focus-visible::after,
.menu a[aria-current="page"]::after {
  transform: scaleX(1);
}

.menu a[aria-current="page"] {
  color: var(--cor-primaria);
  font-weight: 600;
}

/* O botão-pílula de inscrição */
.menu a.menu__cta {
  padding: .5rem 1.25rem;
  background: var(--cor-primaria);
  color: #fff;
  border-radius: 999px;
}

.menu a.menu__cta::after {
  display: none;           /* o botão não recebe o sublinhado */
}

.menu a.menu__cta:hover,
.menu a.menu__cta:focus-visible {
  background: var(--cor-secundaria);
}

💡 Dica Repare no seletor .menu a.menu__cta em vez de só .menu__cta. Como vimos na Aula 06, .menu a tem especificidade (0,1,1) e vence .menu__cta (0,1,0) — se você escrevesse só a classe, o color: #fff seria ignorado e o botão ficaria com texto escuro sobre fundo escuro. .menu a.menu__cta tem (0,2,1) e ganha. Quando um estilo "não pega", a especificidade é a primeira suspeita.

Duas decisões pequenas com efeito grande:

  • :focus-visible ao lado de :hover em todos os estados. Quem navega por teclado precisa ver onde está — e o contorno padrão do navegador, que você talvez tenha removido com outline: none em algum momento, era exatamente isso. Nunca remova o outline sem oferecer uma alternativa visível.
  • display: block nos links aumenta a área clicável para o padding inteiro, e não só para o texto. No celular, isso é a diferença entre acertar e errar o toque.

Quem navega por teclado não deveria ter que percorrer o menu inteiro, em toda página, para chegar ao conteúdo. O link de salto é o primeiro elemento do <body>: invisível para quem usa mouse, é o primeiro que aparece ao pressionar Tab.

site-evento/index.html — logo após <body> e o início do <main>

HTML
<body>
  <a href="#conteudo" class="salto">Pular para o conteúdo</a>

  <header class="cabecalho">
    <div class="container cabecalho__interno">
      <a href="index.html" class="logo">
        <img src="img/logo-sasi.svg" alt="Página inicial — Semana Acadêmica de Sistemas de Informação">
      </a>
      <nav aria-label="Principal">
        <ul class="menu">
          <li><a href="index.html" aria-current="page">Início</a></li>
          <li><a href="programacao.html">Programação</a></li>
          <li><a href="palestrantes.html">Palestrantes</a></li>
          <li><a href="contato.html">Contato</a></li>
          <li><a href="inscricao.html" class="menu__cta">Inscreva-se</a></li>
        </ul>
      </nav>
    </div>
  </header>

  <main id="conteudo" tabindex="-1">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
  </main>
</body>

site-evento/css/estilo.css — seção 6 (utilitários)

CSS
.salto {
  position: absolute;
  left: -9999px;            /* fora da tela, mas presente para o teclado e o leitor de tela */
  top: 1rem;
  padding: .75rem 1.25rem;
  background: var(--cor-primaria);
  color: #fff;
  border-radius: var(--raio-borda);
  z-index: 1000;
}

.salto:focus {
  left: 1rem;               /* ao receber foco, entra na tela */
}

O tabindex="-1" no <main> permite que ele receba foco ao ser alvo do link (sem entrar na sequência normal do Tab), o que faz o leitor de tela começar a ler dali. Sem isso, alguns navegadores rolam até o conteúdo, mas o foco continua no link de salto.

🔎 Por baixo do capô Por que left: -9999px e não display: none? Porque display: none remove o elemento da árvore de acessibilidade: ele deixa de existir para o teclado e para o leitor de tela. Deslocar para fora da tela mantém o elemento "vivo" — invisível, mas focável. É o mesmo raciocínio que você vai reencontrar na Aula 09, quando display não puder ser animado.

5.4 O que fica para a próxima aula

Esse menu funciona perfeitamente em telas largas. No celular, cinco links não cabem lado a lado — e a solução é o menu "hambúrguer", que abre e fecha. A versão responsiva do menu fica para a Aula 08, porque depende de media queries e de um pouco de raciocínio mobile first que você ainda não viu. Hoje, o menu quebra linha de forma aceitável graças ao flex-wrap que você vai adicionar na Mão na massa.

💻 Mão na massa — Esqueleto do site do evento com Grid e Flexbox

Você vai aplicar tudo isso no site da Semana Acadêmica de Sistemas de Informação. Ao final, as cinco páginas terão o mesmo cabeçalho fixo com menu acessível, o rodapé no fim da tela, a página inicial com um herói em duas colunas, a programação com lateral de filtros e grade de cartões, e os palestrantes em uma galeria que se reorganiza sozinha.

Passo 1 — Novas variáveis e a escala de z-index

Abra site-evento/css/estilo.css. Na seção 2 (variáveis), acrescente três variáveis ao bloco :root que você criou na Aula 06 e documente a escala de empilhamento em um comentário.

site-evento/css/estilo.css — seção 2 (variáveis), bloco completo

CSS
/* 2. Variáveis */
:root {
  --cor-primaria: #0b3d5c;
  --cor-secundaria: #1a7fb5;
  --cor-texto: #333333;
  --cor-fundo: #f7f9fb;
  --cor-superficie: #ffffff;   /* fundo de cabeçalho e cartões */
  --cor-borda: #dfe6ec;        /* linhas divisórias */
  --fonte-base: "Inter", Arial, sans-serif;
  --espaco-pequeno: 8px;
  --espaco-medio: 16px;
  --espaco-grande: 32px;
  --raio-borda: 8px;
  --sombra-cartao: 0 2px 8px rgba(0, 0, 0, 0.1);
  --largura-max: 1100px;       /* largura máxima do conteúdo */
}

/* Escala de z-index do projeto:
   100  cabeçalho fixo
   900  sobreposições escuras (overlay)
   1000 modais e link de salto */

Passo 2 — Rodapé no fim da tela e o contêiner central

Na seção 4 (layout), apague as três declarações do main que a Aula 06 escreveu (max-width, margin e padding) e ponha no lugar o padrão do body em coluna mais a classe .container reutilizável. A largura máxima continua sendo a mesma de sempre — 1100 px —, só que agora ela mora na variável --largura-max e é a .container que a aplica.

site-evento/css/estilo.css — seção 4 (layout), início

CSS
/* 4. Layout */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main {
  flex: 1;                     /* empurra o rodapé para o fim da janela */
}

.container {
  width: 100%;
  max-width: var(--largura-max);
  margin-inline: auto;         /* centraliza horizontalmente */
  padding-inline: var(--espaco-medio);
}

margin-inline e padding-inline são as versões "lógicas" de margin-left/margin-right — funcionam também em idiomas escritos da direita para a esquerda. Use-as sempre que a intenção for "dos lados".

Onde a .container entra no HTML. Na Aula 05 ela era uma <div> dentro de cada região; na Aula 06 a <div> saiu de dentro do <main>. Hoje ela volta ao conteúdo principal, mas como classe do próprio elemento, sem <div> extra. Percorra as cinco páginas e garanta que cada uma tenha exatamente um contêiner no conteúdo:

Página Onde vai a .container
index.html na <section class="container hero"> (Passo 5)
programacao.html no <main id="a07-conteudo" tabindex="-1" class="container pagina"> (Passo 6)
palestrantes.html no <main id="a07-conteudo" tabindex="-1" class="container"> (Passo 7)
inscricao.html no <main id="a07-conteudo" tabindex="-1" class="container">
contato.html no <main id="a07-conteudo" tabindex="-1" class="container">

Sem esse passo, inscricao.html e contato.html ficam com o conteúdo colado nas bordas da janela em monitores largos — e é o erro mais comum desta aula.

Passo 3 — Cabeçalho e menu em todas as páginas

Coloque o cabeçalho da seção 5.1 nas cinco páginas, trocando o aria-current="page" para o link da página correspondente. Em programacao.html, por exemplo:

site-evento/programacao.html — cabeçalho

HTML
<a href="#conteudo" class="salto">Pular para o conteúdo</a>

<header class="cabecalho">
  <div class="container cabecalho__interno">
    <a href="index.html" class="logo">
      <img src="img/logo-sasi.svg" alt="Página inicial — Semana Acadêmica de Sistemas de Informação">
    </a>

    <nav aria-label="Principal">
      <ul class="menu">
        <li><a href="index.html">Início</a></li>
        <li><a href="programacao.html" aria-current="page">Programação</a></li>
        <li><a href="palestrantes.html">Palestrantes</a></li>
        <li><a href="contato.html">Contato</a></li>
        <li><a href="inscricao.html" class="menu__cta">Inscreva-se</a></li>
      </ul>
    </nav>
  </div>
</header>

Antes de colar o CSS novo, apague da seção 4 da folha de estilo as três regras de cabeçalho que a Aula 06 escreveu: header { … }, header nav ul { … } e header nav li { display: inline-block; margin-right: var(--espaco-medio) }. As regras header h1, header h2 e header a também saem — o cabeçalho agora tem fundo claro, e texto branco nele ficaria invisível. Se você deixar o margin-right do header nav li no arquivo, ele soma com o gap: 1.5rem do .menu e o espaçamento entre os itens fica dobrado; é exatamente o primeiro sintoma da tabela de Erros comuns desta aula.

Feito isso, cole o CSS do cabeçalho e do menu da seção 5.2 na seção 4 da folha de estilo, e acrescente flex-wrap: wrap ao .menu, para que os links quebrem linha em vez de estourar o cabeçalho quando a janela for estreita — um paliativo até a Aula 08:

site-evento/css/estilo.css — ajuste no .menu

CSS
.menu {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.5rem;
  list-style: none;
}

Adicione o <a href="#a07-conteudo" class="salto"> como primeiro filho do <body> em todas as páginas, coloque id="a07-conteudo" tabindex="-1" no <main> de cada uma e cole o CSS de .salto na seção 6 (utilitários).

Passo 5 — Herói da página inicial com Grid

A página inicial abre com um "herói": título, texto de chamada, dois botões e a imagem do evento. Em telas largas, texto e imagem ficam lado a lado; em telas estreitas, empilham — e o auto-fit decide sozinho.

⚠️ Atenção Este bloco substitui a <section class="hero"> que a Aula 05 criou (aquela com o <h2>, um parágrafo e um botão só) e também o <picture> do banner da Aula 04, cuja imagem passa a ser a do herói. Apague os dois e cole o <main> abaixo no lugar. As seções "Sobre o evento", "Como participar", "Glossário", "Destaques desta edição" e "O evento em números" continuam onde estão, logo depois do herói, dentro do <main>.

site-evento/index.html — conteúdo do <main>

HTML
<main id="conteudo" tabindex="-1">
  <section class="container hero">
    <div class="hero__texto">
      <h1>Semana Acadêmica de Sistemas de Informação</h1>
      <p>Três dias de palestras, minicursos e maratona de programação no campus de Sinop. Aberto a estudantes de todos os cursos.</p>
      <div class="hero__acoes">
        <a class="botao" href="inscricao.html">Inscreva-se</a>
        <a class="botao botao--contorno" href="programacao.html">Ver programação</a>
      </div>
    </div>
    <img src="img/banner.jpg" alt="Auditório lotado durante a abertura da edição anterior" width="1200" height="800">
  </section>
</main>

site-evento/css/estilo.css — seção 4 (layout)

CSS
.hero {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: center;
  gap: var(--espaco-grande);
  padding-block: var(--espaco-grande);
}

.hero img {
  width: 100%;
  height: auto;
  border-radius: var(--raio-borda);
}

.hero__acoes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espaco-medio);
  margin-top: var(--espaco-medio);
}

O segundo botão usa .botao--contorno, o modificador que você já escreveu na Aula 06 — fundo transparente, texto na cor primária e inversão no :hover/:focus-visible. Não crie um .botao--secundario: seria o mesmo componente com outro nome, e componentes duplicados é como uma folha de estilo começa a apodrecer. Se você não encontrar o .botao--contorno na seção 5 da sua folha, é sinal de que o Passo 6 da Aula 06 ficou pela metade — volte lá antes de seguir.

Passo 6 — Programação: lateral de filtros e grade de cartões

A programação é a página mais rica do site. Em telas largas, ela tem uma lateral com filtros por dia e uma área de conteúdo com os cartões de cada atividade. É Grid para a estrutura (lateral + conteúdo) e Grid de novo, com auto-fit, para os cartões — e Flexbox dentro de cada cartão.

⚠️ Atenção Este <main> substitui o conteúdo da programacao.html das Aulas 02 e 04: a <table> dos três dias, a seção "Trilhas do evento" e a seção "As cinco atividades mais procuradas" saem daqui e dão lugar aos cartões. As seções de vídeo, áudio e mapa (Passos 5 a 7 da Aula 04) continuam, depois dos cartões, dentro da <section class="conteudo">. E as regras de table, caption, th, td, thead, tbody e tfoot que você escreveu na Aula 06 ficam na folha de estilo: elas continuam servindo a tabela de horário de atendimento de contato.html.

Repare também em onde o <h1> está: fora da <section class="conteudo">, como primeiro filho do <main>. Se ele ficasse depois do <aside>, a página abriria com um <h2> ("Filtrar por dia") antes do <h1> — hierarquia quebrada, e uma reprovação certa na auditoria de acessibilidade do Lighthouse.

site-evento/programacao.html — conteúdo do <main>

HTML
<main id="conteudo" tabindex="-1" class="container pagina">
  <h1>Programação</h1>

  <aside class="lateral">
    <h2>Filtrar por dia</h2>
    <nav aria-label="Dias do evento">
      <ul class="lista-filtros">
        <li><a href="#dia-1">Dia 1</a></li>
        <li><a href="#dia-2">Dia 2</a></li>
        <li><a href="#dia-3">Dia 3</a></li>
      </ul>
    </nav>
  </aside>

  <section class="conteudo">
    <h2 id="dia-1">Dia 1</h2>
    <ul class="cartoes">
      <li class="cartao">
        <h3>Abertura e palestra magna</h3>
        <p class="cartao__meta"><time datetime="19:00">19h</time> · Auditório Central</p>
        <p>O futuro do desenvolvimento web e o papel de quem está começando agora.</p>
        <a class="botao" href="inscricao.html">Inscrever-se</a>
      </li>
      <li class="cartao">
        <span class="cartao__selo">Esgotado</span>
        <h3>Minicurso: Git e GitHub do zero</h3>
        <p class="cartao__meta"><time datetime="20:00">20h</time> · Laboratório 2</p>
        <p>Versionamento, branches e o primeiro pull request. Traga o notebook.</p>
        <a class="botao" href="inscricao.html" aria-disabled="true">Lista de espera</a>
      </li>
      <li class="cartao">
        <h3>Mesa-redonda: mercado de trabalho em Sinop</h3>
        <p class="cartao__meta"><time datetime="20:00">20h</time> · Sala 105</p>
        <p>Egressos e empresas da região conversam sobre estágios e primeiro emprego.</p>
        <a class="botao" href="inscricao.html">Inscrever-se</a>
      </li>
    </ul>

    <h2 id="dia-2">Dia 2</h2>
    <ul class="cartoes">
      <li class="cartao">
        <h3>Minicurso: acessibilidade na prática</h3>
        <p class="cartao__meta"><time datetime="19:00">19h</time> · Laboratório 1</p>
        <p>Teste seu site com leitor de tela e só com o teclado.</p>
        <a class="botao" href="inscricao.html">Inscrever-se</a>
      </li>
      <li class="cartao">
        <h3>Palestra: segurança em aplicações web</h3>
        <p class="cartao__meta"><time datetime="20:30">20h30</time> · Auditório Central</p>
        <p>Os dez erros mais comuns e como evitá-los desde o primeiro commit.</p>
        <a class="botao" href="inscricao.html">Inscrever-se</a>
      </li>
    </ul>

    <h2 id="dia-3">Dia 3</h2>
    <ul class="cartoes">
      <li class="cartao">
        <h3>Maratona de programação</h3>
        <p class="cartao__meta"><time datetime="18:30">18h30</time> · Laboratórios 1 e 2</p>
        <p>Equipes de três pessoas, quatro horas, dez problemas. Premiação no encerramento.</p>
        <a class="botao" href="inscricao.html">Inscrever equipe</a>
      </li>
      <li class="cartao">
        <h3>Encerramento e premiação</h3>
        <p class="cartao__meta"><time datetime="22:00">22h</time> · Auditório Central</p>
        <p>Resultados da maratona, sorteios e confraternização.</p>
        <a class="botao" href="inscricao.html">Inscrever-se</a>
      </li>
    </ul>
  </section>
</main>

site-evento/css/estilo.css — seção 4 (layout) e seção 5 (componentes)

CSS
/* Estrutura da página de programação: lateral + conteúdo */
.pagina {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-areas:
    "titulo  titulo"
    "lateral conteudo";
  gap: var(--espaco-grande);
  padding-block: var(--espaco-grande);
}

.pagina > h1 { grid-area: titulo; }
.lateral     { grid-area: lateral; }
.conteudo    { grid-area: conteudo; }

.lateral {
  align-self: start;           /* não estica até o fim da coluna */
  position: sticky;            /* acompanha a rolagem, abaixo do cabeçalho */
  top: 88px;                   /* altura do cabeçalho + respiro */
}

.lista-filtros {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--espaco-pequeno);
}

.lista-filtros a {
  display: block;
  padding: var(--espaco-pequeno) var(--espaco-medio);
  border-left: 3px solid var(--cor-borda);
  color: var(--cor-texto);
  text-decoration: none;
}

.lista-filtros a:hover,
.lista-filtros a:focus-visible {
  border-left-color: var(--cor-primaria);
  color: var(--cor-primaria);
}

/* Grade de cartões: quantas colunas couberem, mínimo 280px cada */
.cartoes {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--espaco-medio);
  margin-block: var(--espaco-medio) var(--espaco-grande);
}

/* 5. Componentes */
.cartao {
  position: relative;          /* referência para o selo */
  display: flex;               /* Flexbox DENTRO do cartão */
  flex-direction: column;
  gap: var(--espaco-pequeno);
  padding: var(--espaco-medio);
  background: var(--cor-superficie);
  border: 1px solid var(--cor-borda);
  border-radius: var(--raio-borda);
  box-shadow: var(--sombra-cartao);
}

.cartao .botao {
  margin-top: auto;            /* empurra o botão para o fim, alinhando todos os cartões */
  align-self: flex-start;      /* o botão não estica até a largura do cartão */
}

.cartao__meta {
  color: var(--cor-secundaria);
  font-size: .875rem;
  font-weight: 600;
}

.cartao__selo {
  position: absolute;
  top: var(--espaco-pequeno);
  right: var(--espaco-pequeno);
  padding: 2px 10px;
  font-size: .75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  background: #b42318;
  color: #fff;
  border-radius: 999px;
}

Repare no truque margin-top: auto dentro de um contêiner flex em coluna: a margem automática absorve todo o espaço sobrando e empurra o botão para o rodapé do cartão. Como align-items: stretch (o padrão do Grid) deixa todos os cartões da linha com a mesma altura, os botões ficam alinhados mesmo que os textos tenham tamanhos diferentes.

Passo 7 — Palestrantes: galeria que se reorganiza sozinha

A página de palestrantes reaproveita .cartoes e .cartao, com uma variação para a foto.

⚠️ Atenção Este <main> substitui os seis <article> que as Aulas 02 e 04 escreveram (nome em <h3>, dois parágrafos e um <dl> de atividades e contato). Os nomes e as fotos são exatamente os mesmos — muda a marcação, que passa a ser uma <ul class="cartoes"> com um <li class="cartao"> por pessoa. Mostramos quatro aqui para o exemplo caber; repita o padrão para os seis do elenco.

site-evento/palestrantes.html — conteúdo do <main>

HTML
<main id="conteudo" tabindex="-1" class="container">
  <h1>Palestrantes</h1>
  <p>Profissionais e pesquisadores que vão compartilhar experiências nesta edição.</p>

  <ul class="cartoes">
    <li class="cartao cartao--palestrante">
      <img class="cartao__foto" src="img/palestrante-01.jpg" alt="Ana Lúcia Ferreira" width="400" height="400">
      <h2>Ana Lúcia Ferreira</h2>
      <p class="cartao__meta">Professora e pesquisadora · Inteligência Artificial</p>
      <p>Pesquisa redes neurais para prever a safra de soja e ministra o minicurso de redes neurais.</p>
    </li>
    <li class="cartao cartao--palestrante">
      <img class="cartao__foto" src="img/palestrante-02.jpg" alt="Bruno Takahashi" width="400" height="400">
      <h2>Bruno Takahashi</h2>
      <p class="cartao__meta">Startup AgroData · Ciência de Dados</p>
      <p>Constrói dashboards que os produtores realmente usam e fala sobre dados abertos.</p>
    </li>
    <li class="cartao cartao--palestrante">
      <img class="cartao__foto" src="img/palestrante-03.jpg" alt="Carla Mendes" width="400" height="400">
      <h2>Carla Mendes</h2>
      <p class="cartao__meta">UFMT · Segurança</p>
      <p>Pesquisa o que um ataque de phishing ensina sobre experiência de uso.</p>
    </li>
    <li class="cartao cartao--palestrante">
      <img class="cartao__foto" src="img/palestrante-04.jpg" alt="Diego Nascimento" width="400" height="400">
      <h2>Diego Nascimento</h2>
      <p class="cartao__meta">Prefeitura de Sinop · Desenvolvimento Web</p>
      <p>Trabalha com acessibilidade em portais públicos e conta os erros que a equipe já viu.</p>
    </li>
  </ul>
</main>

site-evento/css/estilo.css — seção 5 (componentes)

CSS
.cartao--palestrante {
  align-items: center;         /* centraliza foto, nome e texto no eixo transversal */
  text-align: center;
}

.cartao__foto {
  width: 120px;
  height: 120px;
  border-radius: 50%;          /* foto redonda */
  object-fit: cover;           /* recorta sem distorcer — a Aula 08 aprofunda */
}

Passo 8 — Rodapé em três colunas

⚠️ Atenção Este rodapé substitui o <footer> de três parágrafos da Aula 02 (realização, e-mail/telefone e linha de copyright). Nenhuma informação se perde: a realização vira a coluna "Sobre o evento", o e-mail vai para o <address> da coluna "Contato" e a linha de copyright vira o .rodape__creditos. Apague o bloco antigo nas cinco páginas e cole este no lugar — inclusive o <nav aria-label="Rodapé"> que a Aula 05 acrescentou em index.html, cujos links agora vivem na coluna "Links".

site-evento/index.html — antes de </body>, em todas as páginas

HTML
<footer class="rodape">
  <div class="container rodape__grade">
    <section>
      <h2>Sobre o evento</h2>
      <p>Organizado por professores e estudantes dos cursos de computação, com apoio do centro acadêmico.</p>
    </section>
    <section>
      <h2>Links</h2>
      <ul>
        <li><a href="programacao.html">Programação completa</a></li>
        <li><a href="inscricao.html">Inscrições</a></li>
        <li><a href="contato.html">Fale com a organização</a></li>
      </ul>
    </section>
    <section>
      <h2>Contato</h2>
      <address>
        <a href="mailto:semana@exemplo.edu.br">semana@exemplo.edu.br</a><br>
        Avenida dos Ingás, 3001 — Sinop, MT
      </address>
    </section>
  </div>
  <p class="rodape__creditos">Semana Acadêmica de Sistemas de Informação</p>
</footer>

site-evento/css/estilo.css — seção 4 (layout)

CSS
.rodape {
  margin-top: var(--espaco-grande);
  padding-block: var(--espaco-grande);
  background: var(--cor-primaria);
  color: #fff;
}

.rodape__grade {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--espaco-grande);
}

.rodape h2 {
  font-size: 1rem;
  margin-bottom: var(--espaco-pequeno);
}

.rodape ul {
  list-style: none;
}

.rodape a {
  color: #fff;
}

.rodape address {
  font-style: normal;
}

.rodape__creditos {
  margin-top: var(--espaco-grande);
  text-align: center;
  font-size: .875rem;
  opacity: .85;
}

Passo 9 — Um paliativo para telas estreitas

A grade de cartões e o rodapé já se adaptam sozinhos graças ao auto-fit. A página de programação, não: com 240px 1fr, em um celular a coluna de conteúdo fica com 100 px de largura. Por hoje, uma media query simples resolve — a Aula 08 vai inverter essa lógica para mobile first e formalizar os breakpoints.

site-evento/css/estilo.css — seção 7 (media queries)

CSS
/* 7. Media queries */
@media (max-width: 1023px) {
  .pagina {
    grid-template-columns: 1fr;
    grid-template-areas:
      "titulo"
      "conteudo"
      "lateral";               /* no celular, os filtros vão para depois do conteúdo */
  }

  .lateral {
    position: static;          /* deixa de acompanhar a rolagem */
  }
}

Como testar

  1. Abra index.html com o Live Server. O cabeçalho deve ficar grudado no topo ao rolar, com o logo à esquerda e o menu à direita, e o link "Início" destacado com o sublinhado na cor primária.
  2. Pressione Tab uma vez: o botão "Pular para o conteúdo" deve aparecer no canto superior esquerdo. Pressione Enter: o foco vai para o <main>. Continue com Tab: cada link do menu mostra o sublinhado crescendo.
  3. Abra contato.html (a página com menos conteúdo): o rodapé deve estar colado no fim da janela, não flutuando no meio, e o conteúdo deve parar em 1100 px no centro — se ele encostar nas bordas, faltou a .container no <main> desta página. Confira o mesmo em inscricao.html.
  4. Passe o mouse entre dois itens do menu e meça o espaço no DevTools: ele deve ser um único gap de 24 px. Se parecer o dobro, a regra header nav li da Aula 06 continua na folha — apague-a.
  5. Em programacao.html, a lateral fica à esquerda e os cartões à direita, em colunas de no mínimo 280 px; o selo "Esgotado" aparece no canto superior direito do minicurso de Git; todos os botões de uma mesma linha ficam alinhados na base. Redimensione a janela: abaixo de 1024 px, tudo vira uma coluna e a lateral vai para o fim.
  6. Em palestrantes.html, as fotos são redondas e os cartões se reorganizam de 4 para 3, 2 e 1 coluna conforme você estreita a janela — sem nenhuma media query.
  7. No DevTools, clique no selo grid ao lado de <ul class="cartoes"> e no selo flex ao lado de <div class="cabecalho__interno"> e confira, com o overlay, que a estrutura é a que você desenhou.

🧪 Laboratório

Nível A — Fixação

A1. Explique a diferença entre position: relative, absolute e fixed quanto a dois critérios: a referência de posicionamento e a ocupação de espaço no fluxo.

A2. Por que um elemento com position: absolute costuma exigir position: relative no elemento pai? O que acontece se o pai não tiver essa declaração?

A3. Em um contêiner com display: flex e flex-direction: row, qual propriedade alinha os itens horizontalmente? E verticalmente? E se a direção for column?

A4. Diferencie space-between, space-around e space-evenly. Desenhe (em papel ou em texto) três itens em cada caso, marcando os espaços.

A5. O que significa flex: 1? Escreva sua forma expandida (as três propriedades) e explique o que cada valor faz.

A6. Escreva o CSS para centralizar perfeitamente um elemento na tela, horizontal e verticalmente, usando Flexbox. Depois reescreva usando Grid com uma linha só.

A7. O que a unidade fr representa? Em grid-template-columns: 200px 1fr 2fr, com um contêiner de 1100 px e gap: 0, qual é a largura de cada coluna? Mostre a conta.

A8. Explique, palavra por palavra, o que faz repeat(auto-fit, minmax(250px, 1fr)). Quantas colunas ela cria em uma janela de 1100 px? E em uma de 600 px?

A9. Escreva um grid-template-areas para um layout com cabeçalho no topo (largura total), lateral à esquerda, conteúdo à direita e rodapé embaixo (largura total). Inclua o grid-template-columns e o grid-template-rows correspondentes.

A10. Quando você usaria Flexbox e quando usaria Grid? Dê dois exemplos concretos de cada, tirados do site do evento.

A11. Por que float não deve mais ser usado para layout? Cite o único caso em que ele ainda é a ferramenta certa.

Nível B — Aplicação

B1. Construa uma barra de navegação com Flexbox: logo à esquerda, cinco links à direita, item ativo destacado com aria-current="page". Em telas abaixo de 768 px, os links devem virar uma coluna centralizada abaixo do logo.

Resultado esperado: em 1200 px, logo e links na mesma linha, com space-between; em 500 px, o logo centralizado em cima e os cinco links empilhados e centralizados abaixo; o link ativo tem cor e sublinhado diferentes; Tab percorre os links na ordem do HTML com foco visível.

Dica

Na media query, troque flex-direction do contêiner do cabeçalho para column e do .menu também; align-items: center centraliza os dois. Não use order — a ordem do HTML já é a que você quer.

B2. Implemente o layout "santo graal": cabeçalho, três colunas (lateral esquerda fixa de 200 px, conteúdo elástico, lateral direita fixa de 200 px) e rodapé, com o rodapé sempre no fim da janela mesmo com pouco conteúdo. Em telas pequenas, tudo vira uma única coluna. Faça com Grid e áreas nomeadas.

Resultado esperado: em 1200 px, cinco áreas visíveis com grid-template-areas de três linhas; com um parágrafo só no conteúdo, o rodapé continua colado no fim da janela; abaixo de 768 px, as áreas se empilham na ordem cabeçalho → conteúdo → lateral esquerda → lateral direita → rodapé.

Dica

Use min-height: 100vh e grid-template-rows: auto 1fr auto no contêiner: a linha 1fr absorve o espaço sobrando. Na media query, redesenhe as áreas com uma palavra por linha — a ordem das strings define a ordem visual, sem tocar no HTML.

B3. Crie uma seção de "planos de inscrição" para o site do evento com três planos lado a lado (Flexbox): Estudante, Profissional e Apoiador. O do meio deve ser destacado (maior, com borda colorida e um selo "Mais popular" posicionado com absolute). Responsivo: os três empilham abaixo de 768 px.

Resultado esperado: três cartões com a mesma altura em telas largas, o do meio 8% maior (transform: scale(1.08) ou padding maior) e com borda na cor primária; o selo fica preso no canto superior direito do cartão do meio, mesmo ao redimensionar; no celular, os cartões ficam um abaixo do outro e o do meio deixa de ser maior.

Dica

align-items: stretch (o padrão) já iguala as alturas. Para o selo, position: relative no cartão e absolute no selo — a receita da seção 1.2. Lembre-se de flex: 1 1 260px nos cartões para que eles cresçam e quebrem linha.

B4. Reescreva o layout do exercício B3 duas vezes: uma versão só com Flexbox e outra só com Grid. Escreva meia página comparando a quantidade de código, a legibilidade e a facilidade de alteração de cada abordagem (ex.: o que muda para acrescentar um quarto plano?).

Resultado esperado: dois arquivos HTML funcionais e visualmente idênticos, e um texto de comparação com pelo menos três diferenças concretas, apontando qual versão você escolheria para o site do evento e por quê.

Dica

Conte as linhas de CSS de cada versão. Na versão Grid, experimente grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)) e veja o que acontece ao adicionar um quarto cartão. Na versão Flexbox, veja o que flex-wrap faz com um número ímpar de cartões na última linha.

B5. Construa um painel (dashboard) da organização do evento com Grid: cabeçalho, quatro cartões de indicadores na primeira linha (inscritos, vagas, palestrantes, minicursos), um cartão grande de "inscrições por dia" ocupando três colunas e um de "últimas inscrições" ocupando uma coluna na segunda linha, e uma tabela de atividades ocupando toda a largura na terceira. Responsivo em três breakpoints.

Resultado esperado: em 1200 px, uma grade de quatro colunas com os spans corretos; em 768 px, duas colunas (os indicadores em 2 × 2, o gráfico em largura total); no celular, tudo em uma coluna; a tabela nunca estoura a largura (use um contêiner com overflow-x: auto).

Dica

Defina grid-template-columns: repeat(4, 1fr) na base e use grid-column: span 3 e span 4 nos itens grandes. Nas media queries, troque o repeat(4, 1fr) por repeat(2, 1fr) e 1fr, e ajuste os spans para que nenhum item peça mais colunas do que a grade tem — se pedir, o Grid cria colunas implícitas e o layout estoura.

Nível C — Desafio

C1. Base do projeto autoral. Defina o tema do seu projeto (o "site do evento" da sua versão: uma feira, um campeonato, uma semana cultural, um curso de extensão) e construa o esqueleto completo de todas as páginas: HTML semântico, estrutura de layout com Grid e áreas nomeadas, componentes internos com Flexbox, sistema de design em variáveis, navegação funcional entre as páginas, cabeçalho fixo e rodapé consistentes em todas elas, link de salto funcionando. Produza também um wireframe (papel fotografado, Figma ou Excalidraw) de três larguras: celular, tablet e desktop. Este exercício é a primeira etapa do que o Marco 2 vai cobrar (Aula 10).

Dica

Comece pelo wireframe, não pelo código: desenhe as três larguras em dez minutos e só então decida quais áreas nomeadas cada página precisa. Reaproveite .container, .cabecalho, .menu, .cartoes e .rodape da Mão na massa — o que muda no seu projeto é o conteúdo e as cores, não a arquitetura. Valide o HTML de cada página no W3C antes de estilizar.

🏆 Desafios

⭐ Flexbox Froggy e Grid Garden até o fim

cssflexboxgrid

Você consegue levar 24 sapos para as vitórias-régias certas usando só justify-content, align-items e flex-wrap? E regar 28 cenouras com grid-column, grid-area e grid-template-areas? Os dois jogos foram criados para fixar, por repetição, exatamente as propriedades desta aula — e muita gente que "já sabia Flexbox" descobre no nível 18 que não sabia align-content. Jogue os dois em português (https://flexboxfroggy.com/#pt-br e https://cssgridgarden.com/#pt-br) e anote o que travou.

Critérios de pronto

  • Captura de tela do último nível de cada jogo concluído (nível 24 do Froggy e nível 28 do Garden).
  • Um arquivo aprendizados.md com três propriedades ou valores que você não conhecia (ou usava errado) antes dos jogos, com uma frase explicando cada um.
  • Para cada uma das três, um trecho do site do evento onde ela poderia ser aplicada.
Pistas
  1. Se travar em um nível, abra a referência de Flexbox da MDN em pt-BR e procure a propriedade que o enunciado do nível cita — o jogo sempre diz qual é.
  2. No Froggy, os níveis a partir do 19 combinam flex-direction com justify-content: lembre que o eixo principal muda de lugar.
  3. No Garden, grid-column: 2 / span 3 e grid-column: 2 / 5 fazem a mesma coisa — os níveis finais testam se você entende as duas formas.
⭐⭐

⭐⭐ Caça ao bug de layout

csslayoutbugdevtools

O código abaixo era para produzir um cabeçalho fixo com menu à direita, uma página com lateral à esquerda e conteúdo à direita, e um selo "Esgotado" no canto do cartão. Nada disso acontece: o cabeçalho rola junto com a página, a lateral cai embaixo do conteúdo, o selo aparece no canto da janela e o menu tem marcadores e espaços duplos. Há cinco bugs. Encontre e corrija todos usando apenas o DevTools como ferramenta de investigação — sem reescrever do zero.

caca-ao-bug.html

HTML
<body>
  <header class="topo">
    <div class="topo__interno">
      <a class="logo" href="index.html">Semana</a>
      <nav aria-label="Principal">
        <ul class="menu">
          <li><a href="index.html">Início</a></li>
          <li><a href="programacao.html">Programação</a></li>
          <li><a href="contato.html">Contato</a></li>
        </ul>
      </nav>
    </div>
  </header>
  <main class="grade">
    <aside class="lateral">Filtros</aside>
    <section class="conteudo">
      <div class="cartao">
        <span class="selo">Esgotado</span>
        <h2>Minicurso de Git</h2>
        <p>Versionamento do zero ao primeiro pull request.</p>
      </div>
    </section>
  </main>
</body>

caca-ao-bug.css

CSS
body { margin: 0; overflow-x: hidden; }
.topo { position: sticky; z-index: 10; background: #fff; }
.topo__interno { display: flex; justify-content: space-between; padding: 16px; }
.menu { display: flex; gap: 24px; }
.menu li { margin-right: 24px; }
.grade { display: grid; grid-template-columns: 240px 1fr; grid-template-areas: "lateral conteudo"; gap: 24px; }
.lateral { grid-area: sidebar; }
.conteudo { grid-area: conteudo; }
.cartao { padding: 16px; border: 1px solid #ccc; }
.selo { position: absolute; top: 8px; right: 8px; background: #b42318; color: #fff; padding: 2px 8px; }

Critérios de pronto

  • O cabeçalho gruda no topo ao rolar a página.
  • A lateral fica à esquerda e o conteúdo à direita, na mesma linha.
  • O selo "Esgotado" fica no canto superior direito do cartão, mesmo ao rolar e redimensionar.
  • O menu não tem marcadores de lista e os links têm exatamente 24 px de espaço entre si.
  • Um comentário acima de cada correção explica o bug em uma linha (por que o comportamento errado acontecia).
Pistas
  1. sticky precisa de duas coisas para funcionar: uma delas está faltando na regra, a outra está sendo sabotada por um ancestral — releia a seção 1.2 e o Investigue da seção 1.5.
  2. No DevTools, ative o overlay da grade e "Show area names": o nome que aparece na primeira coluna é o mesmo que a .lateral está pedindo?
  3. absolute procura o ancestral posicionado mais próximo. Qual é o ancestral posicionado mais próximo do selo neste código?
  4. gap e margin não se anulam — se somam. E ul tem um estilo padrão que ninguém removeu.
⭐⭐

⭐⭐ Mosaico de destaques com Grid

cssgridlayoutprojeto

A página inicial do site do evento precisa de um bloco de destaques em formato de revista: sete cartões de tamanhos diferentes — um grande (2 × 2), dois largos (2 × 1), um alto (1 × 2) e três pequenos (1 × 1) — encaixados sem buracos, como um mosaico. O detalhe: quando a janela estreita e a grade perde colunas, os buracos aparecem, a menos que você conheça uma propriedade que faz o Grid "preencher para trás". Descubra qual é e construa o mosaico.

Critérios de pronto

  • Em 1100 px, os sete cartões ocupam uma grade de quatro colunas sem nenhuma célula vazia.
  • Em 700 px (duas colunas) e em 400 px (uma coluna), continua não havendo buracos e nenhum cartão pede mais colunas do que existem.
  • Nenhum cartão é posicionado com números de linha fixos (grid-column: 3 / 5) — só com span.
  • A ordem de leitura por teclado (Tab nos links dos cartões) faz sentido mesmo quando o Grid reordena visualmente.
Pistas
  1. Procure grid-auto-flow na MDN e leia sobre o valor dense.
  2. grid-auto-rows com um valor fixo (ou minmax) dá altura às linhas implícitas, para que span 2 em linhas tenha efeito visível.
  3. Na media query de duas colunas, reduza todo span maior que 2 para span 2; em uma coluna, span 1 para todos.
  4. dense pode mover um cartão para antes de outro que vem primeiro no HTML. Teste com Tab e, se a ordem ficar estranha, reordene o HTML — não o CSS.
⭐⭐⭐

⭐⭐⭐ Recriação de um layout real

cssgridflexboxlayoutresponsivo

Você consegue reproduzir a página inicial de um site que usa todo dia? Escolha a home de um site conhecido (o portal da sua universidade ou escola, um jornal, uma loja, a página inicial do GitHub) e reproduza seu layout — estrutura e disposição, com conteúdo próprio e sem copiar imagens — usando Grid para a página e Flexbox para os componentes, com responsividade em três larguras. Requisito inegociável: zero float e zero position: absolute para layout (o absolute só pode aparecer em selos e ícones). Ao terminar, você vai perceber quanto dos sites que admira é só Grid e Flexbox bem combinados. Isso entra bem no Marco 2 da unidade.

Critérios de pronto

  • Três pares de capturas lado a lado (original × sua versão) em 375 px, 768 px e 1440 px.
  • O arquivo CSS tem, no topo, um comentário com o desenho das áreas nomeadas de cada largura.
  • O DevTools não mostra nenhum float e nenhum position: absolute em elementos estruturais (cabeçalho, colunas, seções, rodapé).
  • O menu principal é uma lista dentro de <nav aria-label>, com aria-current e link de salto.
  • HTML validado sem erros no W3C.
Pistas
  1. Antes de escrever CSS, abra o site escolhido com o DevTools e clique nos selos grid e flex: muitos sites profissionais deixam a estrutura à mostra.
  2. Desenhe o grid-template-areas das três larguras em papel primeiro; se você não consegue desenhar, não consegue codificar.
  3. Comece pela largura maior (a mais complexa) e reduza — nesta aula ainda é aceitável; na Aula 08 você vai inverter.
  4. Onde o original usa uma imagem de fundo com texto por cima, use uma célula do Grid com background-image e um div com Flexbox para o texto — sem absolute.

🐛 Erros comuns

Sintoma Causa Solução
justify-content: center centralizou na vertical em vez de na horizontal O contêiner está em flex-direction: column; justify-content segue o eixo principal, que agora é vertical Use align-items para o eixo transversal; releia a seção 2.2 sobre os eixos
Espaço duplo (ou irregular) entre os itens do menu margin nos itens somado ao gap do contêiner Remova as margens e use só gap
O cabeçalho com position: sticky não gruda Falta top: 0, ou um ancestral tem overflow: hidden/auto (inclusive overflow-x: hidden no body) Declare top e remova o overflow do ancestral — ou aplique o overflow só ao elemento que realmente precisa
O selo absolute foi parar no canto da página Nenhum ancestral tem position diferente de static position: relative no cartão que deve servir de referência
A lateral caiu para baixo do conteúdo em uma grade de duas colunas O nome em grid-area não bate com o nome em grid-template-areas (letra a mais, acento, maiúscula), e o item foi posicionado automaticamente Confira os nomes com "Show area names" no DevTools; use nomes sem acento e em minúsculas
Cartões com auto-fit ficaram encolhidos à esquerda quando há poucos itens Usou auto-fill, que mantém as colunas vazias Troque por auto-fit, que colapsa as faixas vazias e deixa os itens esticarem
A grade "estourou" e criou uma coluna extra fora do contêiner Um item pede grid-column: span 4 em uma grade de 3 colunas; o Grid cria colunas implícitas Nunca peça mais colunas do que a grade tem; ajuste os spans em cada media query
O layout inteiro rola horizontalmente no celular Um contêiner com width: 1100px fixo, ou uma imagem maior que a tela max-width + width: 100% no contêiner; img { max-width: 100%; height: auto; }
z-index: 9999 e o elemento continua atrás de outro O elemento está dentro de um contexto de empilhamento com z-index menor (criado pelo pai com position + z-index, transform ou opacity) Suba o z-index do ancestral que cria o contexto, ou mova o elemento para fora dele; documente a escala do projeto
O rodapé sobe para o meio da tela em páginas curtas O body não é um contêiner flex em coluna, ou o main não tem flex: 1 body { display: flex; flex-direction: column; min-height: 100vh; } main { flex: 1; }
O menu fica com marcadores (bolinhas) list-style padrão da ul não foi removido .menu { list-style: none; } — e mantenha a lista: ela é semântica
A ordem do Tab não bate com a ordem visual order (Flexbox) ou grid-auto-flow: dense reordenou só a apresentação Reordene o HTML, não o CSS; use order apenas para pequenos ajustes decorativos

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). SILVA, Maurício Samy. Criando sites com HTML, capítulos de posicionamento e layout. MDN Web Docs em pt-BR: os guias "Conceitos básicos do Flexbox" e "Conceitos básicos do Grid Layout" (links em "Para aprofundar"). Anote uma propriedade de cada guia que não apareceu nesta aula.

Parte 2 — Produção (30 min). No seu projeto autoral:

  1. Exercícios B1 (barra de navegação) e B5 (dashboard), em arquivos separados na pasta exercicios/aula07/.
  2. O layout principal do projeto construído com Grid e áreas nomeadas em pelo menos uma página (a mais complexa), com Flexbox no interior dos componentes.
  3. O menu de navegação completo em todas as páginas: lista semântica, nav aria-label, aria-current na página atual, :focus-visible em todos os links e link de salto funcionando.

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um componente do seu projeto (um trecho de HTML + CSS), diga qual dos dois você usou (Grid ou Flexbox) e justifique tecnicamente com base na tabela da seção 4. Se puder, compare a escolha com a de outra pessoa.

Critério de pronto: ao pressionar Tab na primeira página do seu projeto, o link de salto aparece; ao pressionar Enter, o foco vai para o conteúdo; navegando pelo menu, cada link mostra foco visível e a página atual está marcada com aria-current; ao abrir a página mais curta, o rodapé está no fim da janela; ao clicar nos selos grid/flex do DevTools, a estrutura desenhada é a que você planejou.

Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).

✅ Checkpoint do projeto

Ao fim desta aula, o seu projeto autoral deve ter:

  • [ ] css/estilo.css com as variáveis --cor-superficie, --cor-borda e --largura-max e a escala de z-index documentada em comentário.
  • [ ] body em Flexbox de coluna com main { flex: 1 } — rodapé no fim da tela em todas as páginas.
  • [ ] Classe .container com max-width e margin-inline: auto em uso em todas as páginas.
  • [ ] Cabeçalho sticky com logo e menu alinhados por Flexbox, presente e idêntico nas cinco páginas.
  • [ ] Menu como ul dentro de <nav aria-label="Principal">, com aria-current="page" na página atual e estados :hover e :focus-visible.
  • [ ] Link de salto como primeiro elemento do <body> e <main id="a07-conteudo" tabindex="-1">.
  • [ ] Pelo menos uma grade de cartões com repeat(auto-fit, minmax(…, 1fr)).
  • [ ] Pelo menos uma página com estrutura em Grid e áreas nomeadas.
  • [ ] Nenhum float e nenhum position: absolute usado para layout.
  • [ ] HTML de todas as páginas sem erros no validador do W3C.

📚 Para aprofundar

Na próxima aula, o site do evento vai para o celular de verdade: você vai reescrever a folha de estilo no modo mobile first, definir breakpoints onde o layout quebra (e não onde o iPhone manda), transformar o menu em um hambúrguer acessível e respeitar as preferências do usuário — tema escuro e redução de movimento.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Fluxo normal e posicionamento1.1 O que o navegador faz quando você não diz nada1.2 position: cinco maneiras de dizer "fique aqui"1.3 z-index e a pilha de elementos1.4 float — o legado que você precisa reconhecer1.5 overflow: o que acontece quando o conteúdo não cabe2. Flexbox — layout em uma dimensão2.1 O que muda com display: flex2.2 Os dois eixos2.3 Propriedades do contêiner2.4 Propriedades dos itens2.5 Os quatro padrões que resolvem quase tudo3. CSS Grid — layout em duas dimensões3.1 Linhas e colunas ao mesmo tempo3.2 Definindo colunas3.3 Posicionando itens na grade3.4 Áreas nomeadas — o layout desenhado no código3.5 Alinhamento no Grid4. Flexbox ou Grid?5. Construindo o menu de navegação5.1 Marcação5.2 Estilo5.3 Link de salto — o detalhe profissional5.4 O que fica para a próxima aula💻 Mão na massa — Esqueleto do site do evento com Grid e FlexboxPasso 1 — Novas variáveis e a escala de z-indexPasso 2 — Rodapé no fim da tela e o contêiner centralPasso 3 — Cabeçalho e menu em todas as páginasPasso 4 — Link de saltoPasso 5 — Herói da página inicial com GridPasso 6 — Programação: lateral de filtros e grade de cartõesPasso 7 — Palestrantes: galeria que se reorganiza sozinhaPasso 8 — Rodapé em três colunasPasso 9 — Um paliativo para telas estreitasComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ Flexbox Froggy e Grid Garden até o fim⭐⭐ Caça ao bug de layout⭐⭐ Mosaico de destaques com Grid⭐⭐⭐ Recriação de um layout real🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 2 · CSS: estilo, layout e responsividade3 aulas de 50 min + 1 h EAD

Aula 08 — Criando telas responsivas

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Explicar o que a meta viewport faz e por que, sem ela, nenhuma técnica responsiva funciona.
  • Escrever CSS mobile first: a regra base serve ao celular e as media queries de min-width só acrescentam o que muda em telas maiores.
  • Definir breakpoints a partir do ponto em que o layout quebra, e não de uma lista de aparelhos.
  • Construir grades fluidas que se adaptam sem nenhuma media query, com auto-fit e minmax().
  • Tornar imagens e tipografia fluidas com object-fit, aspect-ratio, srcset e clamp().
  • Implementar um menu hambúrguer acessível, com <button>, aria-expanded e aria-controls.
  • Respeitar preferências do sistema do usuário: tema escuro, redução de movimento e impressão.

📋 Pré-requisitos

  • [ ] Pasta site-evento/ com as cinco páginas e o css/estilo.css como ficaram ao fim da Aula 07: cabeçalho fixo com menu horizontal, link de salto, grade de cartões com auto-fit, página de programação com áreas nomeadas e rodapé em três colunas.
  • [ ] VS Code com a extensão Live Server e um navegador com DevTools (Chrome ou Firefox).
  • [ ] Um celular conectado à mesma rede Wi-Fi do computador (opcional, mas vai valer a pena no bloco 3).
  • [ ] Revisar da Aula 07: repeat(auto-fit, minmax()), grid-template-areas e a diferença entre justify-content e align-items.

Na aula passada você estruturou o site da Semana Acadêmica de Sistemas de Informação com Grid e Flexbox e construiu o menu horizontal — mas só olhou o resultado no monitor do laboratório. Abra a mesma página no celular: o menu estoura para fora da tela, os cartões ficam estreitos demais e a lateral de filtros esmaga o conteúdo. Hoje você reescreve a folha de estilo de trás para a frente — primeiro o celular, depois as telas maiores — e cumpre a promessa da Aula 07: o menu hambúrguer.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Viewport, os três pilares do design responsivo, media queries, mobile first e breakpoints
2 50 min Grades fluidas sem media query, imagens responsivas (object-fit, srcset), tipografia com clamp()
3 50 min Menu hambúrguer acessível, preferências do usuário, testes no celular real e Mão na massa

1. Por que "responsivo" — e o que a meta viewport tem a ver com isso

O mesmo index.html que você escreveu na Aula 02 vai ser aberto em um celular de 360 px de largura, em um tablet de 768 px, em um notebook de 1366 px e em um monitor de 2560 px. Não existe "a tela": existe um intervalo contínuo de larguras, e o site precisa funcionar em qualquer ponto dele.

Até o começo dos anos 2010 a solução era manter dois sites: www.site.com para o computador e m.site.com para o celular, com HTML duplicado e conteúdo sempre desatualizado em um dos dois. Design responsivo é a ideia de que um único HTML se adapta à largura disponível, e ele se apoia em três pilares:

  1. A meta viewport, que diz ao celular para não fingir que é um monitor.
  2. Layouts fluidos, feitos com larguras relativas (%, fr, minmax(), max-width) em vez de pixels fixos.
  3. Media queries, para os ajustes pontuais que a fluidez sozinha não resolve.

Repare na ordem: as media queries vêm por último. Um layout bem feito com Grid e Flexbox, como o da Aula 07, já se adapta a boa parte das larguras sem nenhuma query. Elas entram para os pontos em que a fluidez não basta — e a Mão na massa de hoje vai mostrar que são bem poucos.

🧠 Você sabia? O termo Responsive Web Design foi cunhado por Ethan Marcotte em um artigo de 2010 na revista A List Apart. Ele emprestou a ideia da arquitetura responsiva — prédios cujas paredes e iluminação reagem à presença das pessoas — e propôs que páginas fizessem o mesmo com a largura da janela. Antes disso, "site para celular" significava um segundo site.

A meta viewport

Ela está no <head> de todas as suas páginas desde a Aula 02. Hoje você entende o que ela faz.

site-evento/index.html (dentro do <head>, igual nas cinco páginas)

HTML
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Parte O que significa
width=device-width A largura de layout da página passa a ser a largura real do aparelho em pixels CSS (360 px em um celular comum), e não os 980 px que ele finge ter por padrão.
initial-scale=1.0 O zoom inicial é 1:1 — um pixel CSS corresponde a um pixel "lógico" do aparelho. Sem isso, alguns navegadores abrem a página afastada.

Sem essa linha, o celular renderiza a página como se a janela tivesse 980 px de largura e depois encolhe tudo para caber na tela. O resultado é aquele site minúsculo em que você precisa dar zoom para ler qualquer coisa — e nenhuma media query dispara, porque para o navegador a largura "é" 980 px.

⚠️ Atenção Nunca acrescente user-scalable=no nem maximum-scale=1 à meta viewport. Isso bloqueia o zoom, que é a única forma de muita gente conseguir ler o seu site. Os navegadores atuais ignoram essa proibição por questão de acessibilidade, e o Lighthouse penaliza a página que tenta usá-la.

🔎 Por baixo do capô O navegador do celular trabalha com duas "janelas". A viewport de layout é a largura que o CSS enxerga — é nela que width: 100% e as media queries são calculadas. A viewport visual é o pedaço que aparece na tela depois do zoom. A meta viewport iguala as duas na abertura da página. E "pixel CSS" não é o mesmo que pixel físico: um celular com tela de 1080 px físicos e devicePixelRatio igual a 3 reporta 360 px CSS. É por isso que o seu layout de 360 px fica nítido: cada pixel CSS é desenhado com 3 × 3 pixels de verdade.

Toda vez que a largura da viewport muda — você gira o celular, redimensiona a janela, abre o DevTools — o navegador recalcula a posição e o tamanho de todas as caixas da página. Esse recálculo se chama reflow (ou layout). Ele é caro, e vai voltar a aparecer na Aula 09 quando falarmos de animações que travam.

🔬 Investigue Abra site-evento/index.html no Live Server e o console do DevTools (F12). Digite window.innerWidth e anote o valor. Ative o modo dispositivo (Ctrl+Shift+M), escolha "Pixel 7" ou outro celular e repita: o valor cai para algo como 412, mesmo com screen.width informando outra coisa. Agora digite window.devicePixelRatio. Por fim, comente a linha da meta viewport no HTML, salve e observe window.innerWidth de novo no modo dispositivo: 980. Descomente antes de seguir.

Os outros dois pilares

Os layouts fluidos são o assunto da seção 2; as media queries, da seção 3. Vale antecipar a regra de ouro que conecta os dois: primeiro tente resolver com fluidez; só escreva uma media query quando o layout quebrar.

2. Layouts fluidos: o que se adapta sozinho

O contêiner que respira

Você já tem esta regra desde a Aula 07. Releia com outros olhos:

site-evento/css/estilo.css (seção 4 — layout)

CSS
.container {
  width: 100%;
  max-width: var(--largura-max);
  margin-inline: auto;
  padding-inline: var(--espaco-medio);
}

Compare com a versão que muita gente escreve e que quebra no celular:

CSS
/* ❌ Quebra em qualquer tela com menos de 1100 px */
.container {
  width: 1100px;
  margin: 0 auto;
}

A diferença é o par width: 100% + max-width. Em uma tela de 360 px o contêiner ocupa 360 px; em uma de 1920 px ele para em 1100 px e centraliza. Uma regra só, sem media query. O padding-inline garante que o texto não encoste na borda do celular.

⚠️ Atenção width fixa em pixels é a causa número um de rolagem horizontal no celular. Se você precisa de um limite, o limite é max-width. Se precisa de um mínimo, é min-width — e mesmo assim pense duas vezes.

Mídia nunca estoura

site-evento/css/estilo.css (seção 3 — base)

CSS
img,
video {
  max-width: 100%;
  height: auto;
}

Uma foto de 1600 px de largura, sem essa regra, empurra o contêiner de 360 px para 1600 px e cria uma barra de rolagem horizontal na página inteira. Com max-width: 100% ela encolhe até caber; o height: auto mantém a proporção mesmo quando o HTML declara width e height no <img> — e você vai declarar, como mostra a seção 4.

Unidades que se adaptam

Unidade Relativa a Use para
% largura do elemento pai larguras de colunas e de contêineres
fr espaço livre da grade colunas e linhas do Grid
vw / vh 1 % da largura / altura da viewport seções de altura de tela, tipografia fluida
rem tamanho da fonte da raiz espaçamentos, fontes, breakpoints
ch largura do caractere "0" largura máxima de texto corrido (max-width: 65ch)

💡 Dica min-height: 100vh em um celular pode ficar mais alto do que a tela visível, porque a barra de endereço do navegador entra na conta. A unidade dvh (dynamic viewport height) resolve: min-height: 100dvh acompanha a barra aparecendo e sumindo. Funciona em todos os navegadores atuais.

A grade que dispensa media query

Na Aula 07 você escreveu esta linha para os cartões de programação e de palestrantes:

site-evento/css/estilo.css (seção 4 — layout)

CSS
.cartoes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--espaco-medio);
}

Hoje vale fazer a conta do que ela faz em cada largura. O contêiner tem padding-inline de 16 px de cada lado; a largura útil é a largura da tela menos 32 px.

Largura da tela Largura útil Colunas de no mínimo 280 px que cabem
360 px 328 px 1 (com 328 px)
768 px 736 px 2 (com 360 px cada, descontado o gap)
1024 px 992 px 3 (com 320 px cada)
1440 px 1100 px (limite do .container) 3 (com 356 px cada)

O navegador cria quantas colunas de 280 px couberem, distribui o que sobrar entre elas e reorganiza tudo a cada mudança de largura. Nenhuma media query, nenhum breakpoint escolhido por você — o breakpoint nasce do conteúdo, que é exatamente onde ele deve nascer.

📌 Vale gravar auto-fit e auto-fill fazem a mesma conta de quantas colunas cabem. A diferença aparece quando há menos itens do que colunas: auto-fill mantém as colunas vazias (os itens ficam estreitos, com espaço sobrando à direita); auto-fit colapsa as vazias e deixa os itens existentes crescerem. Para grades de cartões, você quase sempre quer auto-fit.

⚠️ Atenção minmax(280px, 1fr) tem um ponto cego: em um contêiner com menos de 280 px de largura (um celular pequeno com padding generoso, ou a lateral estreita da programação), a coluna mínima é maior que o espaço e a grade estoura. A correção é minmax(min(280px, 100%), 1fr): o mínimo passa a ser "280 px ou a largura toda, o que for menor". Você vai aplicar isso na Mão na massa.

3. Media queries e mobile first

Anatomia de uma media query

site-evento/css/estilo.css (seção 7 — media queries)

CSS
@media (min-width: 768px) {
  .grade {
    grid-template-columns: repeat(2, 1fr);
  }
}

Leia como uma pergunta: "a viewport tem pelo menos 768 px de largura?". Se a resposta for sim, as regras dentro das chaves valem; se for não, é como se não existissem. Dentro do bloco você escreve CSS comum — seletores, propriedades, valores — nada muda.

🔎 Por baixo do capô Uma media query não aumenta a especificidade de nada. .grade dentro de @media tem exatamente o mesmo peso de .grade fora. O que decide quem vence, quando a query está ativa, é a regra da cascata que você aprendeu na Aula 06: entre seletores de mesmo peso, a última declarada ganha. Por isso as media queries ficam depois das regras base no arquivo — se você escrever a query antes, a regra base a sobrescreve e parece que "a media query não funciona". O navegador reavalia todas as queries a cada mudança de largura, e cada mudança de resultado dispara um reflow.

Mobile first na prática

Mobile first é uma ordem de escrita: o CSS sem nenhuma media query é o CSS do celular; cada media query de min-width acrescenta o que muda quando a tela cresce.

site-evento/css/estilo.css (exemplo completo)

CSS
/* Base — celular: uma coluna, sem query nenhuma */
.grade {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

/* Tablet — a partir de 768px: duas colunas */
@media (min-width: 768px) {
  .grade {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Notebook — a partir de 1024px: três colunas */
@media (min-width: 1024px) {
  .grade {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* Monitor grande — a partir de 1440px: quatro colunas */
@media (min-width: 1440px) {
  .grade {
    grid-template-columns: repeat(4, 1fr);
  }
}

Em um monitor de 1600 px as quatro queries estão ativas ao mesmo tempo. As três primeiras definem grid-template-columns, e a última vence porque é a última. É o efeito cascata a seu favor: cada query só precisa declarar o que muda naquele tamanho.

Por que mobile first

  1. É onde está o público. Mais da metade dos acessos a sites no Brasil vem de celulares. Um site que começa pelo monitor e "adapta" para o celular está otimizando para a minoria.
  2. Obriga a priorizar. Em 360 px não cabe tudo. Escrever primeiro para a tela pequena força a decidir o que é essencial — e o que é essencial no celular continua essencial no monitor.
  3. Gera CSS aditivo. Queries de min-width acrescentam; queries de max-width precisam desfazer regras já aplicadas. Compare o exemplo acima com a versão desktop first a seguir.
  4. O celular baixa menos CSS aplicável. O aparelho mais fraco, na rede mais lenta, processa só a base. As queries de telas grandes ficam ali, mas não alteram nada.

Desktop first — a mesma grade, escrita ao contrário

CSS
/* Base — monitor grande: quatro colunas */
.grade {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}

/* Até 1439px: desfaz para três */
@media (max-width: 1439px) {
  .grade {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* Até 1023px: desfaz para duas */
@media (max-width: 1023px) {
  .grade {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Até 767px: desfaz para uma */
@media (max-width: 767px) {
  .grade {
    grid-template-columns: 1fr;
  }
}

Funciona, mas repare nos limites "menos um" (1439px, 1023px, 767px) — um convite a erro de conta — e na lógica invertida: a base é a tela que menos gente usa, e o celular é o último caso tratado. Nesta trilha você escreve sempre mobile first.

📌 Vale gravar Uma pergunta clássica: "no CSS mobile first, o que acontece se o navegador não suportar media queries?". Resposta: ele mostra a versão de celular, que é a base e funciona em qualquer largura. No desktop first, o mesmo navegador mostraria as quatro colunas espremidas em 360 px.

Breakpoints: onde o layout quebra

Um breakpoint é a largura em que uma media query entra em ação. A pergunta que todo iniciante faz é "quais são os breakpoints certos?", e a resposta honesta é: os que o seu layout pedir.

A tabela abaixo dá as faixas típicas, para você ter uma referência de vocabulário:

Faixa Dispositivo típico
até 480 px Celular pequeno
481–767 px Celular grande
768–1023 px Tablet
1024–1439 px Notebook / desktop
1440 px ou mais Monitor grande

💡 Dica Não decore breakpoints de aparelhos. O método é: abra o site em uma janela larga, arraste a borda devagar e observe. No instante em que algo quebra — um título vira três linhas feias, os cartões ficam com 200 px, o menu estoura —, olhe o número que o DevTools mostra no canto superior direito. Esse é o seu breakpoint. O design manda no breakpoint, não o iPhone.

Para o site do evento, três pontos bastam e são os que você vai usar hoje: 768 px (o menu cabe em linha e a grade aguenta duas colunas), 1024 px (a lateral de filtros cabe ao lado do conteúdo) e, opcionalmente, 1440 px (mais respiro entre as seções).

Outras condições úteis

Media queries não são só sobre largura. Estas são as que você vai usar com frequência:

CSS
/* Só até 767px (raro em mobile first, mas útil para esconder algo só no celular) */
@media (max-width: 767px) {
  .so-desktop {
    display: none;
  }
}

/* Faixa fechada: só entre 768px e 1023px */
@media (min-width: 768px) and (max-width: 1023px) {
  .lateral {
    display: none;
  }
}

/* Orientação: celular deitado */
@media (orientation: landscape) {
  .hero {
    padding-block: 2rem;
  }
}

/* Impressão: tira o que não faz sentido no papel */
@media print {
  nav,
  footer,
  .sem-impressao {
    display: none;
  }
}

/* Tema escuro escolhido no sistema operacional */
@media (prefers-color-scheme: dark) {
  :root {
    --cor-fundo: #0f1720;
    --cor-texto: #e6edf3;
  }
}

/* Usuário pediu menos movimento */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }
}

As três últimas — impressão, tema escuro e redução de movimento — são preferências do usuário, e a seção 6 volta a elas com calma.

💡 Dica Existe uma sintaxe mais nova, de intervalo: @media (width >= 768px) e @media (768px <= width < 1024px). Ela já funciona em todos os navegadores atuais e evita os limites "menos um". Nesta trilha usamos min-width porque é a forma que você vai encontrar em 99 % do código existente — mas reconheça as duas.

Onde as media queries ficam no arquivo

Na Aula 06 você reservou a seção 7 de estilo.css para elas. Duas escolas convivem no mercado: agrupar todas as queries no fim do arquivo, em ordem crescente de largura, ou manter cada query logo abaixo do componente que ela ajusta. As duas funcionam; o que não pode é misturar. Nesta trilha, para facilitar a leitura e a correção, as queries ficam agrupadas no fim, em ordem crescente: primeiro o bloco de 768px, depois o de 1024px, depois o de 1440px.

4. Imagens e tipografia responsivas

object-fit e aspect-ratio: fotos que não deformam

Os cartões de palestrantes recebem fotos de tamanhos diferentes: uma em 800 × 600, outra em 1200 × 1200, outra em 640 × 960. Com width: 100% cada uma fica com uma altura, e a grade vira um zigue-zague.

site-evento/css/estilo.css (seção 5 — componentes)

CSS
.cartao img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  object-position: center;
  border-radius: var(--raio-borda) var(--raio-borda) 0 0;
}
  • aspect-ratio: 16 / 9 reserva a mesma proporção para todas as imagens, qualquer que seja a largura do cartão.
  • object-fit: cover faz a foto preencher a caixa cortando o que sobra, em vez de esticar (fill, o padrão) ou deixar faixas vazias (contain).
  • object-position escolhe que parte da foto sobrevive ao corte — center para paisagens, top para retratos, onde o rosto fica na parte de cima.

srcset e <picture>: a imagem certa para cada tela

Servir uma foto de 1600 px para um celular de 360 px desperdiça dados e tempo de carregamento. O atributo srcset oferece várias versões e deixa o navegador escolher.

site-evento/index.html (banner da página inicial)

HTML
<img
  src="img/banner-800.jpg"
  srcset="img/banner-400.jpg 400w,
          img/banner-800.jpg 800w,
          img/banner-1600.jpg 1600w"
  sizes="(min-width: 1024px) 1100px, 100vw"
  width="1600"
  height="900"
  alt="Auditório lotado na abertura da Semana Acadêmica de Sistemas de Informação">
  • srcset lista os arquivos e a largura real de cada um (480w = 480 px de largura).
  • sizes diz quanto da tela a imagem vai ocupar: 1100 px em telas a partir de 1024 px, a tela inteira (100vw) abaixo disso. Com essa informação e o devicePixelRatio, o navegador baixa o arquivo adequado.
  • width e height no HTML informam a proporção antes de a imagem carregar. O navegador reserva o espaço e a página não "pula" quando a foto chega — o Lighthouse mede isso como Cumulative Layout Shift, e vai cobrar na seção 7.

Quando a versão de celular precisa ser outra foto (um recorte mais fechado, por exemplo), use <picture>:

HTML
<picture>
  <source media="(min-width: 768px)" srcset="img/banner-largo.jpg">
  <img src="img/banner-quadrado.jpg" width="800" height="800"
       alt="Auditório lotado na abertura da Semana Acadêmica de Sistemas de Informação">
</picture>

O <source> com media funciona como uma media query dentro do HTML; o <img> no fim é obrigatório e serve de reserva.

Tipografia fluida com clamp()

Um <h1> de 3 rem fica ótimo no monitor e ocupa três linhas no celular. Antes de clamp(), a solução era uma media query por tamanho de fonte. Hoje é uma linha:

site-evento/css/estilo.css (seção 3 — base)

CSS
h1 {
  font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem);
}

h2 {
  font-size: clamp(1.375rem, 1.1rem + 1.25vw, 2rem);
}

body {
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.6;
}

p,
li {
  max-width: 65ch;
}

clamp(mínimo, preferido, máximo) devolve o valor preferido, mas nunca abaixo do mínimo nem acima do máximo. No <h1>:

  • em 360 px, 1.2rem + 2.5vw dá 1.2 × 16 + 9 = 28,2 px — abaixo do mínimo de 28 px? Não: 28,2 px é ligeiramente acima, então esse é o valor usado;
  • em 1024 px, dá 19,2 + 25,6 = 44,8 px, entre os limites;
  • em 1920 px, daria 67,2 px, mas o máximo de 3 rem (48 px) segura.

⚠️ Atenção O termo preferido mistura rem com vw de propósito. Se fosse só 2.5vw, a fonte deixaria de responder ao zoom do navegador (Ctrl++), porque vw depende da largura da janela e não do tamanho da fonte — e isso viola o critério 1.4.4 da WCAG, que exige que o texto possa crescer até 200 %. O rem no cálculo devolve o controle ao usuário.

💡 Dica No celular, nunca deixe texto corrido nem campos de formulário abaixo de 16 px. Além de ficar difícil de ler, o Safari do iPhone dá zoom automático em qualquer <input> com fonte menor que 16 px quando ele recebe foco, e o layout inteiro "salta".

5. O menu hambúrguer — acessível de verdade

Na Aula 07 o menu era uma lista horizontal com cinco links, e em 360 px ela não cabe. A solução clássica é esconder a lista atrás de um botão com três barrinhas — o "hambúrguer" — que só aparece em telas pequenas.

Por que <button>, e não <div> ou <a>

Você vai ver por aí menus abertos por <div class="hamburguer"> ou por <a href="#">. Os dois estão errados, e a razão é acessibilidade:

  • Um <div> não recebe foco por Tab, não reage a Enter nem a Espaço e é anunciado pelo leitor de tela como "texto", não como algo clicável.
  • Um <a href="#"> é focável, mas semanticamente é um link: o leitor de tela anuncia "link", o usuário espera navegar para outro lugar, e o # faz a página rolar para o topo.
  • Um <button> é focável, reage às duas teclas, é anunciado como "botão" e aceita dois atributos que contam o resto da história: aria-expanded (está aberto ou fechado?) e aria-controls (controla qual elemento?).

A marcação

Este é o bloco final do cabeçalho. Ele substitui o <nav> da Aula 07 nas cinco páginas:

site-evento/index.html (dentro de .cabecalho__interno, depois do logo)

HTML
<nav aria-label="Principal">
  <button class="menu-botao" aria-expanded="false" aria-controls="menu-principal">
    <span class="menu-botao__icone" aria-hidden="true"></span>
    Menu
  </button>
  <ul id="menu-principal" class="menu">
    <li><a href="index.html" aria-current="page">Início</a></li>
    <li><a href="programacao.html">Programação</a></li>
    <li><a href="palestrantes.html">Palestrantes</a></li>
    <li><a href="contato.html">Contato</a></li>
    <li><a href="inscricao.html" class="menu__cta">Inscreva-se</a></li>
  </ul>
</nav>

Três detalhes:

  1. O <span> do ícone tem aria-hidden="true" porque é decorativo — o leitor de tela lê a palavra "Menu", que está ali como texto de verdade.
  2. aria-controls="menu-principal" aponta para o id da lista. Alguns leitores de tela oferecem um atalho para "pular para o elemento controlado".
  3. aria-expanded="false" é o estado. É esse atributo que o CSS vai observar para mostrar ou esconder a lista — e é o único que o JavaScript vai trocar.

O CSS: base para o celular, a query acrescenta o desktop

site-evento/css/estilo.css (seção 5 — componentes; substitui o .menu da Aula 07)

CSS
/* Cabeçalho: logo à esquerda, botão à direita (celular) */
.cabecalho__interno {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--espaco-medio);
  padding-block: var(--espaco-medio);
}

/* Botão hambúrguer */
.menu-botao {
  display: inline-flex;
  align-items: center;
  gap: var(--espaco-pequeno);
  padding: var(--espaco-pequeno) var(--espaco-medio);
  background: transparent;
  border: 1px solid var(--cor-borda);
  border-radius: var(--raio-borda);
  color: var(--cor-texto);
  font: inherit;
  cursor: pointer;
}

.menu-botao:focus-visible {
  outline: 3px solid var(--cor-secundaria);
  outline-offset: 2px;
}

/* As três barrinhas: uma no span, duas nos pseudoelementos */
.menu-botao__icone {
  position: relative;
  display: block;
  width: 20px;
  height: 2px;
  background: currentColor;
}

.menu-botao__icone::before,
.menu-botao__icone::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
}

.menu-botao__icone::before {
  top: -6px;
}

.menu-botao__icone::after {
  top: 6px;
}

/* Lista: fechada por padrão, cai abaixo do cabeçalho quando aberta */
.menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  flex-direction: column;
  gap: 0;
  margin: 0;
  padding: var(--espaco-pequeno) var(--espaco-medio);
  list-style: none;
  background: var(--cor-superficie);
  border-bottom: 1px solid var(--cor-borda);
  box-shadow: var(--sombra-cartao);
}

/* O atributo que o JavaScript troca é o que abre a lista */
.menu-botao[aria-expanded="true"] + .menu {
  display: flex;
}

.menu a {
  display: block;
  padding: var(--espaco-medio) 0;
  border-bottom: 1px solid var(--cor-borda);
}

.menu li:last-child a {
  border-bottom: 0;
}

E, na seção 7, a query que transforma o mesmo HTML no menu horizontal da Aula 07:

site-evento/css/estilo.css (seção 7 — media queries)

CSS
@media (min-width: 768px) {
  .menu-botao {
    display: none;
  }

  .menu {
    display: flex;
    position: static;
    flex-direction: row;
    align-items: center;
    gap: var(--espaco-grande);
    padding: 0;
    background: transparent;
    border-bottom: 0;
    box-shadow: none;
  }

  .menu a {
    padding: var(--espaco-pequeno) 0;
    border-bottom: 0;
  }
}

Alguns pontos para entender, não só copiar:

  • A lista aberta usa position: absolute com top: 100%. O ancestral posicionado mais próximo é o .cabecalho, que é sticky — e sticky conta como posicionado, como você viu na Aula 07. A lista cai exatamente abaixo do cabeçalho, por cima do conteúdo.
  • O seletor .menu-botao[aria-expanded="true"] + .menu combina um seletor de atributo com o combinador de irmão adjacente (+), ambos da Aula 06. Ele só casa quando o botão está com aria-expanded="true" e a lista vem logo depois dele — que é o caso do HTML acima.
  • A query de 768 px desfaz mais coisas do que o normal (position, background, box-shadow). É a exceção que confirma a regra do mobile first: o menu de celular e o de desktop são componentes genuinamente diferentes, então não há como um ser só "a base mais um pouco" do outro.
  • O sublinhado animado de .menu a::after da Aula 07 continua valendo nas duas versões.

⚠️ Atenção display: none remove a lista da árvore de acessibilidade — o leitor de tela não a encontra. Aqui isso é correto: o menu está fechado e o usuário não deveria alcançar links invisíveis com Tab. Na Aula 09 você vai animar a abertura, e vai aprender que display não anima; a técnica passa a ser opacity + visibility + transform, que preserva o mesmo comportamento acessível.

Oito linhas de JavaScript — antecipação da Unidade 3

O CSS já sabe abrir e fechar. Falta alguém trocar o atributo quando o botão for clicado. Isso é JavaScript, assunto da Unidade 3 — mas são poucas linhas, e vale entendê-las agora:

site-evento/js/menu.js

JavaScript
// Seleciona o botão e a lista pelo seletor CSS
const botao = document.querySelector('.menu-botao');
const menu = document.querySelector('#menu-principal');

// A cada clique, inverte o valor de aria-expanded
botao.addEventListener('click', () => {
  const aberto = botao.getAttribute('aria-expanded') === 'true';
  botao.setAttribute('aria-expanded', String(!aberto));
});

E a chamada, no <head> das cinco páginas:

site-evento/index.html

HTML
<script src="js/menu.js" defer></script>

O atributo defer faz o navegador baixar o script em paralelo e só executá-lo depois de terminar de ler o HTML — sem ele, document.querySelector('.menu-botao') rodaria antes de o botão existir e devolveria null.

O que importa aqui é a divisão de trabalho: o JavaScript só troca um atributo; o CSS decide o que aparece. Nada de menu.style.display = 'flex' — quem manda na apresentação é a folha de estilo, e o dia em que você quiser animar a abertura vai mexer só nela.

🔎 Por baixo do capô Com o menu fechado, o leitor de tela anuncia algo como "Menu, botão, recolhido". Depois do clique, "Menu, botão, expandido" — e a pessoa sabe que apareceu algo, mesmo sem enxergar a tela. É o aria-expanded que produz essa frase. Um <div> com uma classe .aberto não produz nada.

💡 Dica O menu ainda não fecha com Esc, nem com um toque fora dele, nem devolve o foco ao botão. Essas três coisas são o exercício B3 de hoje — com dicas — e vão ficar naturais na Unidade 3. Faça o básico funcionar primeiro.

6. Preferências do usuário

O sistema operacional guarda escolhas que a pessoa fez uma vez e espera ver respeitadas em todo lugar: tema escuro, menos animações, tamanho de fonte. O CSS lê essas escolhas por media queries — sem JavaScript e sem botão no seu site.

Tema escuro: prefers-color-scheme

Como todas as cores do site vivem em variáveis desde a Aula 06, um tema escuro é redefinir as variáveis dentro de uma media query. Nenhum seletor de componente precisa mudar.

site-evento/css/estilo.css (seção 2 — variáveis; acrescente a variável nova e o bloco)

CSS
:root {
  --cor-primaria: #0b3d5c;
  --cor-secundaria: #1a7fb5;
  --cor-sobre-primaria: #ffffff;
  --cor-texto: #333333;
  --cor-fundo: #f7f9fb;
  --cor-superficie: #ffffff;
  --cor-borda: #dfe6ec;
  --fonte-base: "Inter", Arial, sans-serif;
  --espaco-pequeno: 8px;
  --espaco-medio: 16px;
  --espaco-grande: 32px;
  --raio-borda: 8px;
  --sombra-cartao: 0 2px 8px rgba(0, 0, 0, 0.1);
  --largura-max: 1100px;
  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --cor-primaria: #7ec8e3;
    --cor-secundaria: #a9dcef;
    --cor-sobre-primaria: #0f1720;
    --cor-texto: #e6edf3;
    --cor-fundo: #0f1720;
    --cor-superficie: #172231;
    --cor-borda: #2a3948;
    --sombra-cartao: 0 2px 8px rgba(0, 0, 0, 0.5);
  }
}
  • color-scheme: light dark avisa o navegador que a página suporta os dois temas. Ele passa a desenhar barras de rolagem, campos de formulário e a cor de fundo padrão de acordo com o tema — de graça.
  • A variável nova, --cor-sobre-primaria, existe porque no tema claro a primária é escura e o texto sobre ela é branco; no escuro a primária vira azul-claro e o texto sobre ela precisa ser escuro. Toda cor que "fica em cima" de outra cor precisa de variável própria. Na Mão na massa você troca o color: #fff do .menu__cta da Aula 07 por var(--cor-sobre-primaria).

⚠️ Atenção Tema escuro não é "inverter as cores". Verifique o contraste de novo no WebAIM para cada par texto/fundo do tema escuro — o mínimo continua sendo 4.5:1. Fundos escuros costumam pedir texto um pouco menos branco (#e6edf3 em vez de #ffffff) para não "vibrar", e sombras mais fortes para continuarem visíveis.

Redução de movimento: prefers-reduced-motion

Pessoas com distúrbios vestibulares configuram o sistema para reduzir animações, e o site precisa obedecer. O bloco abaixo desliga transições e animações quando essa preferência está ativa. Ele vai para o fim da folha de estilo, porque precisa vencer tudo:

site-evento/css/estilo.css (última regra do arquivo)

CSS
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Hoje o único movimento do site é o sublinhado do menu; a Aula 09 acrescenta transições, transformações e animações, e volta a este bloco com calma — inclusive para explicar por que é uma das poucas situações em que !important se justifica.

Impressão: @media print

A página de programação vai ser impressa por gente que quer levar a grade de horários no bolso. No papel, cabeçalho fixo, menu, rodapé e link de salto não fazem sentido:

site-evento/css/estilo.css (seção 7 — media queries)

CSS
@media print {
  .cabecalho,
  .rodape,
  .salto,
  .menu-botao {
    display: none;
  }

  body {
    background: #fff;
    color: #000;
    font-size: 12pt;
  }

  .pagina {
    display: block;
  }

  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
  }
}

A última regra imprime a URL ao lado de cada link externo — no papel ninguém clica. O attr(href) lê o atributo do próprio elemento, uma função que você já viu na Aula 06 com ::after.

🔬 Investigue No DevTools, abra o menu de três pontos → More toolsRendering. Role até Emulate CSS media feature prefers-color-scheme e alterne entre light e dark: o site troca de tema sem você tocar no sistema operacional. Logo abaixo, Emulate CSS media feature prefers-reduced-motion e, mais acima, Emulate CSS media typeprint mostra a página como ela sairia na impressora. Deixe esse painel aberto durante a Mão na massa.

7. Testando de verdade

Um site "responsivo" que só foi testado arrastando a janela do Chrome não foi testado. Três ferramentas, em ordem crescente de confiança:

1. Modo dispositivo do DevTools

Ctrl+Shift+M (ou o ícone de celular no canto do DevTools). Escolha um aparelho na lista ou digite a largura à mão. Use o modo Responsive e arraste a borda devagar para achar os breakpoints. Ative ThrottlingSlow 4G para sentir o carregamento em rede ruim. É rápido e ótimo para layout — mas simula: o toque vira clique de mouse, a barra de endereço não existe e as fontes do sistema são as do seu computador.

2. Celular real, na mesma rede

O Live Server serve a pasta do projeto na porta 5500. Do celular, na mesma rede Wi-Fi, acesse o endereço IP do computador nessa porta:

  1. Descubra o IP do computador: ip addr (Linux), ipconfig (Windows) ou ifconfig (macOS). Procure algo como 192.168.0.15.
  2. No navegador do celular, abra http://192.168.0.15:5500/site-evento/ (troque pelo seu IP).
  3. Se não abrir, o Live Server pode estar ouvindo só em 127.0.0.1. Nas configurações do VS Code, defina liveServer.settings.host como 0.0.0.0, reinicie o Live Server e tente de novo. Se ainda não abrir, o firewall do computador está bloqueando a porta 5500.

No aparelho de verdade aparecem os problemas que o simulador esconde: o menu que não abre porque o botão tem 24 px e o dedo tem 40, a fonte que fica pequena demais, o campo de formulário que dá zoom sozinho, a altura de 100vh que fica escondida atrás da barra de endereço. É sobre isso a atividade assíncrona de hoje.

3. Lighthouse

No DevTools, aba Lighthouse → marque MobileAnalyze page load. Ele simula um celular de entrada em rede lenta e devolve notas de 0 a 100 em Performance, Acessibilidade, Boas práticas e SEO, com a lista do que corrigir. Problemas que ele pega e que você já sabe resolver: meta viewport ausente, texto menor que 12 px, imagens sem width/height, contraste abaixo de 4.5:1, alvos de toque menores que 48 × 48 px, user-scalable=no.

💡 Dica Rode o Lighthouse em uma janela anônima. Extensões do navegador (bloqueadores de anúncio, tradutores) injetam scripts na página e derrubam a nota sem culpa sua.

Lista de verificação responsiva

Passe cada página do site por estes itens antes de considerar pronta:

  • [ ] Nenhuma rolagem horizontal em 320 px, 360 px e 768 px.
  • [ ] Todo texto legível sem zoom; nada abaixo de 16 px no corpo.
  • [ ] Imagens não deformam nem estouram o contêiner.
  • [ ] Menu abre e fecha com o dedo, com o mouse e com Enter/Espaço no teclado.
  • [ ] Alvos de toque (links do menu, botões) com pelo menos 44 × 44 px.
  • [ ] Layout continua fazendo sentido com o celular deitado.
  • [ ] Tema escuro do sistema é respeitado e o contraste continua ≥ 4.5:1.
  • [ ] Lighthouse Mobile sem alertas de viewport, fonte pequena ou alvos de toque.

💻 Mão na massa — Site do evento em qualquer tela

Você vai transformar o site-evento/ da Aula 07 em um site mobile first completo. Trabalhe com o Live Server aberto, o DevTools no modo dispositivo em 360 px e uma segunda janela em 1440 px.

Passo 1 — Inventário do que quebra

Antes de corrigir, meça. Abra index.html e programacao.html em 360 px e anote o que você vê. A lista deve parecer com esta:

Sintoma em 360 px Causa provável
Menu com cinco links estoura para a direita .menu é uma linha flex sem quebra
Lateral de filtros ocupa 240 px e esmaga o conteúdo .pagina tem duas colunas fixas
Título do banner em quatro linhas font-size fixo em rem
Rodapé com três colunas de 100 px cada .rodape__grade tem três colunas fixas
Foto de palestrante achatada <img> sem object-fit

Cada linha vira um passo abaixo.

Passo 2 — Reorganizar estilo.css para mobile first

Percorra a seção 4 (layout) do seu estilo.css e separe o que é base do que é só para telas grandes. A regra é simples: se uma declaração só faz sentido com espaço sobrando (duas ou mais colunas, gap grande, padding generoso), ela vai para uma media query na seção 7. O restante fica como base.

Ao fim da reorganização, a seção 7 do arquivo tem esta estrutura, ainda quase vazia:

site-evento/css/estilo.css (seção 7 — media queries)

CSS
/* 7. Media queries — mobile first, em ordem crescente de largura */

@media (min-width: 768px) {
  /* tablet: menu em linha, rodapé em duas colunas */
}

@media (min-width: 1024px) {
  /* notebook: lateral ao lado do conteúdo, rodapé em três colunas */
}

@media (min-width: 1440px) {
  /* monitor grande: mais respiro */
}

@media print {
  /* impressão */
}

Os comentários vão sendo substituídos por regras nos passos seguintes. O bloco prefers-reduced-motion entra no passo 9, depois de tudo isso.

Passo 3 — Contêiner, mídia e cartões

Confirme que a seção 3 (base) tem a regra de mídia e que a seção 4 tem o contêiner fluido; se não tiver, acrescente:

site-evento/css/estilo.css (seções 3 e 4)

CSS
/* 3. Base */
img,
video {
  max-width: 100%;
  height: auto;
}

/* 4. Layout */
.container {
  width: 100%;
  max-width: var(--largura-max);
  margin-inline: auto;
  padding-inline: var(--espaco-medio);
}

.cartoes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: var(--espaco-medio);
}

E, na seção 5 (componentes), a foto do cartão com proporção fixa. Antes de colar, apague a regra .cartao__foto da Aula 07 (a que fixava width: 120px, height: 120px, border-radius: 50% e object-fit: cover): .cartao img tem especificidade 0-1-1 e venceria .cartao__foto (0-1-0) de qualquer jeito, e duas regras disputando a mesma foto é exatamente o tipo de lixo que faz uma folha de estilo parar de ser previsível. Você pode manter a classe cartao__foto no HTML ou removê-la; o que não pode é deixar a regra órfã no CSS.

site-evento/css/estilo.css (seção 5)

CSS
.cartao img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  object-position: center;
  border-radius: var(--raio-borda) var(--raio-borda) 0 0;
}

/* A foto do palestrante: quadrada, redonda e com o rosto preservado */
.cartao--palestrante img {
  width: 120px;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  object-position: top;
  align-self: center;
}

Os cartões de palestrantes.html já têm a classe cartao--palestrante ao lado de cartao desde a Aula 07 — confira que ela está lá nos seis, porque é ela que devolve a foto redonda de 120 px que a regra genérica acabou de sobrescrever.

Como verificar: em 360 px a grade de programação mostra um cartão por linha; em 768 px, dois; em 1100 px ou mais, três. Nenhuma media query foi escrita para isso.

Passo 4 — Página de programação: uma coluna, depois duas

A .pagina da Aula 07 tinha grid-template-columns: 240px 1fr como base. Inverta: a base é uma coluna, e a lateral só vai para o lado a partir de 1024 px.

site-evento/css/estilo.css (seção 4)

CSS
.pagina {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas:
    "lateral"
    "conteudo";
  gap: var(--espaco-grande);
  padding-block: var(--espaco-grande);
}

.lateral {
  grid-area: lateral;
}

.conteudo {
  grid-area: conteudo;
}

site-evento/css/estilo.css (seção 7 — dentro do bloco de 1024px)

CSS
@media (min-width: 1024px) {
  .pagina {
    grid-template-columns: 240px 1fr;
    grid-template-areas: "lateral conteudo";
  }
}

No celular a lateral de filtros vem antes da lista — ela é curta e é útil filtrar antes de rolar. Se a lateral fosse longa (patrocinadores, links, avisos), bastaria trocar a ordem em grid-template-areas da base para "conteudo" "lateral" — sem tocar no HTML, que continua com <aside> antes de <section>.

Como verificar: em 360 px os filtros aparecem em cima e os cartões embaixo, ambos com a largura toda; a partir de 1024 px a lateral gruda à esquerda com 240 px.

Passo 5 — Menu hambúrguer

Substitua o <nav> das cinco páginas pela marcação da seção 5, mantendo aria-current="page" no link da página correta em cada arquivo. Depois:

  1. Na seção 5 de estilo.css, substitua o .menu da Aula 07 pelo CSS de celular da seção 5 desta aula (.menu-botao, .menu-botao__icone, .menu com display: none, o seletor .menu-botao[aria-expanded="true"] + .menu).
  2. Na seção 7, dentro do bloco de 768 px, cole a query que esconde o botão e coloca o menu em linha.
  3. Crie a pasta js/ e o arquivo js/menu.js com as oito linhas da seção 5.
  4. Inclua <script src="js/menu.js" defer></script> no <head> das cinco páginas, logo depois do <link> do CSS.
  5. Troque, no .menu__cta, color: #fff por color: var(--cor-sobre-primaria) — a variável entra no passo 8.

site-evento/index.html (<head> completo, como fica)

HTML
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Semana Acadêmica de Sistemas de Informação: palestras, minicursos e maratona de programação.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Início — Semana Acadêmica de Sistemas de Informação</title>
  <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@400;600;700&display=swap"
        rel="stylesheet">
  <link rel="stylesheet" href="css/estilo.css">
  <script src="js/menu.js" defer></script>
</head>

Os três <link> da fonte Inter e o <meta name="author"> vêm das Aulas 02 e 06 — eles continuam aqui. Quem copiar este bloco por cima do <head> antigo sem eles perde a tipografia do projeto inteiro.

Como verificar: em 360 px aparece o botão "Menu"; um clique abre a lista abaixo do cabeçalho, outro fecha. No DevTools, aba Elements, observe o atributo aria-expanded mudando a cada clique. Com Tab o botão recebe o anel de foco e Enter abre o menu. A partir de 768 px o botão some e os cinco links ficam em linha, como na Aula 07.

Passo 6 — Tipografia fluida

Na seção 3 (base), substitua os font-size fixos de h1, h2 e body pelos clamp() da seção 4 desta aula, e limite a largura do texto corrido:

site-evento/css/estilo.css (seção 3)

CSS
body {
  font-family: var(--fonte-base);
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.6;
  color: var(--cor-texto);
  background-color: var(--cor-fundo);
}

h1 {
  font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem);
  line-height: 1.15;
}

h2 {
  font-size: clamp(1.375rem, 1.1rem + 1.25vw, 2rem);
  line-height: 1.25;
}

p,
li,
dd {
  max-width: 65ch;
}

Como verificar: arraste a janela de 360 px a 1440 px e veja o título crescer de forma contínua, sem saltos. Dê zoom com Ctrl++ até 200 %: o texto cresce junto.

Passo 7 — Banner da página inicial

Na index.html, a seção de abertura ganha uma classe e uma altura que acompanha a tela:

site-evento/index.html (primeiro bloco dentro de <main>)

HTML
<section class="hero">
  <div class="container">
    <h1>Semana Acadêmica de Sistemas de Informação</h1>
    <p>Três dias de palestras, minicursos e maratona de programação.</p>
    <a href="inscricao.html" class="botao">Inscreva-se</a>
  </div>
</section>

site-evento/css/estilo.css (seção 5) — substitua o bloco .hero da Aula 07

⚠️ Atenção O herói muda de forma: sai o Grid de duas colunas com texto de um lado e <img> do outro (Aula 07, Passo 5) e entra um banner de largura total com a foto como imagem de fundo. Apague as três regras antigas — .hero, .hero img e .hero__acoes — antes de colar as de baixo, e apague também o <div class="hero__texto">, a <img> e o segundo botão da marcação. Se as duas versões coexistirem, o display: grid da antiga briga com o text-align: center da nova e o resultado não é nem um nem outro.

CSS
.hero {
  background: var(--cor-primaria) url("../img/banner.jpg") center / cover no-repeat;
  color: var(--cor-sobre-primaria);
  padding-block: clamp(3rem, 10vw, 8rem);
  text-align: center;
}

.hero h1 {
  color: inherit;
}

.hero p {
  margin-inline: auto;
}

O padding-block com clamp() faz o banner ter 48 px de respiro no celular e até 128 px no monitor. O center / cover posiciona e recorta a foto de fundo do mesmo jeito que object-fit: cover faz em um <img>. Na Aula 09 esse banner ganha um gradiente por cima da foto para garantir o contraste do texto.

Passo 8 — Tema escuro

Na seção 2 (variáveis), acrescente --cor-sobre-primaria: #ffffff; e color-scheme: light dark; ao :root, e logo abaixo o bloco @media (prefers-color-scheme: dark) da seção 6 desta aula, com os valores escuros de todas as variáveis de cor.

Depois, procure no arquivo inteiro por cores escritas "na mão" (#fff, #333, white) e troque cada uma pela variável correspondente. Duas que certamente existem desde a Aula 07: o color: #fff do .menu__cta (vira var(--cor-sobre-primaria)) e a cor de fundo do .cabecalho (deve ser var(--cor-superficie)).

Como verificar: com o painel Rendering do DevTools em prefers-color-scheme: dark, o site inteiro troca de tema; nenhum texto some, nenhum botão fica branco sobre branco. Confira o contraste de --cor-texto sobre --cor-fundo e de --cor-sobre-primaria sobre --cor-primaria no WebAIM.

Passo 9 — Redução de movimento

Cole o bloco @media (prefers-reduced-motion: reduce) da seção 6 como última regra do arquivo, depois de todas as outras media queries.

Como verificar: no painel Rendering, ative prefers-reduced-motion: reduce. Passe o mouse sobre um link do menu: o sublinhado aparece de imediato, sem deslizar.

Passo 10 — Rodapé: uma, duas, três colunas

site-evento/css/estilo.css (seção 4 — base)

CSS
.rodape {
  background: var(--cor-primaria);
  color: var(--cor-sobre-primaria);
  padding-block: var(--espaco-grande);
}

.rodape__grade {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--espaco-grande);
}

.rodape a {
  color: inherit;
}

site-evento/css/estilo.css (seção 7 — blocos de 768px e 1024px)

CSS
@media (min-width: 768px) {
  .rodape__grade {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .rodape__grade {
    grid-template-columns: repeat(3, 1fr);
  }
}

Como verificar: as três colunas do rodapé (sobre, links, contato) empilham em 360 px, ficam duas em 768 px (a terceira desce) e três a partir de 1024 px.

Passo 11 — Impressão da programação

Cole a query @media print da seção 6 no fim da seção 7 (antes do bloco de prefers-reduced-motion). Abra programacao.html, pressione Ctrl+P e olhe a pré-visualização: só a grade de horários, em preto sobre branco, com as URLs dos links externos entre parênteses.

Como testar

Abra cada uma das cinco páginas em três larguras e confira:

Largura O que você deve ver
360 px Botão "Menu" no cabeçalho; uma coluna em tudo (cartões, filtros, rodapé); título do banner em no máximo duas linhas; sem rolagem horizontal
768 px Menu horizontal com cinco links; cartões em duas colunas; rodapé em duas colunas; filtros ainda acima do conteúdo
1440 px Layout completo: lateral à esquerda com 240 px, cartões em três colunas, rodapé em três colunas, contêiner parado em 1100 px

Depois, os três testes da seção 7: modo dispositivo em "Pixel 7" e "iPad", o celular real pelo IP do computador, e o Lighthouse Mobile em index.html — a meta é nenhum alerta de viewport, fonte ou alvo de toque. Guarde a captura do relatório: ela entra no checkpoint.

🧪 Laboratório

Nível A — Fixação

A1. Por que a meta viewport é indispensável para responsividade? Descreva, em duas frases, o que o celular faz com uma página que não a declara.

A2. Qual a diferença entre a abordagem mobile first e desktop first na escrita das media queries? Qual delas usa min-width e qual usa max-width, e por que a primeira gera CSS "aditivo"?

A3. Escreva uma media query que aplique regras apenas entre 768 px e 1023 px de largura.

A4. O que faz img { max-width: 100%; height: auto; } e por que é uma regra praticamente obrigatória? O que aconteceria com a proporção de uma imagem que tem width="800" height="600" no HTML se faltasse o height: auto?

A5. Escreva a meta viewport correta e explique cada um de seus dois valores.

A6. Por que os breakpoints devem ser definidos pelo conteúdo e não por modelos de aparelho? Descreva o método prático para encontrá-los.

A7. O que faz clamp(1rem, 2.5vw, 1.5rem) aplicado a font-size? Calcule o valor resultante em uma tela de 320 px, de 800 px e de 1200 px.

A8. Explique o que prefers-color-scheme permite fazer e escreva um exemplo que troque as cores de fundo e de texto de um site para o tema escuro.

A9. Por que um menu hambúrguer deve ser um <button> com aria-expanded, e não um <div> ou um <a href="#">? Liste três diferenças práticas.

A10. Dada a grade grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) dentro de um contêiner de 1000 px com gap: 20px, quantas colunas o navegador cria e qual a largura de cada uma? E se houver só dois itens na grade — o que muda entre auto-fit e auto-fill?

A11. O CSS abaixo foi escrito mobile first, mas o estudante reclama que "a media query não funciona". Encontre o erro sem rodar o código:

CSS
@media (min-width: 768px) {
  .cartoes {
    grid-template-columns: repeat(2, 1fr);
  }
}

.cartoes {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

Nível B — Aplicação

B1. Crie uma galeria de 12 imagens com CSS Grid usando auto-fit / minmax, gap de 16 px, imagens com aspect-ratio: 4 / 3 e object-fit: cover, e efeito de scale no :hover com transição. Use fotos livres (Unsplash, Pexels) ou o serviço https://picsum.photos/600/450?random=1 mudando o número final.

Resultado esperado: em 360 px uma coluna; em 768 px duas; em 1200 px três ou quatro, sem nenhuma media query. Todas as imagens têm a mesma proporção, nenhuma deforma, e passar o mouse amplia a foto dentro da moldura sem estourar o cartão.

Dica

Para o zoom não vazar da moldura, o elemento que envolve a imagem precisa de overflow: hidden. A transição vai no img (estado normal), não no :hover, senão a volta é abrupta — a Aula 09 explica por quê. Replique o efeito em :focus-within se cada imagem for um link.

B2. Pegue o site institucional que você criou no exercício C1 da Aula 02 (exercicios/aula02/curso/) e torne-o totalmente responsivo, com pelo menos 3 breakpoints. Documente em comentários no CSS por que escolheu cada breakpoint — qual elemento quebrou e em que largura.

Resultado esperado: nenhuma rolagem horizontal entre 320 px e 1920 px; menu, tabela, formulário e imagens legíveis e usáveis no celular; três blocos @media (min-width) em ordem crescente, cada um com um comentário de uma linha justificando a largura.

Dica

Comece pela meta viewport e pela regra de mídia; só então redimensione. A tabela é o elemento que mais resiste: se ela não couber, envolva-a em um <div> com overflow-x: auto como solução mínima — o B4 mostra a solução completa.

B3. Implemente um menu responsivo completo: horizontal acima de 768 px, hambúrguer abaixo. Requisitos: <button> com aria-expanded e aria-controls, abertura animada por transform, fechamento pelo Esc e pelo clique fora, foco preso dentro do menu enquanto aberto e devolvido ao botão ao fechar.

Resultado esperado: o menu abre e fecha pelo botão, por Esc e por um toque fora dele; com o menu aberto, Tab circula só entre o botão e os links do menu; ao fechar, o foco volta ao botão; a abertura desliza em vez de aparecer de repente.

Dica

Este é um exercício-ponte para a Unidade 3; faça uma exigência de cada vez, sobre o menu.js da aula. Fechar com Esc: document.addEventListener('keydown', (evento) => { if (evento.key === 'Escape') fechar(); }). Clique fora: um click no document que só fecha se !nav.contains(evento.target). Foco preso: pegue todos os links do menu com menu.querySelectorAll('a'), e em um keydown de Tab no último link (ou Shift+Tab no primeiro) chame evento.preventDefault() e .focus() no outro extremo. Devolver o foco: botao.focus() dentro de fechar(). Para animar, troque display: none por opacity: 0; visibility: hidden; transform: translateY(-8px) e a Aula 09 explica o resto.

B4. Construa uma tabela responsiva: em telas largas, tabela normal; abaixo de 640 px, cada linha vira um cartão com os rótulos das colunas via data-* e ::before. Mantenha a marcação semântica de tabela. Use a grade de horários da programação do evento (dia, horário, atividade, local).

Resultado esperado: acima de 640 px, uma <table> comum com <thead>; abaixo, cada <tr> aparece como um bloco com borda, e cada célula mostra "Horário: 19h00", "Local: Auditório", com o rótulo em negrito à esquerda do valor.

Dica

Cada <td> recebe data-rotulo="Horário" no HTML. Na media query de max-width: 639px: thead { position: absolute; left: -9999px; } (esconde sem tirar do leitor de tela), tr { display: block; margin-bottom: 1rem; border: 1px solid var(--cor-borda); }, td { display: flex; gap: 1rem; } e td::before { content: attr(data-rotulo); font-weight: 600; min-width: 6rem; }. Não use display: none no <thead> — o leitor de tela perderia os cabeçalhos.

B5. Torne o site do seu projeto autoral totalmente responsivo em três breakpoints e registre capturas de tela em 360 px, 768 px e 1440 px de cada página.

Resultado esperado: para cada uma das cinco páginas, três capturas nomeadas pagina-360.png, pagina-768.png e pagina-1440.png em uma pasta capturas/; nenhuma mostra rolagem horizontal; o menu está aberto em pelo menos uma captura de 360 px.

Dica

No modo dispositivo do DevTools, o menu de três pontos ao lado da largura tem Capture full size screenshot, que salva a página inteira de uma vez na largura escolhida. Faça primeiro o index.html, valide os três tamanhos, e só então replique o padrão nas outras páginas — o CSS é o mesmo.

Nível C — Desafio

C1. Construa portal/index.html + portal/css/portal.css, um portal de notícias acadêmicas responsivo, com:

  • Estrutura (Grid, com áreas nomeadas): cabeçalho, barra lateral, conteúdo principal e rodapé.
  • Cabeçalho (Flexbox): logo à esquerda, menu horizontal à direita, position: sticky; top: 0 com sombra.
  • Conteúdo principal (Grid): um destaque ocupando 2 colunas, seguido de 6 cartões em repeat(auto-fit, minmax(280px, 1fr)). Cada cartão: imagem com object-fit: cover, categoria, título, resumo, data e link.
  • Barra lateral (Flexbox em coluna): caixa de busca, lista de mais lidos, lista de tags.
  • Rodapé (Grid de 4 colunas): sobre, links úteis, contato, redes sociais.
  • Responsividade: até 767 px, uma coluna, lateral depois do conteúdo, menu vira lista vertical; de 768 px a 1023 px, cartões em 2 colunas, lateral ainda abaixo; a partir de 1024 px, layout completo com lateral à esquerda.
  • Requisitos obrigatórios: mobile first; variáveis CSS; gap no lugar de margens entre itens; :focus visível; contraste AA; nenhuma media query para a grade de cartões (use auto-fit / minmax).
Dica

Comece pelo esqueleto de áreas na base: grid-template-areas: "cabecalho" "conteudo" "lateral" "rodape" — a lateral já fica depois do conteúdo no celular sem mexer no HTML. Em 1024 px redesenhe: "cabecalho cabecalho" "lateral conteudo" "rodape rodape" com grid-template-columns: 260px 1fr. O destaque de 2 colunas é grid-column: span 2 dentro da grade de cartões — em 360 px só há uma coluna, e o span 2 estoura; proteja com grid-column: 1 / -1 (da primeira à última linha-guia, quantas houver). O rodapé de 4 colunas segue o mesmo repeat(auto-fit, minmax(200px, 1fr)).

🏆 Desafios

⭐ Grid Garden completo

gridlayoutresponsivo

Você escreveu repeat(auto-fit, minmax()) hoje e talvez ainda não tenha certeza de por que funciona — porque decorar a linha não é o mesmo que enxergar as linhas-guia da grade. O Grid Garden é um jogo de 28 fases em que cada regra CSS rega uma horta; ele obriga a pensar em linhas-guia, span, valores negativos e grid-template-areas sem nenhum atalho.

Critérios de pronto

  • Captura de tela da fase 28 concluída, com o seu nome visível na página (escreva-o em um comentário no editor do jogo antes do print).
  • Um arquivo grid-garden.md com três propriedades ou valores que você não conhecia antes do jogo, cada um com uma linha explicando o que faz.
  • Um exemplo real: aplique grid-column: 1 / -1 em algum elemento do seu projeto autoral (o destaque da página inicial, por exemplo) e explique em um comentário por que -1 é melhor que span 3.
Pistas
  1. O jogo está em https://cssgridgarden.com/#pt-br, em português.
  2. As fases 14 a 18 usam números de linha negativos: -1 é sempre a última linha-guia, não importa quantas colunas a grade tenha.
  3. As fases 22 em diante mostram grid-template e grid-area juntos — releia a seção "Áreas nomeadas" da Aula 07 antes de tentar.

⭐ Caça ao bug: o site que só funciona no monitor de quem escreveu

bugresponsivocssdevtools

Alguém jura que "o site está responsivo, testei no meu monitor". No celular ele aparece minúsculo, com rolagem horizontal, imagem estourando e um menu que nunca vira hambúrguer. Há seis erros entre o HTML e o CSS abaixo. Encontre todos sem "reescrever do zero" — o desafio é diagnosticar, não refazer.

quebrado/index.html (trecho do <head>)

HTML
<head>
  <meta charset="UTF-8">
  <title>Semana Acadêmica</title>
  <link rel="stylesheet" href="css/quebrado.css">
</head>

quebrado/css/quebrado.css

CSS
.container {
  width: 1200px;
  margin: 0 auto;
}

.menu {
  display: flex;
  gap: 2rem;
}

@media (max-width: 768px) {
  .menu {
    flex-direction: column;
  }
}

.cartoes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}

.cartoes img {
  width: 800px;
}

h1 {
  font-size: 64px;
}

Critérios de pronto

  • Um arquivo diagnostico.md com uma tabela Erro | Sintoma que ele causa | Correção, com as seis linhas.
  • O CSS corrigido, mobile first, sem rolagem horizontal em 320 px e com o menu virando coluna no celular.
  • Uma captura do DevTools em 360 px antes e outra depois da correção.
Pistas
  1. Um dos erros não está no CSS. Sem ele, o celular finge ter 980 px e nenhuma query de max-width: 768px dispara.
  2. Duas larguras fixas em pixels empurram o contêiner para além da tela: uma em um bloco, outra em uma imagem.
  3. A media query do menu está escrita desktop first e "funcionaria" — mas pense no que acontece se, em vez de max-width, o projeto inteiro for migrado para min-width: o que precisa virar base e o que precisa ir para a query?
  4. auto-fill com apenas três cartões deixa colunas vazias à direita em telas largas; e 64px num <h1> de 360 px ocupa quatro linhas. clamp() resolve o segundo.
⭐⭐

⭐⭐ Menu hambúrguer sem uma linha de JavaScript

cssresponsivoacessibilidadelayout

O menu.js de hoje tem oito linhas — mas dá para ter zero. Um <input type="checkbox"> escondido, um <label> que faz papel de botão e a pseudoclasse :checked da Aula 06 conseguem abrir e fechar o menu só com CSS. Construa essa versão, faça funcionar, e então responda à pergunta que importa: o que ela perde em relação à versão com <button> e aria-expanded? Descubra testando com o teclado e com um leitor de tela (NVDA no Windows, VoiceOver no macOS, TalkBack no Android).

Critérios de pronto

  • Menu que abre e fecha em 360 px sem nenhum <script>, e vira horizontal a partir de 768 px.
  • O <label> é alcançável por Tab e o menu abre com Espaço (dica: isso exige que o checkbox continue focável — não use display: none nele).
  • Um arquivo comparacao.md com uma tabela Critério | Versão com button | Versão com checkbox cobrindo: o que o leitor de tela anuncia ao focar o controle, se o estado aberto/fechado é anunciado, se Enter abre o menu, e quanto código cada versão tem.
  • Uma conclusão de três linhas: qual versão você usaria no projeto autoral, e por quê.
Pistas
  1. O seletor é o mesmo raciocínio de hoje, trocando o atributo pela pseudoclasse: #abrir-menu:checked ~ .menu { display: flex; } — o combinador ~ (irmão geral) alcança a lista mesmo com o <label> no meio.
  2. Para esconder o checkbox sem tirá-lo do teclado, use a mesma técnica do link de salto da Aula 07: position: absolute; left: -9999px, ou a classe utilitária "visualmente oculto" (clip, width: 1px, height: 1px, overflow: hidden).
  3. O anel de foco precisa aparecer no <label>, não no checkbox invisível: #abrir-menu:focus-visible + label { outline: 3px solid var(--cor-secundaria); }.
  4. No leitor de tela, um checkbox é anunciado como "caixa de seleção, não marcada" — compare com "botão, recolhido" e pense em qual frase ajuda mais quem não vê a tela.

Para ir além: procure o elemento <details>/<summary> do HTML e tente uma terceira versão do menu com ele — sem JavaScript e com semântica de "expansível" de graça.

⭐⭐⭐

⭐⭐⭐ Nota 90 no Lighthouse Mobile

responsivoperformanceacessibilidadedevtoolsprojeto

Rode o Lighthouse Mobile no index.html do seu projeto autoral agora, antes de mexer em qualquer coisa, e guarde o relatório. É comum a primeira nota de Performance ficar abaixo de 60 em um site com fotos grandes, e a de Acessibilidade tropeçar em contraste e alvos de toque. O desafio é chegar a 90 ou mais nas duas — e cada ponto ganho vai ter um motivo técnico que você consegue explicar.

Critérios de pronto

  • Relatório do Lighthouse Mobile "antes" e "depois" salvos como lighthouse-antes.html e lighthouse-depois.html (o botão de três pontos do relatório exporta), com Performance ≥ 90 e Acessibilidade ≥ 90 no "depois", em todas as cinco páginas.
  • Imagens servidas com srcset em pelo menos três larguras, com width e height declarados, e a maior versão com no máximo 200 KB.
  • Tabela de dados responsiva (a técnica do B4) em pelo menos uma página.
  • Tema escuro via prefers-color-scheme com todos os pares de contraste ≥ 4.5:1, verificados no WebAIM e listados em contraste.md.
  • Um melhorias.md com, para cada alerta do "antes", a correção aplicada e a seção da aula que a explica.
  • Isso entra bem no Marco 2 da unidade.
Pistas
  1. Abra o relatório "antes" e clique em cada alerta: o Lighthouse mostra o elemento exato e um link "Learn more" para a explicação oficial — comece pelos itens marcados em vermelho.
  2. Para gerar as três larguras de cada foto sem software pago, o Squoosh (https://squoosh.app) redimensiona e converte para WebP no navegador; guarde a versão JPG como reserva no src.
  3. Fontes do Google carregadas com <link> custam requisições e podem derrubar Performance. Meça antes de decidir: rode o Lighthouse com a Inter do projeto (que já vem com display=swap e preconnect desde a Aula 06) e depois com a font-family trocada por Arial, sans-serif, e registre a diferença em pontos e em milissegundos de LCP. Se a perda for pequena, a identidade visual vale o preço — a decisão é sua, mas com número.
  4. Alvos de toque menores que 48 × 48 px aparecem em "Tap targets": aumente o padding dos links do menu e do rodapé em vez de aumentar a fonte.

🐛 Erros comuns

Sintoma Causa Solução
Site aparece minúsculo no celular, com tudo em 980 px, e nenhuma media query dispara Meta viewport ausente ou digitada errado (name="viewpoint", content sem width=device-width) Copie exatamente <meta name="viewport" content="width=device-width, initial-scale=1.0"> para o <head> de todas as páginas
Rolagem horizontal no celular, mesmo com layout "responsivo" Alguma largura fixa em pixels: width: 1200px em um contêiner, width: 800px em uma imagem, um gap ou padding grande demais para 320 px max-width + width: 100% nos contêineres; img { max-width: 100%; height: auto; }; no DevTools, use * { outline: 1px solid red; } para achar quem estoura
"A media query não funciona" — a regra dentro dela é ignorada A query está antes da regra base com a mesma especificidade, e a base a sobrescreve na cascata Mova as media queries para depois das regras base (seção 7 do arquivo)
Cartões com colunas vazias à direita quando há poucos itens auto-fill em vez de auto-fit auto-fit colapsa as faixas vazias; use auto-fill só quando quiser reservar o espaço
Grade estoura para fora do contêiner em celulares pequenos minmax(280px, 1fr) com contêiner mais estreito que 280 px minmax(min(280px, 100%), 1fr)
Menu hambúrguer não abre ao tocar no celular, mas abre no DevTools O <script> está antes do botão no HTML e sem defer: querySelector devolve null e o addEventListener falha com Cannot read properties of null <script src="js/menu.js" defer></script> no <head>, ou o script no fim do <body>
Menu abre no DevTools, mas o botão não reage a Enter nem aparece no Tab O controle é um <div> ou um <span>, não um <button> Troque por <button> com aria-expanded e aria-controls
Texto ilegível no celular e o Safari dá zoom sozinho ao tocar em um campo font-size abaixo de 16 px no corpo ou nos <input> Corpo em clamp(1rem, 0.95rem + 0.25vw, 1.125rem); campos com font-size: 1rem
Breakpoints "funcionam" no simulador e quebram no aparelho real Larguras escolhidas de uma lista de dispositivos; o aparelho real tem outra largura ou está deitado Defina breakpoints onde o layout quebra, e teste no modo Responsive arrastando a borda
Imagens deformadas nos cartões width e height forçados sem object-fit aspect-ratio + object-fit: cover
Tema escuro deixa botões brancos com texto branco Cores escritas "na mão" (#fff) em vez de variáveis; variável de texto-sobre-primária inexistente Toda cor via variável; crie --cor-sobre-primaria e redefina no bloco dark
Página imprime o menu, o rodapé e um fundo escuro Sem @media print Esconda navegação e rodapé, force fundo branco e texto preto na query de impressão

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (15 min). SILVA, Maurício Samy. Criando sites com HTML, capítulo de design responsivo. MDN: Media queries (https://developer.mozilla.org/pt-BR/docs/Web/CSS/CSS_media_queries) — leia a página de introdução e a de "Usando media queries"; anote a sintaxe de intervalo e as três preferências do usuário que você viu hoje.

Parte 2 — Produção (40 min). No seu projeto autoral:

  1. O exercício B5 completo: o site responsivo em três breakpoints (mobile first, queries agrupadas no fim do estilo.css em ordem crescente), com as capturas de tela de cada página em 360 px, 768 px e 1440 px na pasta capturas/.
  2. O menu responsivo do exercício B3 funcionando em todas as páginas — no mínimo o básico da aula (<button>, aria-expanded, CSS mobile first, menu.js com defer); os fechamentos por Esc e por clique fora valem como extra.
  3. Tema escuro via prefers-color-scheme e o bloco prefers-reduced-motion no fim do arquivo.

Parte 3 — Teste no celular (5 min). Abra o seu site em um celular real pelo IP do computador (ou peça o de outra pessoa). Registre (no fórum da turma, se você está cursando esta trilha em grupo, ou nas suas anotações) um problema que apareceu no aparelho de verdade e não no simulador do DevTools — botão pequeno demais para o dedo, fonte ilegível, 100vh escondido pela barra de endereço, campo que dá zoom sozinho — e como você o resolveu.

Critério de pronto: nenhuma rolagem horizontal em 320 px em nenhuma página; menu hambúrguer abre e fecha por toque, por clique e por Enter no teclado, com aria-expanded mudando no DevTools; capturas das cinco páginas nas três larguras; tema escuro sem nenhum par de contraste abaixo de 4.5:1; Lighthouse Mobile sem alerta de viewport, fonte pequena ou alvo de toque.

Guarde no seu repositório: commit + push (ou a pasta do projeto, se ainda não usa Git).

✅ Checkpoint do projeto

Ao fim desta aula, o repositório do seu projeto autoral deve ter:

  • [ ] Meta viewport correta no <head> das cinco páginas.
  • [ ] css/estilo.css escrito mobile first: regras base para o celular, blocos @media (min-width: 768px), (min-width: 1024px) e, se necessário, (min-width: 1440px) agrupados na seção 7, em ordem crescente.
  • [ ] img, video { max-width: 100%; height: auto; } na seção base e nenhuma largura fixa em pixels em contêineres.
  • [ ] Grade de cartões com repeat(auto-fit, minmax(min(280px, 100%), 1fr)) e sem media query própria.
  • [ ] Fotos dos cartões com aspect-ratio e object-fit: cover; imagem principal com srcset, sizes, width e height.
  • [ ] Tipografia fluida com clamp() em body, h1 e h2; texto corrido limitado a 65ch.
  • [ ] Menu hambúrguer com <button aria-expanded aria-controls>, js/menu.js carregado com defer, e menu horizontal a partir de 768 px.
  • [ ] Layout de duas colunas (lateral + conteúdo) só a partir de 1024 px, via grid-template-areas.
  • [ ] Tema escuro com prefers-color-scheme, variável --cor-sobre-primaria e color-scheme: light dark.
  • [ ] Bloco prefers-reduced-motion como última regra do arquivo.
  • [ ] @media print escondendo cabeçalho, menu e rodapé.
  • [ ] Pasta capturas/ com as quinze imagens (cinco páginas × três larguras).

📚 Para aprofundar

Na próxima aula o site ganha movimento: transições que suavizam o :hover dos cartões e do menu, transformações 2D e 3D, animações com @keyframes — e, principalmente, como medir no painel Performance do DevTools por que animar transform é barato e animar left trava o celular. O bloco prefers-reduced-motion que você colou hoje no fim do arquivo vai fazer todo o sentido.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Por que "responsivo" — e o que a meta viewport tem a ver com issoA meta viewportOs outros dois pilares2. Layouts fluidos: o que se adapta sozinhoO contêiner que respiraMídia nunca estouraUnidades que se adaptamA grade que dispensa media query3. Media queries e mobile firstAnatomia de uma media queryMobile first na práticaPor que mobile firstBreakpoints: onde o layout quebraOutras condições úteisOnde as media queries ficam no arquivo4. Imagens e tipografia responsivasobject-fit e aspect-ratio: fotos que não deformamsrcset e <picture>: a imagem certa para cada telaTipografia fluida com clamp()5. O menu hambúrguer — acessível de verdadePor que <button>, e não <div> ou <a>A marcaçãoO CSS: base para o celular, a query acrescenta o desktopOito linhas de JavaScript — antecipação da Unidade 36. Preferências do usuárioTema escuro: prefers-color-schemeRedução de movimento: prefers-reduced-motionImpressão: @media print7. Testando de verdade1. Modo dispositivo do DevTools2. Celular real, na mesma rede3. LighthouseLista de verificação responsiva💻 Mão na massa — Site do evento em qualquer telaPasso 1 — Inventário do que quebraPasso 2 — Reorganizar estilo.css para mobile firstPasso 3 — Contêiner, mídia e cartõesPasso 4 — Página de programação: uma coluna, depois duasPasso 5 — Menu hambúrguerPasso 6 — Tipografia fluidaPasso 7 — Banner da página inicialPasso 8 — Tema escuroPasso 9 — Redução de movimentoPasso 10 — Rodapé: uma, duas, três colunasPasso 11 — Impressão da programaçãoComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ Grid Garden completo⭐ Caça ao bug: o site que só funciona no monitor de quem escreveu⭐⭐ Menu hambúrguer sem uma linha de JavaScript⭐⭐⭐ Nota 90 no Lighthouse Mobile🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 2 · CSS: estilo, layout e responsividade3 aulas de 50 min + 1 h EAD

Aula 09 — Animações e efeitos em CSS

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Aplicar transições para suavizar mudanças de estado em botões, links, cartões e menus, escolhendo duração e curva adequadas.
  • Usar transformações 2D e 3D (translate, scale, rotate, skew, perspective) para mover, girar e escalar elementos sem afetar o layout dos vizinhos.
  • Criar animações com @keyframes e controlá-las com as propriedades de animation, incluindo animações escalonadas e múltiplas.
  • Explicar o pipeline de renderização (layout → paint → composite) e escolher propriedades baratas (transform e opacity) para animar sem travar a interface.
  • Respeitar prefers-reduced-motion e os critérios de acessibilidade da WCAG sobre movimento, piscadas e pausa.
  • Construir efeitos visuais comuns — sombras, gradientes, filtros, backdrop-filter, efeitos de hover e revelação ao rolar — com feedback também para quem navega por teclado.
  • Medir o desempenho de uma animação no painel Performance do DevTools e provar, com números, que transform é mais barato que left.

📋 Pré-requisitos

  • [ ] Site do evento (site-evento/) com as cinco páginas estilizadas pelo css/estilo.css da Aula 06, layout Grid + Flexbox e menu acessível da Aula 07, e responsivo com menu hambúrguer, tema escuro e js/menu.js da Aula 08.
  • [ ] Seu projeto autoral no mesmo estágio (responsivo em três larguras).
  • [ ] VS Code com Live Server; Chrome ou Firefox com DevTools — hoje você vai usar o painel Performance e a aba Rendering.
  • [ ] Um celular na mesma rede Wi-Fi do computador (para sentir a diferença de desempenho de verdade).

Na aula passada você fez o site do evento se adaptar a qualquer tela: mobile first, três breakpoints, menu hambúrguer com aria-expanded, imagens fluidas e tema escuro com prefers-color-scheme. O site funciona em qualquer lugar — mas tudo nele acontece de repente: o menu aparece num estalo, o botão muda de cor sem aviso, os cartões surgem prontos. Hoje você dá movimento ao site, com transições, transformações e animações que comunicam algo, custam pouco para o navegador e respeitam quem prefere menos movimento. Esta aula fecha a Unidade 2.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Por que animar; transições (as quatro propriedades, curvas, durações, o que pode ser animado); transformações 2D e 3D
2 50 min @keyframes e animation; animações de referência; performance (layout → paint → composite, will-change); prefers-reduced-motion e WCAG
3 50 min Efeitos visuais (sombras, gradientes, filtros, hover, revelação ao rolar); Mão na massa: polindo o site do evento; Laboratório

1. Por que animar

Animação em interface não é enfeite. Ela serve a três funções concretas:

Função Exemplo
Feedback O botão escurece e afunda ao ser pressionado — o clique foi registrado
Continuidade Um painel desliza para dentro — o usuário entende de onde ele veio e para onde vai voltar
Atenção Uma mensagem de erro treme levemente — o olho é direcionado ao campo com problema

Pense no menu hambúrguer da Aula 08. Quando ele aparece de repente, o cérebro precisa de um instante para entender "de onde veio isso?". Quando ele desliza de cima para baixo em 250 ms, a origem é óbvia: saiu do botão. É a mesma quantidade de informação, entregue com menos esforço.

⚠️ Atenção Regra de ouro: se a animação não comunica nada, remova-a. Movimento gratuito cansa, atrasa a interação (o usuário espera a animação acabar para clicar) e prejudica pessoas com sensibilidade vestibular. Toda animação desta aula precisa responder à pergunta "o que isso está dizendo ao usuário?".

🧠 Você sabia? Os "12 princípios da animação" foram formulados por dois animadores da Disney, Frank Thomas e Ollie Johnston, no livro The Illusion of Life — décadas antes de existir CSS. Vários deles migraram direto para as interfaces: ease in / ease out (nada no mundo físico começa nem para de repente), anticipation (um botão que "afunda" antes de abrir um painel) e follow through (o painel que passa um pouquinho do ponto e volta — o efeito de ricochete do cubic-bezier). As diretrizes de movimento do Material Design, da Apple e da Microsoft citam esses princípios explicitamente.

2. Transições

Uma transição interpola automaticamente a mudança de um valor entre dois estados. Você declara os dois estados (normal e :hover, por exemplo) e diz ao navegador "em vez de trocar de repente, leve 200 ms para ir de um ao outro". O navegador calcula todos os valores intermediários, quadro a quadro.

css/estilo.css (trecho de exemplo)

CSS
.botao {
  background-color: var(--cor-primaria);
  transform: translateY(0);
  transition: background-color 200ms ease, transform 200ms ease;
}

.botao:hover {
  background-color: var(--cor-secundaria);
  transform: translateY(-2px);
}

Passe o mouse sobre o botão: a cor muda suavemente e ele sobe 2 px. Tire o mouse: ele desce e volta à cor original, também suavemente — porque a transition está declarada no estado normal, e vale para os dois sentidos.

As quatro propriedades

CSS
.botao {
  transition-property: background-color, transform;   /* o que anima */
  transition-duration: 200ms;                         /* quanto tempo */
  transition-timing-function: ease;                   /* a curva de velocidade */
  transition-delay: 0s;                               /* espera antes de começar */
}

/* Atalho: propriedade  duração  curva  atraso */
.painel {
  transition: transform 300ms ease-out 100ms;
}

O atalho transition aceita várias transições separadas por vírgula, cada uma com sua duração e curva. Na prática você quase sempre usa o atalho.

📌 Vale gravar A transition deve ser declarada no estado base do elemento (.botao), não no estado de interação (.botao:hover). Se ficar no :hover, a entrada é suave e a saída é abrupta — porque ao tirar o mouse a regra :hover deixa de valer, e com ela a transition.

Funções de tempo

A curva define como a velocidade varia ao longo da transição. É o que separa um movimento "mecânico" de um movimento natural.

Valor Comportamento Uso típico
linear Velocidade constante Rotação contínua, barras de progresso
ease Acelera e desacelera (padrão) Uso geral
ease-in Começa devagar Elemento saindo da tela
ease-out Termina devagar Elemento entrando na tela
ease-in-out Devagar nas duas pontas Movimentos longos
cubic-bezier(.34, 1.56, .64, 1) Curva personalizada Efeito de "ricochete" (passa do ponto e volta)
steps(5, end) Saltos discretos Sprites, efeito de digitação

A regra de bolso: o que entra usa ease-out (chega rápido e assenta devagar, como um carro estacionando); o que sai usa ease-in (parte devagar e some rápido). Use o site cubic-bezier.com para desenhar curvas visualmente e comparar duas lado a lado.

Duração adequada

Faixa Percepção
100–150 ms Micro-interações: :hover, foco, mudança de cor
200–300 ms Padrão para a maioria dos casos
300–500 ms Painéis, modais, elementos grandes que percorrem distância
acima de 500 ms Percebido como lento e irritante

Elementos pequenos que se movem pouco precisam de menos tempo; elementos grandes que atravessam a tela precisam de mais. Na dúvida, comece em 200 ms e ajuste olhando.

⚠️ Atenção transition: all é uma armadilha. Ele anima todas as propriedades que mudarem, inclusive as que você não pretendia — e as caras (width, height, margin). Pior: quando você acrescentar CSS meses depois, coisas passam a animar sem você entender por quê. Liste as propriedades explicitamente, sempre.

O que pode e o que não pode ser animado

Só propriedades com valores numericamente interpoláveis transicionam. O navegador precisa conseguir calcular "o valor no meio do caminho" entre o estado inicial e o final.

  • Funcionam: color, background-color, opacity, width, height, transform, box-shadow, border-radius, padding, margin, font-size.
  • Não funcionam: display, font-family, position, flex-direction, grid-template-columns (em alguns navegadores), visibility (tem regra especial, veja abaixo).

Não existe "meio caminho" entre display: none e display: block. Por isso o exemplo abaixo simplesmente não anima:

CSS
/* Errado: display não interpola — o painel aparece de repente */
.painel {
  display: none;
  transition: display 300ms;
}

A solução clássica combina opacity (que interpola) com visibility (que tira o elemento da navegação por teclado e do leitor de tela):

CSS
/* Certo: opacity anima; visibility esconde do teclado e do leitor de tela */
.painel {
  opacity: 0;
  visibility: hidden;
  transition: opacity 300ms ease, visibility 0s 300ms;
}

.painel.ativo {
  opacity: 1;
  visibility: visible;
  transition: opacity 300ms ease, visibility 0s;
}

Repare no truque do visibility. Ao esconder, visibility 0s 300ms espera os 300 ms do fade terminar e só então vira hidden — senão o painel sumiria instantaneamente antes de desvanecer. Ao mostrar, visibility 0s (sem atraso) torna o elemento visível na hora, e o opacity faz o fade de entrada.

🔎 Por baixo do capô Por que opacity: 0 sozinho não basta? Porque um elemento transparente continua no fluxo e continua focável: quem navega por Tab cai em links invisíveis, e o leitor de tela lê o conteúdo de um painel que "não está lá". visibility: hidden resolve os dois problemas. Navegadores recentes começam a aceitar transition-behavior: allow-discrete junto com @starting-style para animar até display, mas a dupla opacity + visibility funciona em todos e é o que você vai usar nesta trilha.

3. Transformações

transform altera a renderização do elemento sem afetar o layout dos vizinhos. Um elemento com transform: translateX(50px) é desenhado 50 px à direita, mas para o resto da página ele continua exatamente onde estava — ninguém é empurrado, nada é recalculado. É por isso que transform é barato (a §6 mostra o quanto).

CSS
/* Translação: move sem tirar do fluxo */
.a { transform: translateX(20px); }
.b { transform: translateY(-10px); }
.c { transform: translate(20px, -10px); }

/* Escala: 1 = tamanho original */
.d { transform: scale(1.05); }          /* 5% maior nos dois eixos */
.e { transform: scaleX(2) scaleY(0.5); } /* estica na horizontal, achata na vertical */

/* Rotação: graus ou voltas */
.f { transform: rotate(45deg); }
.g { transform: rotate(-0.25turn); }    /* um quarto de volta no sentido anti-horário */

/* Inclinação */
.h { transform: skewX(15deg); }

/* Combinadas — a ORDEM importa */
.i { transform: translateX(50px) rotate(45deg); }  /* move, depois gira no lugar */
.j { transform: rotate(45deg) translateX(50px); }  /* gira o eixo, depois move ao longo do eixo girado */

Sobre a ordem: as funções são aplicadas da esquerda para a direita, sobre o sistema de coordenadas do elemento. Em .i, o elemento desliza 50 px para a direita e então gira em torno do próprio centro. Em .j, o elemento primeiro gira — e com ele giram os eixos X e Y — e só então anda 50 px "para a direita", que agora aponta para a diagonal. Os dois terminam em lugares diferentes. Teste no DevTools trocando a ordem e veja.

Ponto de origem

Toda transformação acontece em torno de um ponto. O padrão é o centro do elemento; transform-origin muda isso.

CSS
.padrao   { transform-origin: center; }     /* padrão: gira/escala em torno do centro */
.canto    { transform-origin: top left; }   /* gira em torno do canto superior esquerdo */
.base     { transform-origin: 50% 100%; }   /* meio da borda inferior — um pêndulo */

Um ponteiro de relógio gira em torno da base, não do centro; um menu suspenso "cresce" a partir do topo; o sublinhado do menu do site do evento cresce a partir da esquerda. Sempre que a origem padrão parecer estranha, é transform-origin que resolve.

Transformações 3D

Com perspective no contêiner pai, o navegador passa a projetar as transformações 3D dos filhos com profundidade. O exemplo clássico é o cartão que vira para mostrar o verso — a estrutura tem três camadas: o palco (com a perspectiva), o virador (que gira) e as duas faces.

exemplos/cartao-3d.html

HTML
<div class="virador">
  <div class="virador__faces">
    <div class="virador__face virador__frente">
      <img src="img/palestrante-ana.jpg" alt="">
      <h3>Ana Souza</h3>
    </div>
    <div class="virador__face virador__verso">
      <p>Engenheira de software, trabalha com sistemas distribuídos e ensina Go.</p>
      <a href="palestrantes.html#ana">Ver palestra</a>
    </div>
  </div>
</div>

exemplos/cartao-3d.css

CSS
.virador {
  perspective: 1000px;            /* no CONTÊINER pai — nunca no elemento que gira */
  width: 280px;
  height: 360px;
}

.virador__faces {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;   /* os filhos continuam em 3D, e não achatados */
  transition: transform 600ms ease;
}

.virador__face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;    /* esconde a face que está de costas para você */
  border-radius: var(--raio-borda);
  background: var(--cor-superficie);
  box-shadow: var(--sombra-cartao);
}

.virador__verso {
  transform: rotateY(180deg);     /* começa virado para trás */
}

.virador:hover .virador__faces,
.virador:focus-within .virador__faces {
  transform: rotateY(180deg);
}

Quanto menor o valor de perspective, mais "perto" está o observador e mais exagerada fica a distorção. Valores entre 800 px e 1200 px parecem naturais. O :focus-within faz o cartão virar também quando o link do verso recebe foco por teclado — sem isso, quem não usa mouse jamais veria o verso.

4. Animações com @keyframes

Transições precisam de uma mudança de estado (hover, classe adicionada, foco). Animações rodam sozinhas, podem ter vários passos intermediários e podem se repetir.

Uma animação tem duas partes: a definição dos quadros-chave (@keyframes) e a aplicação a um elemento (animation).

CSS
@keyframes pulsar {
  0%   { transform: scale(1);    opacity: 1; }
  50%  { transform: scale(1.08); opacity: 0.85; }
  100% { transform: scale(1);    opacity: 1; }
}

.alerta {
  animation: pulsar 1.5s ease-in-out infinite;
}

O navegador interpola entre cada par de quadros consecutivos: de 0 % a 50 % o elemento cresce e clareia; de 50 % a 100 % volta. Com infinite, recomeça para sempre.

Quando há só dois passos, from e to são mais legíveis:

CSS
@keyframes surgir {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

As propriedades de animation

CSS
.cartao {
  animation-name: surgir;
  animation-duration: 400ms;
  animation-timing-function: ease-out;
  animation-delay: 100ms;
  animation-iteration-count: 1;        /* ou infinite, ou um número */
  animation-direction: normal;         /* reverse | alternate | alternate-reverse */
  animation-fill-mode: forwards;       /* none | forwards | backwards | both */
  animation-play-state: running;       /* paused */
}

/* Atalho: nome duração curva atraso repetições direção preenchimento */
.cartao {
  animation: surgir 400ms ease-out 100ms 1 normal forwards;
}

Um detalhe do atalho: quando há dois valores de tempo, o primeiro é a duração e o segundo é o atraso. animation: surgir 400ms 100ms significa "dura 400 ms, começa depois de 100 ms".

⚠️ Atenção animation-fill-mode é o que quase todo mundo esquece. Sem ele, quando a animação termina o elemento volta ao estado original — o cartão que surgiu do nada desaparece de novo. Com forwards, o último quadro permanece aplicado. Com backwards, o primeiro quadro é aplicado durante o animation-delay (útil em animações escalonadas: o elemento fica invisível enquanto espera a sua vez). both faz as duas coisas.

animation-direction: alternate inverte o sentido a cada repetição (vai e volta, como um pêndulo), o que evita o "salto" no fim de cada ciclo de uma animação infinite. reverse roda a animação de trás para frente, sempre.

Animações escalonadas

Cartões que surgem um após o outro, em cascata, parecem mais vivos que todos surgindo ao mesmo tempo. A forma ingênua é um atraso por posição:

CSS
.cartao { animation: surgir 400ms ease-out backwards; }
.cartao:nth-child(1) { animation-delay: 0ms; }
.cartao:nth-child(2) { animation-delay: 80ms; }
.cartao:nth-child(3) { animation-delay: 160ms; }
.cartao:nth-child(4) { animation-delay: 240ms; }

Funciona, mas não escala: com 12 cartões são 12 regras. Com uma variável CSS por elemento, o mesmo efeito fica genérico:

CSS
.cartao {
  animation: surgir 400ms ease-out backwards;
  animation-delay: calc(var(--i, 0) * 80ms);
}
HTML
<li class="cartao" style="--i: 0">Primeiro cartão</li>
<li class="cartao" style="--i: 1">Segundo cartão</li>
<li class="cartao" style="--i: 2">Terceiro cartão</li>

O var(--i, 0) tem um valor de reserva: se algum cartão vier sem --i, ele anima sem atraso, em vez de quebrar. Repare no backwards: sem ele, os cartões que ainda estão esperando o atraso ficariam visíveis no estado final e só "piscariam" quando a animação começasse.

Múltiplas animações

Um elemento pode rodar várias animações ao mesmo tempo, separadas por vírgula. Cada uma tem sua duração, curva, atraso e repetição:

CSS
.balao {
  animation:
    surgir 400ms ease-out,
    flutuar 3s ease-in-out 400ms infinite;
}

O balão surge em 400 ms e, quando termina de surgir, passa a flutuar para sempre. Cuidado: se duas animações mexem na mesma propriedade (aqui, as duas usam transform), a última declarada vence enquanto estiver rodando.

🔎 Por baixo do capô Quando você aplica animation a um elemento, o navegador cria uma linha do tempo interna para ele e, a cada quadro (idealmente 60 por segundo), calcula em que ponto da linha ele está, interpola os valores dos dois quadros-chave vizinhos e reaplica o estilo. Tudo isso acontece fora do fluxo de JavaScript da página — se a animação usa só transform e opacity, ela roda até com a thread principal ocupada. É a diferença entre "o botão continua girando enquanto a página carrega" e "o botão trava".

5. Animações úteis de referência

Cinco animações que aparecem em quase todo projeto. Copie para o seu css/estilo.css conforme precisar — todas usam só transform, opacity ou background-position.

css/estilo.css (seção 5 — componentes)

CSS
/* Carregando — giro contínuo */
@keyframes girar {
  to { transform: rotate(360deg); }
}

.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid var(--cor-borda);
  border-top-color: var(--cor-secundaria);
  border-radius: 50%;
  animation: girar 700ms linear infinite;
}

/* Esqueleto de carregamento — brilho que percorre a caixa */
@keyframes brilho {
  to { background-position-x: -200%; }
}

.esqueleto {
  background: linear-gradient(90deg,
    var(--cor-esqueleto) 40%,
    var(--cor-esqueleto-brilho) 50%,
    var(--cor-esqueleto) 60%);
  background-size: 200% 100%;
  border-radius: var(--raio-borda);
  animation: brilho 1.4s linear infinite;
}

/* Balançar — campo inválido */
@keyframes tremer {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-6px); }
  40%, 80% { transform: translateX(6px); }
}

.campo.invalido {
  animation: tremer 400ms ease;
}

/* Entrada lateral */
@keyframes entrarEsquerda {
  from { opacity: 0; transform: translateX(-24px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* Flutuação sutil — para um ícone ou ilustração de destaque */
@keyframes flutuar {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-8px); }
}

O @keyframes girar só tem to: quando falta o from, o navegador usa o estado atual do elemento como ponto de partida (rotate(0deg)). O esqueleto usa um gradiente com o dobro da largura da caixa e desloca a posição do fundo — o "brilho" do meio parece passar de um lado ao outro.

💡 Dica Dê nomes de @keyframes que descrevam o movimento (surgir, tremer, girar), não o uso (animacaoDoCartao). Assim a mesma animação serve para cartões, alertas e imagens, e você constrói aos poucos uma biblioteca reutilizável — o desafio ⭐⭐⭐ desta aula é exatamente isso.

6. Performance: as duas propriedades baratas

O navegador desenha uma página em etapas. Entender essas etapas é o que separa uma animação que roda lisa no celular de uma que trava.

  1. Layout (também chamado de reflow): o navegador calcula a posição e o tamanho de cada elemento. Se um elemento muda de largura, todos os vizinhos, os filhos e às vezes a página inteira precisam ser recalculados.
  2. Paint: o navegador pinta os pixels de cada elemento — cores, bordas, sombras, texto.
  3. Composite: o navegador junta as camadas já pintadas na tela, com posição, escala e opacidade. Essa etapa roda na GPU e é muito barata.

Cada propriedade CSS, quando animada, dispara a partir de uma dessas etapas — e tudo que vem depois:

Propriedade animada Etapas disparadas Custo
width, height, margin, padding, top, left Layout + Paint + Composite Alto
background-color, box-shadow, color, border-radius Paint + Composite Médio
transform, opacity Só Composite Baixo

A conclusão prática cabe numa frase: sempre que possível, anime apenas transform e opacity. Um painel que desliza com left obriga o navegador a refazer o layout da página a cada quadro; o mesmo painel com translateX só muda a posição de uma camada já pintada.

CSS
/* Errado: recalcula o layout a cada quadro — trava em celular */
.menu-lateral {
  left: -300px;
  transition: left 300ms;
}
.menu-lateral.aberto {
  left: 0;
}

/* Certo: só composição — 60 fps mesmo em aparelhos modestos */
.menu-lateral {
  transform: translateX(-300px);
  transition: transform 300ms;
}
.menu-lateral.aberto {
  transform: translateX(0);
}

O resultado visual é idêntico. O custo, não.

🧠 Você sabia? A maioria das telas atualiza 60 vezes por segundo. Isso dá ao navegador 16,7 ms para produzir cada quadro — calcular layout, pintar e compor. Se um quadro demora mais que isso, ele é pulado, e o olho percebe o "engasgo": é o que os desenvolvedores chamam de jank. Animar left numa página grande pode custar 30 ms por quadro só de layout; animar transform custa uma fração de milissegundo, porque a GPU só desloca uma textura já pronta. Telas de 120 Hz (comuns em celulares atuais) dão apenas 8,3 ms por quadro — a margem só diminui.

will-change — com moderação

CSS
.painel {
  will-change: transform;
}

will-change avisa o navegador com antecedência: "este elemento vai animar transform, prepare uma camada dedicada para ele". Isso evita o pequeno atraso da criação da camada no início da animação. Mas cada camada consome memória da GPU. Aplique apenas em elementos que realmente vão animar e que mostraram problema na medição; nunca em * ou em dezenas de elementos. Deixar will-change espalhado piora o desempenho — exatamente o oposto do pretendido.

🔬 Investigue Abra o site do evento, pressione F12 e vá ao painel Performance. No ícone de engrenagem, marque CPU: 4× slowdown (simula um celular barato). Clique em gravar, passe o mouse sobre alguns cartões e abra o menu hambúrguer, pare a gravação. Na linha do tempo, procure as barras roxas (Layout) e verdes (Paint) e o gráfico de FPS no topo. Agora troque temporariamente uma transição de transform por left (ou margin-left) e repita. Compare: quantos quadros caíram abaixo de 60 fps? Quanto tempo total foi gasto em Layout? Guarde as duas capturas — o desafio ⭐⭐ pede exatamente esse relatório. Bônus: na aba Rendering (menu ⋮ → More tools), ative Paint flashing: tudo que é repintado pisca em verde. Uma animação boa quase não pisca.

7. Acessibilidade — não é opcional

Movimento excessivo causa náusea, tontura e desorientação em pessoas com distúrbios vestibulares — e o problema não é raro. Todo sistema operacional moderno tem uma opção "reduzir movimento" (no Android, em Acessibilidade; no iOS, em Acessibilidade → Movimento; no Windows, em Configurações de exibição). O CSS consegue ler essa preferência:

css/estilo.css (última seção do arquivo, depois de todas as media queries)

CSS
/* Respeita a preferência do sistema por menos movimento.
   Precisa ficar no FIM da folha para vencer qualquer regra anterior. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Este bloco é uma das poucas situações em que !important é justificado: ele precisa vencer qualquer regra do projeto, inclusive as mais específicas, e ninguém deve conseguir sobrescrevê-lo por acidente. Cole-o no fim de toda folha de estilo desta trilha.

Dois detalhes que costumam gerar dúvida:

  • Por que 0.01ms e não 0? Na Unidade 3 você vai escrever JavaScript que espera o evento transitionend ou animationend para fazer algo (fechar um painel, por exemplo). Com duração zero, alguns navegadores não disparam o evento e o código trava. Com 0,01 ms, a animação "acontece" instantaneamente e o evento dispara.
  • Por que a Aula 08 já tinha uma versão curta disso? Lá o bloco só existia para você conhecer a media query. Esta versão cobre pseudo-elementos, repetições infinitas e rolagem suave — é a definitiva.

Reduzir movimento não significa remover feedback. A melhor prática é trocar o movimento por um fade: onde o cartão deslizava 24 px para cima, ele apenas aparece. O desafio ⭐ desta aula trabalha isso.

Outras regras, todas com base na WCAG 2.1:

  • Nada que pisque mais de 3 vezes por segundo (critério 2.3.1 — risco de convulsão fotossensível).
  • Animações infinitas que chamam atenção devem poder ser pausadas (critério 2.2.2 — pausar, parar, ocultar). Carrosséis automáticos precisam de botão de pausa visível.
  • Nunca use animação como única forma de comunicar informação: o campo inválido treme e fica com borda vermelha e mostra uma mensagem de texto.
  • Todo efeito de :hover precisa de um equivalente em :focus-visible — quem navega por teclado também merece feedback.

🔬 Investigue No DevTools, aba Rendering, procure Emulate CSS media feature prefers-reduced-motion e escolha reduce. Recarregue o site do evento. Tudo que ainda se mexe é uma regra que escapou do bloco acima — geralmente uma animação declarada em um arquivo carregado depois de estilo.css, ou uma transition inline. Faça o mesmo com prefers-color-scheme: dark para conferir o tema escuro da Aula 08.

8. Efeitos visuais

Nem todo efeito envolve movimento. Sombras, gradientes e filtros dão profundidade e hierarquia à página — e, combinados com transições, produzem os efeitos de hover que você vê nos sites profissionais.

Sombras

CSS
/* Elevação sutil — cartões em repouso */
.cartao { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); }

/* Elevação média — cartão em hover, menu suspenso */
.cartao:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }

/* Sombra interna — campo de formulário "afundado" */
.campo { box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); }

/* Anel de foco — alternativa ao outline que respeita o border-radius */
.botao:focus-visible { box-shadow: 0 0 0 3px rgba(26, 127, 181, 0.4); }

/* Múltiplas sombras — uma curta e densa, outra longa e difusa */
.modal { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 8px 24px rgba(0, 0, 0, 0.12); }

/* Sombra que acompanha o formato (útil em PNG com transparência e SVG) */
.logo { filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.2)); }

Os quatro valores de box-shadow são: deslocamento horizontal, deslocamento vertical, desfoque e cor. Sombras realistas têm deslocamento vertical maior que o horizontal (a luz vem de cima) e cor preta com pouca opacidade, nunca cinza sólido. box-shadow desenha um retângulo (ou o border-radius da caixa); filter: drop-shadow() segue o contorno real dos pixels — é o que você quer numa logo com fundo transparente.

Gradientes

CSS
/* Linear: direção por palavra-chave ou ângulo */
.faixa   { background: linear-gradient(to right, #0b3d5c, #1a7fb5); }
.faixa-2 { background: linear-gradient(135deg, #0b3d5c 0%, #1a7fb5 60%, #7ec8e3 100%); }

/* Radial: a partir de um ponto */
.bolha { background: radial-gradient(circle at 30% 30%, #1a7fb5, #0b3d5c); }

/* Cônico: gira em torno do centro — gráficos de pizza sem JavaScript */
.pizza { background: conic-gradient(#0b3d5c 0% 25%, #1a7fb5 25% 100%); }

/* Texto com gradiente */
.titulo-gradiente {
  background: linear-gradient(90deg, #0b3d5c, #1a7fb5);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Sobreposição escura em imagem, para garantir contraste do texto */
.hero {
  background:
    linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55)),
    url("../img/banner.jpg") center / cover no-repeat;
  color: #ffffff;
}

O último exemplo é o mais importante para o projeto: texto branco sobre uma foto nunca tem contraste garantido — em alguma parte da imagem haverá um trecho claro. Um gradiente de duas cores iguais funciona como um "vidro escuro" sobre a foto e garante o contraste AA em toda a área. Repare no caminho ../img/banner.jpg: dentro de css/estilo.css, os caminhos são relativos ao arquivo CSS, não à página HTML.

Filtros

CSS
.a { filter: blur(4px); }
.b { filter: brightness(1.2); }
.c { filter: contrast(1.1); }
.d { filter: grayscale(100%); }
.e { filter: saturate(1.4); }
.f { filter: sepia(60%); }
.g { filter: hue-rotate(90deg); }
.h { filter: invert(100%); }
.i { filter: grayscale(100%) brightness(1.1); }   /* combináveis, aplicados em ordem */

/* Desfoque do que está ATRÁS do elemento — o "vidro fosco" */
.modal-fundo {
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(8px);
}

filter altera o próprio elemento; backdrop-filter altera o que está atrás dele — é o efeito de vidro fosco dos menus e modais modernos. Como a foto de um palestrante em preto e branco que ganha cor no hover: filter: grayscale(100%) no estado normal, grayscale(0) no :hover, com transition: filter 300ms.

Efeitos de hover comuns

CSS
/* 1. Sublinhado que cresce da esquerda e recolhe pela direita */
.link {
  position: relative;
  text-decoration: none;
}

.link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 100%;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 250ms ease;
}

.link:hover::after,
.link:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* 2. Cartão que se eleva */
.cartao {
  transition: transform 200ms ease, box-shadow 200ms ease;
}

.cartao:hover,
.cartao:focus-within {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}

/* 3. Zoom da imagem dentro da moldura */
.moldura {
  overflow: hidden;
  border-radius: var(--raio-borda);
}

.moldura img {
  display: block;
  transition: transform 400ms ease;
}

.moldura:hover img,
.moldura:focus-within img {
  transform: scale(1.06);
}

O sublinhado (efeito 1) é o mesmo que você usou "de bônus" no menu da Aula 07 — agora com um refinamento: a troca de transform-origin faz a linha crescer da esquerda ao entrar e recolher pela direita ao sair, como se deslizasse por baixo do texto. No zoom (efeito 3), o overflow: hidden da moldura corta o excesso da imagem ampliada; sem ele, a imagem invadiria os vizinhos.

⚠️ Atenção Sempre replique o efeito de :hover também em :focus-visible (ou :focus-within, quando o elemento interativo está dentro do cartão). Caso contrário, quem navega por teclado não recebe nenhum retorno visual — e não tem como saber onde está.

Revelação ao rolar a página

O efeito de seções que "surgem" conforme você rola é a combinação de uma transição CSS com um pedacinho de JavaScript que observa quando o elemento entra na tela. O JavaScript é a Unidade 3; aqui você só precisa saber que ele adiciona uma classe — toda a animação continua no CSS.

css/estilo.css (seção 6 — utilitários)

CSS
/* Só esconde quando o JavaScript está ativo; sem JS, o conteúdo aparece normalmente */
.js .revelar {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 500ms ease, transform 500ms ease;
}

.js .revelar.visivel {
  opacity: 1;
  transform: translateY(0);
}

js/efeitos.js

JavaScript
// Marca que o JavaScript está ativo: o CSS só esconde os .revelar com essa classe.
document.documentElement.classList.add("js");

// Observa cada elemento .revelar e adiciona "visivel" quando 15% dele entra na tela.
const observador = new IntersectionObserver((entradas) => {
  entradas.forEach((entrada) => {
    if (entrada.isIntersecting) {
      entrada.target.classList.add("visivel");
      observador.unobserve(entrada.target); // já revelou: para de observar
    }
  });
}, { threshold: 0.15 });

document.querySelectorAll(".revelar").forEach((elemento) => observador.observe(elemento));

index.html (antes de </body>)

HTML
<script src="js/efeitos.js" defer></script>

💡 Dica Repare na classe .js no <html>. Se o CSS escondesse .revelar incondicionalmente e o JavaScript falhasse (bloqueado, com erro, ou ainda carregando numa conexão lenta), o conteúdo ficaria invisível para sempre. Com o .js adicionado pelo próprio script, sem JavaScript a página é só uma página normal. Conteúdo nunca deve depender de animação para existir — esse é o padrão correto, sem exceção.

💻 Mão na massa — Polindo o site do evento

Você vai aplicar os dez itens da prática guiada ao site da Semana Acadêmica de Sistemas de Informação. Todos os trechos vão em css/estilo.css, respeitando a ordem da folha da Aula 06 (1 reset · 2 variáveis · 3 base · 4 layout · 5 componentes · 6 utilitários · 7 media queries), mais um arquivo novo, js/efeitos.js. Abra o site com o Live Server e mantenha o DevTools aberto: cada passo tem um resultado visível.

Passo 1 — variáveis de movimento e transições nos elementos interativos

Assim como as cores e os espaçamentos, as durações e curvas do projeto viram variáveis. Se um dia o evento quiser um site "mais calmo", você muda três linhas.

css/estilo.css (seção 2 — variáveis; acrescente ao :root existente)

CSS
:root {
  --duracao-curta: 150ms;
  --duracao-media: 250ms;
  --duracao-longa: 400ms;
  --curva-padrao: ease-out;
  --cor-esqueleto: #e4e9ee;         /* fundo do cartão-esqueleto */
  --cor-esqueleto-brilho: #f2f5f8;  /* faixa clara que percorre o esqueleto */
}

E, no bloco @media (prefers-color-scheme: dark) que você escreveu na Aula 08, os dois valores escuros correspondentes — sem eles, o esqueleto vira uma mancha branca no tema escuro:

CSS
@media (prefers-color-scheme: dark) {
  :root {
    --cor-esqueleto: #1e2a3a;
    --cor-esqueleto-brilho: #2a3948;
  }
}

As duas cores do esqueleto entram como variáveis pelo mesmo motivo de todas as outras: o Boss desta aula exige "nenhuma cor solta na folha", e um valor fixo aqui quebraria o tema escuro exatamente no componente que mais aparece durante o carregamento.

css/estilo.css (seção 5 — componentes; substitua as regras de .botao, links e campos da Aula 06)

CSS
/* Botões: cor, elevação e "afundar" ao pressionar */
.botao {
  display: inline-block;
  padding: var(--espaco-pequeno) var(--espaco-grande);
  background-color: var(--cor-primaria);
  color: #ffffff;
  border: 0;
  border-radius: 999px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--duracao-curta) var(--curva-padrao),
    transform var(--duracao-curta) var(--curva-padrao),
    box-shadow var(--duracao-curta) var(--curva-padrao);
}

.botao:hover,
.botao:focus-visible {
  background-color: var(--cor-secundaria);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.botao:active {
  transform: translateY(0);
  box-shadow: none;
}

.botao:focus-visible {
  outline: 3px solid var(--cor-secundaria);
  outline-offset: 3px;
}

/* Links de conteúdo */
main a {
  color: var(--cor-secundaria);
  transition: color var(--duracao-curta) var(--curva-padrao);
}

main a:hover,
main a:focus-visible {
  color: var(--cor-primaria);
}

/* Campos do formulário de inscrição (Aula 03) */
.campo input,
.campo select,
.campo textarea {
  border: 1px solid var(--cor-borda);
  border-radius: var(--raio-borda);
  padding: var(--espaco-pequeno) var(--espaco-medio);
  transition:
    border-color var(--duracao-curta) var(--curva-padrao),
    box-shadow var(--duracao-curta) var(--curva-padrao);
}

.campo input:focus-visible,
.campo select:focus-visible,
.campo textarea:focus-visible {
  outline: none;
  border-color: var(--cor-secundaria);
  box-shadow: 0 0 0 3px rgba(26, 127, 181, 0.35);
}

Todas as durações ficam entre 150 ms e 250 ms — micro-interações. Repare que o :active não tem transição própria: ele herda a do estado base, e o botão "afunda" em 150 ms.

Passo 2 — menu com sublinhado animado e item ativo

Substitua o ::after do menu da Aula 07 por esta versão, que cresce da esquerda e recolhe pela direita:

css/estilo.css (seção 4 — layout, regras do .menu)

CSS
.menu a {
  position: relative;
  display: block;
  padding: 0.5rem 0;
  color: var(--cor-texto);
  text-decoration: none;
  font-weight: 500;
  transition: color var(--duracao-curta) var(--curva-padrao);
}

.menu a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  background: var(--cor-secundaria);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--duracao-media) var(--curva-padrao);
}

.menu a:hover,
.menu a:focus-visible {
  color: var(--cor-secundaria);
}

.menu a:hover::after,
.menu a:focus-visible::after,
.menu a[aria-current="page"]::after {
  transform: scaleX(1);
  transform-origin: left;
}

.menu a[aria-current="page"] {
  color: var(--cor-primaria);
  font-weight: 600;
}

.menu__cta::after {
  display: none;
}

O item ativo (aria-current="page", que cada página marca no seu próprio link desde a Aula 07) fica com o sublinhado permanente e não anima — ele está lá, não precisa chamar atenção.

Passo 3 — cartões que se elevam

Os cartões de programacao.html e palestrantes.html (grade .cartoes da Aula 07) ganham elevação no hover e quando o link interno recebe foco.

css/estilo.css (seção 5 — componentes)

CSS
.cartao {
  position: relative;               /* já existia: ancora o selo absolute */
  background: var(--cor-superficie);
  border-radius: var(--raio-borda);
  box-shadow: var(--sombra-cartao);
  overflow: hidden;                 /* corta o zoom da imagem */
  transition:
    transform var(--duracao-media) var(--curva-padrao),
    box-shadow var(--duracao-media) var(--curva-padrao);
}

.cartao:hover,
.cartao:focus-within {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}

.cartao__imagem {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  transition: transform var(--duracao-longa) var(--curva-padrao);
}

.cartao:hover .cartao__imagem,
.cartao:focus-within .cartao__imagem {
  transform: scale(1.05);
}

.cartao a:focus-visible {
  outline: 3px solid var(--cor-secundaria);
  outline-offset: 2px;
}

Passo 4 — hero com gradiente sobre a imagem

A página inicial ganha uma seção de destaque com a foto do auditório ao fundo. Sem o gradiente, o título branco some nas partes claras da foto.

index.html (logo depois do <header>, dentro de <main id="a09-conteudo">)

HTML
<section class="hero" aria-labelledby="titulo-hero">
  <div class="container hero__conteudo">
    <p class="hero__chamada">Semana Acadêmica de Sistemas de Informação</p>
    <h1 id="titulo-hero">Três dias de palestras, minicursos e maratona de programação</h1>
    <p>Auditório Central · vagas limitadas</p>
    <a href="inscricao.html" class="botao">Garanta sua vaga</a>
  </div>
</section>

css/estilo.css (seção 4 — layout)

CSS
.hero {
  background:
    linear-gradient(rgba(11, 61, 92, 0.78), rgba(11, 61, 92, 0.78)),
    url("../img/banner.jpg") center / cover no-repeat;
  color: #ffffff;
  padding-block: clamp(3rem, 10vw, 7rem);
  text-align: center;
}

.hero__chamada {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.875rem;
  opacity: 0.85;
}

.hero h1 {
  font-size: clamp(1.75rem, 4vw, 3rem);
  max-width: 20ch;
  margin-inline: auto;
}

.hero .botao {
  margin-top: var(--espaco-grande);
}

Usei a própria cor primária do evento (#0b3d5c) com 78 % de opacidade no lugar do preto: o resultado tem a identidade do site e ainda assim garante o contraste. Confira no WebAIM: branco sobre #0b3d5c passa com folga no nível AAA.

Passo 5 — entrada escalonada dos cartões da programação

programacao.html (a lista de cartões existente; acrescente o --i em cada item)

HTML
<ul class="cartoes">
  <li class="cartao" style="--i: 0">
    <img src="img/palestra-ia.jpg" alt="" class="cartao__imagem">
    <span class="cartao__selo">Palestra</span>
    <h3>Inteligência artificial no dia a dia do desenvolvedor</h3>
    <p>Como usar assistentes de código sem perder o controle do que você entrega.</p>
    <a href="#ia">Ver detalhes</a>
  </li>
  <li class="cartao" style="--i: 1">
    <img src="img/minicurso-git.jpg" alt="" class="cartao__imagem">
    <span class="cartao__selo">Minicurso</span>
    <h3>Git e GitHub do zero</h3>
    <p>Versionamento, branches e o primeiro pull request em três horas.</p>
    <a href="#git">Ver detalhes</a>
  </li>
  <li class="cartao" style="--i: 2">
    <img src="img/maratona.jpg" alt="" class="cartao__imagem">
    <span class="cartao__selo">Competição</span>
    <h3>Maratona de programação</h3>
    <p>Equipes de três, seis problemas, quatro horas. Inscrições por equipe.</p>
    <a href="#maratona">Ver detalhes</a>
  </li>
</ul>

css/estilo.css (seção 5 — componentes)

CSS
@keyframes surgir {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

.cartoes .cartao {
  animation: surgir var(--duracao-longa) var(--curva-padrao) backwards;
  animation-delay: calc(var(--i, 0) * 80ms);
}

⚠️ Atenção Enquanto a animação surgir está rodando, o transform dela vence o transform do :hover do Passo 3 — é a regra de cascata das animações. Como ela dura 400 ms e acontece só no carregamento, ninguém percebe. Mas se você fizer uma animação infinite com transform num cartão, o hover de elevação para de funcionar. Uma propriedade, um dono.

Passo 6 — spinner no botão e esqueleto de palestrante a confirmar

Ainda não há JavaScript para "carregar" nada de verdade, mas o site já pode ter os dois componentes prontos. O spinner vira um estado do botão de inscrição (a Unidade 3 vai ativá-lo ao enviar o formulário); o esqueleto representa os palestrantes ainda não confirmados em palestrantes.html.

css/estilo.css (seção 5 — componentes; as animações girar e brilho da §5 precisam estar no arquivo)

CSS
/* Botão em estado de carregamento: o texto some e um spinner ocupa o centro */
.botao--carregando {
  position: relative;
  color: transparent;
  pointer-events: none;
}

.botao--carregando::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 20px;
  height: 20px;
  border: 3px solid rgba(255, 255, 255, 0.4);
  border-top-color: #ffffff;
  border-radius: 50%;
  animation: girar 700ms linear infinite;
}

/* Cartão-esqueleto: mesmo tamanho de um cartão real, sem conteúdo */
.cartao--esqueleto {
  padding: var(--espaco-medio);
  display: grid;
  gap: var(--espaco-pequeno);
}

.esqueleto--foto {
  aspect-ratio: 16 / 9;
}

.esqueleto--linha {
  height: 1rem;
}

.esqueleto--curta {
  width: 60%;
}

/* Texto só para leitores de tela (se ainda não tiver este utilitário, adicione na seção 6) */
.visualmente-oculto {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

palestrantes.html (último item da grade .cartoes)

HTML
<li class="cartao cartao--esqueleto" style="--i: 3">
  <span class="visualmente-oculto">Palestrante a confirmar</span>
  <div class="esqueleto esqueleto--foto" aria-hidden="true"></div>
  <div class="esqueleto esqueleto--linha" aria-hidden="true"></div>
  <div class="esqueleto esqueleto--linha esqueleto--curta" aria-hidden="true"></div>
</li>

inscricao.html (para testar o spinner, adicione a classe temporariamente ao botão de envio)

HTML
<button type="submit" class="botao botao--carregando">Enviar inscrição</button>

O aria-hidden="true" nas caixas cinzas e o texto visualmente oculto fazem o leitor de tela anunciar "Palestrante a confirmar" em vez de silêncio ou de três caixas vazias. Depois de testar o spinner, remova a classe botao--carregando do botão.

Passo 7 — o menu hambúrguer passa a deslizar

Na Aula 08 o menu no celular alternava entre display: none e display: flex. Esse é o caso clássico de "não anima". Você vai trocar por opacity + visibility + transform, mantendo o js/menu.js exatamente como está — ele só inverte o aria-expanded, e é o CSS que reage.

css/estilo.css (seção 4 — layout; remova as regras .menu { display: none; } e .menu-botao[aria-expanded="true"] + .menu { display: flex; } da Aula 08 e coloque estas no lugar)

CSS
/* Celular: o menu fica ancorado logo abaixo do cabeçalho, fora de vista */
.menu {
  position: absolute;       /* em relação ao .cabecalho, que é sticky (Aula 07) */
  top: 100%;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: var(--espaco-medio);
  background: var(--cor-superficie);
  border-bottom: 1px solid var(--cor-borda);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  list-style: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition:
    opacity var(--duracao-media) var(--curva-padrao),
    transform var(--duracao-media) var(--curva-padrao),
    visibility 0s var(--duracao-media);
}

.menu-botao[aria-expanded="true"] + .menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition:
    opacity var(--duracao-media) var(--curva-padrao),
    transform var(--duracao-media) var(--curva-padrao),
    visibility 0s;
}

/* O ícone do botão gira 90° quando o menu está aberto */
.menu-botao__icone {
  display: inline-block;
  transition: transform var(--duracao-media) var(--curva-padrao);
}

.menu-botao[aria-expanded="true"] .menu-botao__icone {
  transform: rotate(90deg);
}

css/estilo.css (seção 7 — media queries; dentro do @media (min-width: 768px) já existente, substitua a regra do .menu)

CSS
@media (min-width: 768px) {
  .menu {
    position: static;
    flex-direction: row;
    gap: 1.5rem;
    padding: 0;
    background: transparent;
    border: 0;
    box-shadow: none;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: none;
  }
}

Este é o "painel lateral" do item 7 da prática guiada: entra com transform, sai suavemente e o visibility garante que, fechado, os links não sejam alcançáveis por Tab.

Passo 8 — revelação ao rolar em duas seções

index.html (as duas seções abaixo do hero recebem a classe revelar)

HTML
<section class="container revelar" id="sobre" aria-labelledby="titulo-sobre">
  <h2 id="titulo-sobre">Sobre o evento</h2>
  <p>A Semana Acadêmica de Sistemas de Informação reúne estudantes, professores e profissionais do mercado em três dias de palestras, minicursos e competições no campus de Sinop.</p>
</section>

<section class="container revelar" id="numeros" aria-labelledby="titulo-numeros">
  <h2 id="titulo-numeros">A edição em números</h2>
  <ul class="numeros">
    <li><strong>12</strong> palestras</li>
    <li><strong>6</strong> minicursos</li>
    <li><strong>300</strong> vagas</li>
  </ul>
</section>

Crie o arquivo js/efeitos.js com o código da §8 (o IntersectionObserver que adiciona a classe visivel) e adicione as regras .js .revelar ao css/estilo.css (seção 6 — utilitários). Inclua o script no fim de index.html:

HTML
<script src="js/menu.js" defer></script>
<script src="js/efeitos.js" defer></script>

Passo 9 — bloco prefers-reduced-motion no fim da folha

Cole o bloco completo da §7 como última regra de css/estilo.css, depois de todas as media queries. Se ele ficar antes de alguma transition declarada mais abaixo, o !important ainda vence — mas a convenção "fica no fim" evita que alguém procure por ele no meio do arquivo.

Passo 10 — auditoria no painel Performance

  1. Abra index.html no Chrome, F12 → painel Performance.
  2. Na engrenagem, marque CPU: 4× slowdown.
  3. Clique em Record, role a página do topo ao rodapé passando o mouse sobre os cartões, abra e feche o menu hambúrguer (redimensione para menos de 768 px antes), pare a gravação.
  4. Observe o gráfico de FPS no topo: ele deve ficar verde e estável. Barras vermelhas indicam quadros perdidos.
  5. Na faixa Main, procure blocos roxos de Layout. Durante as animações não deve haver nenhum — se houver, alguma transição está usando uma propriedade de layout.

Como testar

  • Botões e links: passe o mouse e navegue por Tab. Cada elemento interativo reage em até 250 ms, e o retorno do teclado é igual ao do mouse.
  • Menu: em qualquer página, o sublinhado cresce da esquerda ao passar o mouse e recolhe pela direita ao sair; o item da página atual fica sublinhado o tempo todo.
  • Cartões: recarregue programacao.html — os cartões surgem um após o outro, com 80 ms de intervalo. Depois disso, o hover eleva o cartão e amplia a imagem sem vazar da moldura.
  • Hero: o título é legível em qualquer parte da foto; reduza a janela — o título e o espaçamento acompanham (clamp).
  • Esqueleto e spinner: o último cartão de palestrantes.html "brilha"; o botão com botao--carregando mostra um círculo girando no lugar do texto.
  • Menu hambúrguer: abaixo de 768 px, o menu desliza de cima em 250 ms ao clicar no botão e desaparece suavemente ao fechar. Com o menu fechado, Tab não entra nos links dele.
  • Revelação: recarregue index.html e role — as seções "Sobre o evento" e "A edição em números" surgem subindo. Desative o JavaScript (DevTools → Ctrl+Shift+P → "Disable JavaScript") e recarregue: as seções aparecem normalmente, sem animação.
  • Movimento reduzido: em Rendering, emule prefers-reduced-motion: reduce. Tudo passa a acontecer instantaneamente, mas nada some nem deixa de funcionar.
  • Performance: a gravação com CPU 4× mostra FPS estável e nenhum bloco de Layout durante as animações.

Resultado esperado: o site do evento continua idêntico em estrutura, mas cada interação agora tem resposta visual suave, os cartões chegam em cascata, o hero é legível, o menu do celular desliza e ninguém que prefira menos movimento é prejudicado. Faça o commit com a mensagem Aula 09: transições, animações e efeitos.

🧪 Laboratório

Nível A — Fixação

A1. Qual a diferença entre transition e animation? Quando usar cada uma?

A2. Escreva a transição de background-color e transform em 250 ms com curva ease-out, na forma abreviada.

A3. Por que transition: all é desaconselhado? Dê dois motivos.

A4. Explique cada uma das quatro propriedades da transição (transition-property, transition-duration, transition-timing-function, transition-delay).

A5. Diferencie ease-in, ease-out e ease-in-out, indicando quando usar cada uma.

A6. Por que display: none não pode ser transicionado? Qual a alternativa e como ela evita que o elemento escondido receba foco?

A7. Qual a diferença de resultado entre transform: translateX(50px) rotate(45deg) e transform: rotate(45deg) translateX(50px)? Desenhe.

A8. O que faz transform-origin? Dê um exemplo em que alterá-lo é necessário.

A9. Escreva um @keyframes que leve a opacidade de 0 a 1 e desloque o elemento 20 px para cima.

A10. O que faz animation-fill-mode: forwards? O que acontece sem ele?

A11. Qual a diferença entre animation-direction: alternate e reverse?

A12. Por que transform e opacity são as propriedades mais performáticas para animar? Cite as etapas do pipeline que cada grupo de propriedades dispara.

A13. Reescreva de forma performática: .painel { top: -200px; transition: top 300ms; } e o estado aberto .painel.aberto { top: 0; }.

A14. Para que serve will-change? Qual o risco de abusar dela?

A15. Escreva o bloco prefers-reduced-motion completo e explique por que ele usa !important e por que usa 0.01ms em vez de 0.

A16. Qual o limite de piscadas por segundo previsto na WCAG (critério 2.3.1) e por quê?

A17. Escreva um gradiente linear diagonal (135°) de #0b3d5c para #1a7fb5.

A18. Qual a diferença entre box-shadow e filter: drop-shadow()? Em que caso só o segundo funciona?

A19. Por que todo efeito de :hover deve ser replicado em :focus-visible? Qual a diferença entre :focus e :focus-visible?

A20. O que faz backdrop-filter e em que componente ele é tipicamente usado?

Nível B — Aplicação

B1. Construa um botão completo com estados animados: normal, :hover (elevação), :active (pressionado), :disabled (opaco, sem transição) e :focus-visible (anel). Nenhum estado pode depender de cor.

Resultado esperado: um botão que sobe no hover, afunda no clique, mostra um anel no foco por teclado e, desabilitado, fica opaco com cursor not-allowed — e cada estado é distinguível mesmo em uma captura de tela em preto e branco.

Dica

Declare a transition no estado base. Para o :disabled, use transition: none e opacity: 0.5 — um botão desabilitado não deve "reagir" ao mouse. O anel de foco pode ser outline com outline-offset ou box-shadow: 0 0 0 3px.

B2. Faça um cartão giratório 3D para um palestrante: a frente mostra a foto e o nome; o verso mostra a mini-biografia e o link da palestra. O giro acontece no hover e também no foco por teclado.

Resultado esperado: o cartão vira em 600 ms com profundidade real; o verso não aparece espelhado; ao pressionar Tab até o link do verso, o cartão vira sozinho.

Dica

perspective no pai, transform-style: preserve-3d no elemento que gira, backface-visibility: hidden nas duas faces, e o verso começa com rotateY(180deg). Use :focus-within no pai para o giro por teclado. O exemplo completo está na §3.

B3. Crie um spinner e um esqueleto de carregamento para uma listagem de 6 cartões. Simule o carregamento com um setTimeout de 2 segundos (o trecho de JavaScript de 4 linhas está na Dica) e faça a transição do esqueleto para o conteúdo real.

Resultado esperado: ao abrir a página, seis cartões cinza "brilham" por 2 segundos e então desvanecem, dando lugar aos cartões reais, que surgem com surgir.

Dica

Tenha os dois conjuntos de cartões no HTML (esqueleto e real), com o real escondido por opacity: 0; visibility: hidden. O JavaScript apenas troca uma classe no contêiner após 2 s: setTimeout(() => lista.classList.add("carregado"), 2000). O CSS faz o resto com .carregado .esqueleto e .carregado .cartao.

B4. Implemente um menu lateral (drawer) que entra da esquerda animando apenas transform, com sobreposição escura em opacity, fechamento pelo Esc, pelo clique fora e por botão, e com o foco preso dentro dele enquanto estiver aberto (o esqueleto de JavaScript para abrir/fechar e prender o foco costuma ser fornecido pronto nesta altura do curso; se você ainda não estudou JavaScript, adie essa parte para depois da Aula 13 e volte para terminá-la — o CSS já pode ser feito agora).

Resultado esperado: o painel desliza em 300 ms; o fundo escurece junto; nenhuma barra de Layout aparece no painel Performance durante a abertura; com o painel fechado, seus links não recebem foco.

Dica

Painel: position: fixed; inset: 0 auto 0 0; width: min(320px, 85vw); transform: translateX(-100%); aberto: translateX(0). Sobreposição: position: fixed; inset: 0; background: rgba(0,0,0,0.5); opacity: 0; visibility: hidden com o truque do visibility 0s 300ms. Use a escala de z-index da Aula 07 (900 para a sobreposição, 1000 para o painel).

B5. Construa um acordeão animado com três itens: a altura anima suavemente, o ícone gira 180°, apenas um item fica aberto por vez, e o conteúdo fechado não é alcançável por teclado.

Resultado esperado: clicar em um título abre o conteúdo com deslizamento suave e fecha o que estava aberto; o "chevron" gira; Tab pula os conteúdos fechados.

Dica

Altura auto não transiciona. Duas saídas: usar grid-template-rows: 0fr1fr no contêiner (com overflow: hidden no filho), que anima em navegadores modernos, ou max-height com um valor maior que o conteúdo. Para fechar os outros, use <details name="acordeao"> com o mesmo name em todos, ou um pouco de JavaScript. Para o teclado, visibility: hidden no conteúdo fechado.

B6. Faça uma galeria de 6 fotos com efeito de zoom e legenda: a imagem amplia dentro da moldura, a legenda desliza de baixo para cima e um filtro de saturação é aplicado. Tudo reversível e suave.

Resultado esperado: ao passar o mouse (ou focar por teclado), a foto cresce 6 % sem vazar da moldura, ganha cor mais viva e a legenda sobe do rodapé da moldura; ao sair, tudo volta na mesma velocidade.

Dica

Moldura com position: relative; overflow: hidden. Legenda position: absolute; bottom: 0; transform: translateY(100%), e translateY(0) no hover. Na imagem, filter: saturate(1)saturate(1.4) e transform: scale(1.06). Use :focus-within na moldura se a foto for um link.

B7. Implemente uma barra de progresso animada que vai de 0 % ao valor real ao entrar na viewport, com o número contando junto, usando IntersectionObserver (adapte o js/efeitos.js).

Resultado esperado: ao rolar até a seção "vagas preenchidas", a barra cresce de 0 a 72 % em 1,2 s e o texto passa de "0 %" a "72 %" ao mesmo tempo.

Dica

A barra é um transform: scaleX(0) com transform-origin: left que vai a scaleX(var(--progresso)) quando ganha a classe visivel — é transform, portanto barato. O número contando é a única parte que precisa de JavaScript (um setInterval curto ou requestAnimationFrame) — se você ainda não chegou na Aula 13, um trecho pronto costuma ser fornecido para focar no CSS; senão, é um bom exercício escrevê-lo você mesmo.

B8. Crie um conjunto de notificações (toasts) que entram deslizando pela direita, empilham-se, somem automaticamente após 4 segundos com animação de saída e podem ser fechadas manualmente.

Resultado esperado: um botão "Testar notificação" adiciona um toast no canto inferior direito; ele entra da direita, fica 4 s, e sai para a direita desvanecendo; três cliques rápidos empilham três toasts sem sobreposição.

Dica

Contêiner position: fixed; bottom: 1rem; right: 1rem; display: flex; flex-direction: column; gap: 0.5rem. Entrada com @keyframes entrarDireita (espelho de entrarEsquerda) e animation-fill-mode: both. A saída é outra classe (.saindo) com uma animação que leva a translateX(120%) e opacity: 0; remova o elemento no animationend.

B9. Anime a validação do formulário de inscrição: campo inválido treme e ganha borda vermelha; campo válido exibe um ícone de confirmação que surge com escala; a mensagem de erro desliza para baixo.

Resultado esperado: ao sair de um campo inválido (:invalid após interação, ou classe .invalido), ele treme 400 ms e fica vermelho com uma mensagem abaixo; um campo válido mostra um "✓" que cresce de scale(0) a scale(1) com ricochete.

Dica

Use a animação tremer da §5. Para não tremer antes de o usuário digitar, prefira :user-invalid (navegadores recentes) ou uma classe adicionada por JavaScript. O ícone pode ser um ::after com content: "✓" e transform: scale(0)scale(1) com cubic-bezier(.34, 1.56, .64, 1). A mensagem de erro nasce com opacity: 0; transform: translateY(-4px).

B10. Construa um carrossel de depoimentos com transição entre slides, indicadores clicáveis, avanço automático a cada 5 segundos, pausa no hover e no foco, e botão explícito de pausar.

Resultado esperado: três depoimentos alternam a cada 5 s com fade ou deslizamento; os três pontinhos abaixo indicam e escolhem o slide; passar o mouse ou focar qualquer coisa do carrossel interrompe o avanço; o botão "Pausar" vira "Continuar" e realmente para o relógio.

Dica

Faça a versão só com CSS primeiro: uma faixa display: flex com transform: translateX(calc(var(--slide) * -100%)) e transition: transform 500ms. O avanço automático e o botão de pausa precisam de JavaScript (setInterval / clearInterval) — se você ainda não chegou na Aula 13, um trecho pronto costuma ser fornecido para focar no CSS; senão, vale a pena escrevê-lo você mesmo. O botão de pausa é obrigatório pela WCAG 2.2.2 — sem ele, o exercício não está pronto.

Nível C — Desafio

C1. Página de apresentação animada. Construa uma landing page de um produto (ou do próprio evento) com: hero com gradiente animado ao fundo e texto que entra escalonado; menu que muda de aparência ao rolar (fica compacto e ganha sombra); seções que se revelam por IntersectionObserver; contadores animados; galeria com zoom; depoimentos em carrossel; formulário com validação animada; rodapé com links de sublinhado animado. Requisitos: todas as animações usam apenas transform e opacity onde for possível, prefers-reduced-motion respeitado, e a página mantém 60 fps na gravação do painel Performance com CPU 4×. Comece pelo hero e o menu; o restante pode ser terminado depois e reaproveita os exercícios B6, B7, B9 e B10.

Dica

O gradiente animado ao fundo é um background-size: 200% 200% com @keyframes deslocando background-position — barato o suficiente para um único elemento grande. O "menu que muda ao rolar" é uma classe .compacto adicionada pelo JavaScript quando window.scrollY > 80, e o CSS transiciona padding e box-shadow — aqui padding é aceitável porque muda uma vez, não a cada quadro. Faça a auditoria de performance no fim de cada seção, não só no fim da página.

🏆 Desafios

⭐ Caça ao bug: quatro animações que não animam

cssanimacaobugdevtools

Alguém enviou o CSS abaixo dizendo "nada anima, o CSS deve estar bugado". Não está — o navegador está fazendo exatamente o que foi pedido. Há quatro erros conceituais, cada um de um tipo diferente visto nesta aula. Encontre e corrija todos sem reescrever do zero: a intenção de quem escreveu deve ser preservada.

CSS
.botao {
  background: #0b3d5c;
  color: #fff;
}
.botao:hover {
  background: #1a7fb5;
  transform: translateY(-2px);
  transition: all 200ms ease;
}

.cartao-3d {
  perspective: 1000px;
  transform-style: preserve-3d;
  transition: transform 600ms;
}
.cartao-3d:hover {
  transform: rotateY(180deg);
}

@keyframes aparecer {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}
.destaque {
  animation: aparecer 500ms ease-out;
}

.painel {
  display: none;
  transition: opacity 300ms;
}
.painel.aberto {
  display: block;
  opacity: 1;
}

Critérios de pronto

  • O botão entra e sai do hover suavemente, e a transição lista só as propriedades que mudam.
  • O cartão 3D gira com profundidade visível (não "achatado"), com a perspectiva no elemento certo.
  • O .destaque permanece visível depois que a animação termina.
  • O .painel desvanece ao abrir e ao fechar, e fechado não recebe foco por teclado.
  • Um comentário acima de cada correção diz, em uma linha, qual era o erro.
Pistas
  1. Releia o callout 📌 Vale gravar da §2: onde a transition deve ser declarada?
  2. Na §3, perspective e transform-style ficam em elementos diferentes — qual vai no pai e qual no filho?
  3. A §4 tem um callout inteiro sobre a propriedade que "quase todo mundo esquece".
  4. A §2 mostra a dupla que substitui display quando você quer animar a entrada e a saída.

⭐ Movimento que degrada com elegância

cssanimacaoacessibilidaderefatoracao

Ative prefers-reduced-motion: reduce no sistema (ou emule no DevTools) e abra qualquer site grande: a maioria simplesmente remove tudo, e junto some o feedback — o painel que deslizava agora aparece do nada, o toast que entrava pela direita simplesmente "está lá". Reduzir movimento não é remover comunicação. Pegue as animações abaixo, que são chamativas demais para quem tem sensibilidade vestibular, e reescreva o CSS para que, com movimento reduzido, cada uma seja substituída por um fade curto — sem depender do bloco !important genérico do fim da folha.

CSS
@keyframes entrarGirando {
  from { opacity: 0; transform: translateX(-100vw) rotate(-360deg); }
  to   { opacity: 1; transform: translateX(0) rotate(0); }
}
.cartao-destaque { animation: entrarGirando 1.2s ease-out both; }

@keyframes saltar {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-40px); }
}
.icone-atencao { animation: saltar 700ms ease-in-out infinite; }

.painel-lateral {
  transform: translateX(-100%);
  transition: transform 500ms cubic-bezier(.34, 1.56, .64, 1);
}
.painel-lateral.aberto { transform: translateX(0); }

Critérios de pronto

  • Com movimento reduzido, o cartão de destaque aparece com fade de 200 ms, sem deslocamento nem rotação.
  • O ícone de atenção não salta; em vez disso, muda de opacidade duas vezes e para (nada infinito).
  • O painel lateral abre e fecha com fade de 200 ms, sem deslizar, e continua inalcançável por teclado quando fechado.
  • Sem movimento reduzido, as três animações originais continuam funcionando.
  • A solução usa a media query prefers-reduced-motion de forma específica por componente, e não !important em *.
Pistas
  1. A media query pode redefinir o @keyframes? Não — mas pode trocar o animation-name do elemento por outro @keyframes só de opacidade.
  2. Para o ícone, animation-iteration-count: 2 dentro da media query resolve o "infinito"; a §7 explica por quê.
  3. Para o painel, dentro da media query troque a transition por opacity + visibility (§2) e faça transform: none nos dois estados.
  4. Teste com a emulação do DevTools ligada e desligada — as duas versões precisam funcionar.

Para ir além: transforme a técnica em uma convenção do seu projeto autoral: toda animação nova nasce com sua versão reduzida ao lado.

⭐⭐

⭐⭐ left contra transform: meça, não acredite

cssperformancedevtoolsinvestigacao

Esta aula afirma que animar transform é mais barato que animar left. Você vai provar isso com números, do jeito que um engenheiro de front-end faz antes de defender a escolha para outra pessoa. Implemente o mesmo painel deslizante de duas formas: uma animando left (ou width), outra animando transform. Meça as duas com o painel Performance do DevTools, capture o número de quadros por segundo e o tempo gasto em Layout, e escreva um relatório de uma página comparando os resultados, com as capturas de tela como evidência. Repita o teste com a CPU limitada em 4× — é aí que a diferença aparece de verdade.

Critérios de pronto

  • Duas páginas (painel-left.html e painel-transform.html) com o mesmo painel, o mesmo conteúdo (coloque pelo menos 200 elementos na página para o layout ter custo) e o mesmo botão de abrir/fechar.
  • Quatro gravações do painel Performance: cada versão com CPU normal e com CPU 4×.
  • O relatório (relatorio.md ou PDF de uma página) traz uma tabela com FPS mínimo e tempo total de Layout para as quatro gravações, as capturas, e uma conclusão de três linhas.
  • O relatório explica, com suas palavras, por que a diferença existe (pipeline da §6).
  • Bônus: repita no celular real usando a depuração remota do Chrome (chrome://inspect).
Pistas
  1. No painel Performance, o resumo (aba Summary) mostra o tempo total de Rendering; a aba Bottom-Up mostra quanto foi Layout.
  2. O gráfico de FPS fica no topo da gravação; passe o mouse sobre as barras vermelhas para ver a duração de cada quadro perdido.
  3. Se a diferença não aparecer com CPU normal, não conclua que "é igual": aumente o número de elementos e ligue o 4×.
  4. Grave só o trecho da animação (clique em Record, abra o painel, feche, pare) — gravações longas escondem o pico.
⭐⭐⭐

⭐⭐⭐ Sua biblioteca de animações

cssanimacaorefatoracaoprojeto

Bibliotecas como a Animate.css têm milhões de downloads por semana — e cabem em um arquivo CSS que você já sabe ler. Crie o seu animacoes.css com no mínimo 20 animações reutilizáveis, organizadas em quatro categorias (entrada, saída, atenção e carregamento), controladas por variáveis CSS (--duracao, --atraso, --curva) e aplicáveis por classe (.anim-surgir, .anim-tremer). Documente cada uma em uma página de demonstração com o código ao lado do exemplo funcionando e um controle deslizante (<input type="range">) que ajusta a duração ao vivo.

Critérios de pronto

  • animacoes.css com ≥ 20 animações, cada uma em um @keyframes com nome em português descrevendo o movimento, e uma classe .anim-* que a aplica lendo var(--duracao, 400ms), var(--atraso, 0ms) e var(--curva, ease-out).
  • Pelo menos 5 animações por categoria, e as de atenção têm animation-iteration-count finito por padrão.
  • Todas usam apenas transform, opacity ou background-position — nenhuma anima propriedade de layout.
  • O arquivo termina com o bloco prefers-reduced-motion que troca todas as entradas/saídas por fade (não por remoção).
  • demo.html mostra cada animação com um botão "Reproduzir", o código-fonte ao lado (em <pre><code>) e um <input type="range"> que altera --duracao no :root ao vivo.
  • Um README.md explica como usar a biblioteca em qualquer projeto em 5 linhas.

As 3 linhas de JavaScript do controle deslizante:

JavaScript
const controle = document.querySelector('input[type="range"]');
controle.addEventListener('input', () => {
  document.documentElement.style.setProperty('--duracao', controle.value + 'ms');
});
Pistas
  1. Para "reproduzir" uma animação de novo por clique, remova a classe, force um reflow (void elemento.offsetWidth) e adicione a classe de volta — esse é o truque clássico.
  2. Variáveis CSS entram em animation-duration: var(--duracao, 400ms) normalmente; o valor de reserva garante que a classe funcione sem configuração.
  3. Para a saída, animation-fill-mode: forwards é obrigatório — senão o elemento "volta" ao terminar.
  4. Olhe a lista de nomes da Animate.css para não esquecer categorias inteiras (flip, zoom, slide, fade, bounce), mas escreva cada @keyframes você mesmo.

Para ir além: publique a biblioteca no GitHub Pages (Aula 15) e use-a no seu projeto autoral no lugar das animações soltas.

🔥

🔥 Boss — Projeto pronto para o Marco 2

csslayoutresponsivoanimacaoprojeto

Quatro aulas atrás o seu projeto autoral era HTML puro. Hoje ele tem um sistema de design, um layout de verdade, responde a qualquer tela e se move com propósito. O Boss desta unidade é fechar tudo isso em um site que você mostraria numa entrevista — e que é a base direta do Marco 2. Não há nada novo aqui: há tudo da Unidade 2, junto, funcionando ao mesmo tempo, no seu tema.

Critérios de pronto

  • Aula 06 — sistema de design: css/estilo.css único, organizado nas 7 seções, com :root de pelo menos 12 variáveis (cores, espaçamentos, fonte, raio, sombra, durações, curva); nenhuma cor ou duração "solta" no meio do arquivo; contraste AA em todos os textos, incluindo o hero.
  • Aula 07 — layout: estrutura da página em Grid com áreas nomeadas, componentes internos em Flexbox, cabeçalho sticky com <nav aria-label> em lista, aria-current="page" em cada página, link de salto funcionando, rodapé sempre no fim da janela; zero float e zero position: absolute para layout.
  • Aula 08 — responsividade: mobile first com três breakpoints escolhidos pelo conteúdo (documentados em comentário), grade de cartões sem media query (auto-fit/minmax), imagens fluidas com object-fit, tipografia com clamp(), menu hambúrguer com <button aria-expanded aria-controls>, tema escuro via prefers-color-scheme.
  • Aula 09 — movimento: transições em todos os elementos interativos (150–250 ms) com :focus-visible equivalente ao :hover; menu hambúrguer que desliza (transform + opacity + visibility); cartões com entrada escalonada e elevação; hero com gradiente; pelo menos duas seções com revelação ao rolar que aparecem normalmente sem JavaScript; bloco prefers-reduced-motion no fim da folha; nenhuma animação de propriedade de layout.
  • Qualidade: Lighthouse (modo mobile) com Acessibilidade ≥ 90 e Boas práticas ≥ 90 em todas as páginas; gravação do painel Performance com CPU 4× sem quadros perdidos durante a rolagem e a abertura do menu; validador W3C sem erros (Aula 02).
  • Entrega: capturas de cada página em 360 px, 768 px e 1440 px, a captura do Lighthouse e a do painel Performance, tudo numa pasta evidencias/ no repositório.
Pistas
  1. Use os checkpoints das Aulas 06, 07, 08 e 09 como lista de verificação, na ordem — a maioria dos itens você já fez; o Boss é o que falta e a integração.
  2. Rode o Lighthouse antes de mexer em qualquer coisa e anote a nota: os itens de acessibilidade que ele aponta (contraste, nomes de botão, alt, ordem de títulos) são os mais rápidos de resolver.
  3. A gravação de Performance sem quadros perdidos é o critério que mais reprova: procure transition: all, box-shadow animado em muitos elementos ao mesmo tempo e will-change espalhado.
  4. Deixe o tema escuro por último e teste-o com o menu aberto, no hero e nos cartões — é onde as cores "fixas" que escaparam das variáveis aparecem.

Para ir além: peça a alguém que use o seu site só pelo teclado por dois minutos e anote onde essa pessoa se perdeu. Cada ponto anotado é um item do checklist de qualidade do Marco 2 que você acabou de garantir.

🐛 Erros comuns

Sintoma Causa Solução
O hover entra suave, mas ao tirar o mouse o elemento "pula" de volta transition declarada dentro de :hover — ao sair, a regra deixa de valer Declare a transition no estado base do elemento (.botao), não no :hover
Ao acrescentar uma regra nova, coisas que não deviam animar passaram a animar (largura, margem) transition: all anima qualquer propriedade que mude Liste as propriedades explicitamente: transition: transform 200ms, opacity 200ms
A animação roda lisa no computador e engasga no celular; Performance mostra barras roxas de Layout a cada quadro Animação de width, left, top, margin ou height Reescreva com transform (translate/scale); confirme no painel Performance com CPU 4×
O elemento aparece com a animação e, quando ela termina, some ou volta ao estado inicial Falta animation-fill-mode — o padrão none descarta o último quadro animation-fill-mode: forwards (ou both em animações com atraso)
Cartões escalonados aparecem todos de uma vez e "piscam" quando chega a vez de cada um animation-delay sem backwards: durante a espera o elemento fica no estado normal animation-fill-mode: backwards (ou both) para aplicar o primeiro quadro durante o atraso
Micro-interação parece "lenta e arrastada" Duração acima de 500 ms em hover, foco ou troca de cor Reduza para 150–250 ms; reserve 300–500 ms para painéis e modais
Quem navega por Tab não vê nenhum retorno visual Efeito só em :hover, sem :focus-visible (ou :focus-within no cartão) Replique cada regra de :hover em :focus-visible; teste sem o mouse
Usuário relata tontura; o site ignora a opção "reduzir movimento" do sistema Bloco prefers-reduced-motion ausente, ou declarado antes de outras regras sem !important Cole o bloco completo da §7 no fim de estilo.css
A página ficou mais lenta depois de "otimizar" com will-change will-change aplicado a dezenas de elementos cria dezenas de camadas na GPU Use apenas no elemento que comprovadamente precisa; remova do restante
Animação infinita (spinner, pulsar) distrai e não pode ser parada Sem controle de pausa, viola a WCAG 2.2.2 Ofereça botão de pausa, ou limite as repetições; carrossel automático precisa de "Pausar"
O cartão 3D vira, mas parece achatado, sem profundidade; o verso aparece espelhado perspective no próprio elemento que gira em vez do pai; falta backface-visibility: hidden perspective no contêiner pai, transform-style: preserve-3d no que gira, backface-visibility: hidden nas faces
transition: display 300ms não faz nada; o painel aparece de repente display não é interpolável opacity + visibility com o atraso de visibility 0s 300ms na saída (§2)
A imagem de fundo do hero não aparece (404 na aba Network) Caminho relativo ao HTML usado dentro do CSS Dentro de css/estilo.css, o caminho é relativo ao CSS: url("../img/banner.jpg")
Seções com .revelar ficam invisíveis quando o JavaScript está desativado ou com erro CSS esconde .revelar incondicionalmente Esconda apenas .js .revelar, com a classe js adicionada pelo próprio script
O hover de elevação do cartão parou de funcionar depois de adicionar uma animação Duas regras disputam a mesma propriedade transform; a animação em execução vence Não anime transform de forma infinita no mesmo elemento que usa transform no hover; ou anime um filho

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (15 min). SILVA, Maurício Samy. Criando sites com HTML, capítulo sobre efeitos e transformações. MDN: Using CSS transitions e Using CSS animations (links em "Para aprofundar"). Anote uma coisa que a MDN explica e esta aula não.

Parte 2 — Entrega (40 min). Aplique ao seu projeto autoral a Mão na massa completa, com os dez requisitos:

  1. Variáveis de movimento e transições em todos os elementos interativos (150–250 ms).
  2. Menu com sublinhado animado e item ativo.
  3. Cartões com elevação no hover, replicada em :focus-within.
  4. Hero com gradiente sobre a imagem garantindo contraste AA.
  5. Entrada escalonada dos cartões com --i.
  6. Spinner e esqueleto de carregamento.
  7. Menu hambúrguer (ou painel lateral) que abre com transform e sai suavemente.
  8. Revelação ao rolar em duas seções, com js/efeitos.js.
  9. Bloco prefers-reduced-motion no fim da folha.
  10. Captura de tela do painel Performance (CPU 4×) mostrando a rolagem sem quedas de quadro, salva em evidencias/performance.png.

Critério de pronto: todos os dez itens presentes; emular prefers-reduced-motion: reduce no DevTools não esconde nem quebra nada; desativar o JavaScript não esconde nenhuma seção; a captura do painel Performance está no repositório.

Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um site com movimento bem empregado e outro com movimento excessivo, explicando tecnicamente a diferença (o que cada animação comunica, duração, propriedade animada).

Guarde no seu repositório: commit + push (ou a pasta do projeto).

✅ Checkpoint do projeto

Ao fim desta aula — e da Unidade 2 — o repositório do seu projeto autoral precisa ter:

  • [ ] css/estilo.css único, organizado nas 7 seções, com :root incluindo as variáveis de movimento (--duracao-*, --curva-padrao).
  • [ ] Todos os botões, links, campos e cartões com transição entre 150 ms e 250 ms, e cada :hover com seu :focus-visible.
  • [ ] Menu com sublinhado animado (transform: scaleX + transform-origin) e item ativo via aria-current.
  • [ ] Hero com gradiente sobre imagem, título legível em qualquer ponto da foto, tamanhos com clamp().
  • [ ] Cartões com entrada escalonada (--i + animation-delay + backwards) e elevação no hover.
  • [ ] Spinner (@keyframes girar) e esqueleto (@keyframes brilho) prontos no CSS, com texto para leitor de tela.
  • [ ] Menu hambúrguer da Aula 08 animado com opacity + visibility + transform, sem display: none, e js/menu.js inalterado.
  • [ ] js/efeitos.js com IntersectionObserver e pelo menos duas seções .revelar que aparecem mesmo sem JavaScript.
  • [ ] Bloco prefers-reduced-motion completo como última regra do CSS.
  • [ ] Nenhuma animação de width, height, top, left ou margin; nenhum transition: all; nenhum will-change sem justificativa.
  • [ ] evidencias/performance.png com a gravação sem quedas de quadro.
  • [ ] Tudo o que os checkpoints das Aulas 06, 07 e 08 pediam continua funcionando (design system, layout Grid/Flexbox, responsividade em três larguras, tema escuro).

📚 Para aprofundar

Isso encerra a Unidade 2. Em quatro aulas o seu site saiu de HTML puro para um sistema de design em variáveis, um layout de verdade com Grid e Flexbox, responsividade em qualquer tela e movimento que comunica sem atrapalhar. O Marco 2 é exatamente esse site — o Boss desta aula é o roteiro para fechá-lo, e ele fecha na próxima aula. Na próxima aula você abre a Unidade 3 com a Introdução ao JavaScript: um js/app.js novo entra na pasta js/ ao lado do menu.js (Aula 08) e do efeitos.js (hoje), e você começa a ler linha a linha o código que até agora colou "sem entender" — a leitura completa dos dois arquivos se fecha na Aula 13, com funções e eventos. O site deixa de ser só apresentação e começa a reagir ao usuário.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Por que animar2. TransiçõesAs quatro propriedadesFunções de tempoDuração adequadaO que pode e o que não pode ser animado3. TransformaçõesPonto de origemTransformações 3D4. Animações com @keyframesAs propriedades de animationAnimações escalonadasMúltiplas animações5. Animações úteis de referência6. Performance: as duas propriedades barataswill-change — com moderação7. Acessibilidade — não é opcional8. Efeitos visuaisSombrasGradientesFiltrosEfeitos de hover comunsRevelação ao rolar a página💻 Mão na massa — Polindo o site do eventoPasso 1 — variáveis de movimento e transições nos elementos interativosPasso 2 — menu com sublinhado animado e item ativoPasso 3 — cartões que se elevamPasso 4 — hero com gradiente sobre a imagemPasso 5 — entrada escalonada dos cartões da programaçãoPasso 6 — spinner no botão e esqueleto de palestrante a confirmarPasso 7 — o menu hambúrguer passa a deslizarPasso 8 — revelação ao rolar em duas seçõesPasso 9 — bloco prefers-reduced-motion no fim da folhaPasso 10 — auditoria no painel PerformanceComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ Caça ao bug: quatro animações que não animam⭐ Movimento que degrada com elegância⭐⭐ left contra transform: meça, não acredite⭐⭐⭐ Sua biblioteca de animações🔥 Boss — Projeto pronto para o Marco 2🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 3 · JavaScript e interatividade3 aulas de 50 min + 1 h EADFecha a unidade · Marco 2

Aula 10 — Introdução ao JavaScript

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Situar o JavaScript entre as três tecnologias do front-end e explicar o que cada uma faz.
  • Incluir scripts corretamente com defer e explicar por que a posição da tag <script> importa.
  • Usar o Console e a aba Sources do DevTools para executar, inspecionar e depurar código.
  • Declarar variáveis com const e let, escolhendo a forma certa em cada caso.
  • Reconhecer os tipos primitivos, prever o resultado de typeof e listar os valores falsy.
  • Montar textos com template literals e usar os métodos essenciais de string.
  • Converter tipos explicitamente e prever o resultado das conversões implícitas.
  • Fechar o Marco 2 do projeto autoral.

📋 Pré-requisitos

  • [ ] Site do evento com as cinco páginas (início, programação, inscrição, palestrantes, contato) estilizadas, responsivas e com animações, abrindo no Live Server.
  • [ ] VS Code com a extensão Live Server; Chrome ou Firefox com o DevTools acessível pelo F12.
  • [ ] Marco 2 pronto para fechar — as instruções completas estão no fim desta aula.

Na aula passada você fechou a Unidade 2 com transições, @keyframes e prefers-reduced-motion: o site do evento está bonito e responsivo, mas continua parado — cada número de vagas foi digitado à mão no HTML e nada reage ao visitante. Hoje começa a Unidade 3: o JavaScript entra em cena, o site ganha o seu primeiro script e o Console do navegador vira a sua principal ferramenta de trabalho.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min O que é JavaScript; as três formas de incluir um script; defer × async; o Console e o DevTools
2 50 min Variáveis com const/let; tipos primitivos e typeof; strings e template literals; conversões e valores falsy
3 50 min Mão na massa: o site do evento ganha um script externo; Marco 2 do projeto

1. O que é JavaScript

As três camadas do front-end

Uma página web é feita de três linguagens que dividem responsabilidades. Você já domina duas delas; hoje entra a terceira.

Tecnologia Responsabilidade Pergunta que responde No site do evento
HTML estrutura e conteúdo o que está na página? títulos, tabela da programação, formulário de inscrição
CSS apresentação como a página aparece? cores, grid do layout, menu responsivo, animações
JavaScript comportamento o que acontece quando o usuário interage? calcular vagas, validar o formulário, filtrar a programação

A analogia clássica é a de um corpo: o HTML é o esqueleto, o CSS é a pele e a roupa, e o JavaScript são os músculos e o sistema nervoso — sem ele, nada se move nem reage.

Uma linguagem, quatro adjetivos

JavaScript é uma linguagem interpretada, de tipagem dinâmica, multiparadigma e padronizada. Cada adjetivo diz algo prático:

  • Interpretada — você não compila nada antes de rodar. O navegador lê o arquivo .js e executa na hora. (Por baixo do capô, os motores modernos compilam trechos "quentes" para código de máquina em tempo real, a chamada compilação JIT; para você, o efeito é que salvar o arquivo e recarregar a página já basta.)
  • Tipagem dinâmica — uma variável não tem tipo fixo; o valor tem tipo. A mesma variável pode guardar um número agora e um texto depois. Isso dá agilidade e também é a origem de boa parte dos bugs que você vai caçar nesta unidade.
  • Multiparadigma — dá para programar de forma imperativa (sequência de comandos), funcional (funções que recebem e devolvem funções, Aula 13) e orientada a objetos (classes, que você verá no Nível 2).
  • Padronizada — a especificação da linguagem chama-se ECMAScript e é mantida pelo comitê TC39 da Ecma International. "JavaScript" é o nome popular; "ECMAScript" é o nome do padrão. Na prática, são sinônimos.

É a única linguagem executada nativamente por todos os navegadores: Chrome, Firefox, Safari e Edge entendem JavaScript sem instalar nada.

JavaScript não é Java

A confusão é antiga e o nome foi a causa. Em 1995, a Netscape lançou a linguagem como "LiveScript" e, semanas depois, renomeou para "JavaScript" em um acordo de marketing com a Sun Microsystems, dona do Java — que estava na moda. As linguagens são diferentes em sintaxe, tipagem, forma de execução e propósito. Dizer que JavaScript é parecido com Java é como dizer que "hamster" é parecido com "ham" — compartilham letras, não natureza.

ECMAScript e as versões

Versão Ano O que trouxe
ES1 1997 primeira padronização
ES5 2009 modo estrito, JSON nativo, forEach, map, filter
ES6 / ES2015 2015 let, const, arrow functions, classes, template literals, módulos, Promises
ES2016 em diante anual pequenas adições por ano: **, includes, async/await, ?., ??, at()

Desde 2015 sai uma versão por ano, com o nome do ano. Nesta trilha você escreve JavaScript moderno (ES2015+): const e let em vez de var, template literals em vez de concatenação, e os métodos de array que você vai conhecer na Aula 12.

🧠 Você sabia? A primeira versão do JavaScript foi escrita por Brendan Eich em dez dias, em maio de 1995, sob pressão para que o Netscape Navigator 2 saísse com uma linguagem de script. O nome interno era "Mocha". Várias decisões tomadas às pressas naquelas semanas — como o comportamento estranho de == e o resultado de typeof null — continuam na linguagem até hoje, porque corrigi-las quebraria milhões de sites.

Onde o JavaScript roda

O código JavaScript é executado por um motor (engine). Cada navegador tem o seu: o Chrome e o Edge usam o V8, o Firefox usa o SpiderMonkey, o Safari usa o JavaScriptCore. Em 2009, o V8 foi retirado do navegador e embrulhado em um programa de linha de comando chamado Node.js — desde então, JavaScript também roda em servidores, e é isso que você vai usar no Nível 2 para construir APIs. Nesta trilha, porém, o único lugar onde o seu JavaScript roda é o navegador.

2. Como incluir JavaScript em uma página

Existem três maneiras de colocar um script em uma página HTML. Só uma delas é a forma correta para o dia a dia.

Arquivo: exemplo-inclusao.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Três formas de incluir JavaScript</title>

  <!-- 3. Externo, com defer — a forma correta -->
  <script src="js/script.js" defer></script>
</head>
<body>
  <!-- 1. Inline, dentro de um atributo — evite -->
  <button onclick="alert('Oi')">Clique</button>

  <!-- 2. Interno, dentro da própria página — só para testes rápidos -->
  <script>
    console.log("Executando o script interno");
  </script>
</body>
</html>
  1. Inline (onclick="…"): mistura comportamento com estrutura, não pode ser reaproveitado entre páginas e dificulta a leitura. É o equivalente ao style="…" que você aprendeu a evitar na Aula 06.
  2. Interno (<script> com código dentro): útil para um experimento de dois minutos, mas o código fica preso a uma única página.
  3. Externo (<script src="…">): um arquivo .js separado, incluído por todas as páginas que precisam dele, com cache do navegador e leitura limpa. É o que você vai usar no site do evento.

defer, async e a posição da tag

O navegador lê o HTML de cima para baixo e vai construindo a árvore de elementos (o DOM, que você conhecerá a fundo na Aula 13). Quando encontra um <script> sem atributos, ele para de ler o HTML, baixa o script, executa e só então continua. Isso tem duas consequências: a página demora mais para aparecer e, pior, o script roda antes de o restante do HTML existir — qualquer tentativa de acessar um elemento que ainda não foi lido falha.

Forma Quando baixa Quando executa Use quando
<script> no <head> sem atributo interrompe a leitura do HTML imediatamente — o DOM ainda não existe nunca, para scripts que tocam a página
<script> antes de </body> quando o HTML já foi lido imediatamente — o DOM já existe solução clássica; funciona, mas atrasa o download
defer em paralelo com a leitura do HTML depois que todo o HTML foi lido, na ordem em que foram declarados sempre — é a melhor opção
async em paralelo com a leitura do HTML assim que terminar de baixar, fora de ordem só para scripts independentes, como analytics

A regra desta trilha: <script src="…" defer> no <head>. Você ganha o download em paralelo, a garantia de que o HTML inteiro já foi lido e a ordem previsível entre vários scripts — o que vai importar na Aula 12, quando um arquivo de dados precisar ser carregado antes do arquivo que o usa.

🔎 Por baixo do capô O defer só faz sentido com src. Em um script interno (sem src), o atributo é ignorado e o código roda na hora. Se você precisa de um script interno que espere o HTML, coloque-o antes de </body> — ou, melhor, mova o código para um arquivo externo.

O primeiro script completo

Arquivo: primeiro.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Primeiro script</title>
  <script src="js/primeiro.js" defer></script>
</head>
<body>
  <h1>Semana Acadêmica de Sistemas de Informação</h1>
  <p>Vagas restantes: <strong id="vagas">?</strong></p>
</body>
</html>

Arquivo: js/primeiro.js

JavaScript
// Tudo depois de // até o fim da linha é comentário: o navegador ignora.
console.log("O script carregou!");

const vagasTotais = 120;
const inscritos = 87;
const vagasRestantes = vagasTotais - inscritos;

console.log("Vagas restantes:", vagasRestantes);

// Localiza o elemento com id="vagas" e troca o texto dele.
// A manipulação da página será aprofundada na Aula 13; por ora,
// leia esta linha como "escreva o valor dentro do <strong>".
document.querySelector("#vagas").textContent = vagasRestantes;

Abra primeiro.html no Live Server, pressione F12 e olhe a aba Console: a mensagem "O script carregou!" e o número 33 devem aparecer, e o ? da página deve ter virado 33.

🔬 Investigue Remova o defer da tag <script> em primeiro.html, salve e recarregue. A página mostra ? e o Console exibe Uncaught TypeError: Cannot read properties of null (reading 'textContent') (no Firefox: document.querySelector(...) is null). Leia a mensagem com calma: querySelector devolveu null porque o <strong> ainda não existia quando o script rodou. Agora mova a tag <script> (ainda sem defer) para antes de </body> e recarregue: funciona de novo. Você acabou de ver, na prática, por que a posição importa. Devolva o defer ao <head> antes de continuar.

3. O Console e o DevTools

Abrindo o DevTools

F12 abre o painel; Ctrl+Shift+J (no macOS, Cmd+Option+J) abre direto na aba Console. Você já usou as abas Elements e Styles na Unidade 2; agora as abas que interessam são Console e Sources.

Os métodos de console

console.log é a sua principal ferramenta de depuração enquanto aprende. Mas o objeto console tem mais do que log:

Arquivo: js/console-demo.js

JavaScript
console.log("Mensagem comum");
console.info("Informação — igual ao log, com ícone em alguns navegadores");
console.warn("Aviso — fundo amarelo");
console.error("Erro — fundo vermelho e pilha de chamadas");

// Vários valores separados por vírgula: o console mostra cada um com o tipo certo
const nome = "Ana";
const nota = 8.5;
console.log("Aluna:", nome, "Nota:", nota);

// typeof mostra o tipo do valor
console.log("Valor:", nota, typeof nota);

// console.table monta uma tabela a partir de um array de objetos
console.table([
  { nome: "Ana", nota: 8 },
  { nome: "Bruno", nota: 7 },
]);

// console.group agrupa mensagens com recuo (útil para separar etapas)
console.group("Cálculo de vagas");
console.log("Total: 120");
console.log("Inscritos: 87");
console.groupEnd();

// console.time mede quanto tempo passou entre time e timeEnd
console.time("cálculo");
const resultado = 120 - 87;
console.timeEnd("cálculo"); // cálculo: 0.01ms (o número varia)

// console.assert só imprime se a condição for falsa
console.assert(resultado === 33, "O cálculo de vagas está errado!");

// console.count conta quantas vezes foi chamado com aquele rótulo
console.count("carregamento");

// console.clear limpa o console

💡 Dica Use vírgula, não +, para mostrar vários valores: console.log("Nota:", nota) preserva o tipo e a cor de cada valor; console.log("Nota: " + nota) transforma tudo em texto. Isso importa quando o valor é um objeto — com + você vê apenas [object Object].

Escrevendo direto no Console

O Console é um REPL (leia, avalie, imprima, repita): digite uma expressão, pressione Enter e veja o resultado na hora. É o melhor lugar para testar uma dúvida em cinco segundos.

JavaScript
2 + 2
// 4

"Web".toUpperCase()
// "WEB"

typeof 42
// "number"

document.title
// "Primeiro script"

Para escrever mais de uma linha, use Shift+Enter. Para reaproveitar o último resultado, use $_. Para pegar o elemento selecionado na aba Elements, use $0. As setas para cima e para baixo percorrem o histórico.

A aba Sources e os breakpoints

console.log funciona, mas exige que você adivinhe onde colocar a mensagem, salve, recarregue e leia. A aba Sources oferece algo melhor: o breakpoint, um ponto de parada. Abra js/primeiro.js na árvore de arquivos, clique no número de uma linha e recarregue a página. O navegador pausa exatamente ali e mostra, no painel Scope, o valor de cada variável naquele instante. Os botões no topo permitem avançar linha a linha (F10), entrar em uma função (F11) ou continuar a execução (F8).

Você também pode pausar por código: a instrução debugger; no meio do script tem o mesmo efeito de um breakpoint quando o DevTools está aberto.

JavaScript
const vagasTotais = 120;
const inscritos = 87;
debugger; // a execução pausa aqui, com o DevTools aberto
const vagasRestantes = vagasTotais - inscritos;

Nas primeiras semanas, console.log basta. Quando os scripts crescerem (Aula 13 em diante), os breakpoints vão economizar horas.

alert, prompt e confirm

Os três abrem caixas de diálogo do próprio navegador: alert("texto") mostra uma mensagem; prompt("pergunta") pede um texto e devolve o que o usuário digitou (ou null se cancelar); confirm("pergunta") devolve true ou false. Você vai vê-los em tutoriais antigos, e eles servem para um experimento rápido. Em aplicações reais são inadequados: bloqueiam a página inteira até serem fechados, não podem ser estilizados, ignoram o CSS do site, atrapalham leitores de tela e alguns navegadores os suprimem depois do segundo uso. Na Aula 14 você vai construir mensagens de validação de verdade, dentro da própria página.

4. Variáveis: const, let e por que não var

Uma variável é um nome que aponta para um valor guardado na memória. Em JavaScript moderno há duas palavras-chave para declarar variáveis — e uma terceira, herdada, que você precisa reconhecer para nunca usar.

JavaScript
let contador = 0;        // pode receber outro valor depois
const PI = 3.14159;      // NÃO pode receber outro valor
var antigo = "evite";    // legado: escopo de função e comportamento confuso

Regra prática: declare tudo com const. Só troque por let quando o valor precisar mudar ao longo do programa (um contador, um acumulador, um estado que alterna). Nunca use var em código novo — a Aula 11 mostra em detalhe o que ele faz de estranho.

const congela a ligação, não o conteúdo

JavaScript
const lista = [1, 2, 3];
lista.push(4);      // permitido: o conteúdo mudou, a referência continua a mesma
console.log(lista); // [1, 2, 3, 4]

lista = [5, 6];     // Uncaught TypeError: Assignment to constant variable.

const impede que o nome seja religado a outro valor. Se o valor é um array ou um objeto, o seu interior continua editável. Isso confunde no começo, mas é coerente: const protege a ligação entre nome e valor, não o valor em si.

Nomes de variáveis

  • Podem conter letras, dígitos, _ e $; não podem começar com dígito. Acentos são permitidos, mas evite-os — o teclado do colega pode não ter.
  • São case-sensitive: nome, Nome e NOME são três variáveis diferentes.
  • Convenção: camelCase para variáveis e funções (totalInscritos), PascalCase para classes (EventoCard), MAIUSCULO_COM_UNDERSCORE para constantes globais que nunca mudam (VAGAS_TOTAIS).
  • Nomes devem descrever o conteúdo: totalAlunosAprovados, não x ou tmp2. O código é lido muito mais vezes do que escrito.
  • Palavras reservadas não podem ser nomes: let, class, for, if, return, new e cerca de quarenta outras. O Console avisa com Uncaught SyntaxError: Unexpected token se você tentar.
JavaScript
// Bons nomes
const nomeDoEvento = "Semana Acadêmica de Sistemas de Informação";
const VAGAS_TOTAIS = 120;
let inscritosConfirmados = 87;

// Nomes ruins (funcionam, mas ninguém entende)
const n = "Semana Acadêmica de Sistemas de Informação";
const v = 120;
let x = 87;

5. Tipos de dados

Os sete tipos primitivos

Um valor primitivo é um valor simples, sem partes internas, copiado por valor.

JavaScript
const texto = "Olá";                 // string — texto entre aspas
const numero = 42;                   // number — inteiro
const decimal = 3.14;                // number — decimal (não existe tipo separado)
const verdadeiro = true;             // boolean — true ou false
const nada = null;                   // null — ausência intencional de valor
let indefinido;                      // undefined — declarada, mas sem valor
const grande = 9007199254740993n;    // bigint — inteiros gigantes, com n no fim
const simbolo = Symbol("id");        // symbol — identificador único (raro no Nível 1)

Tudo que não é primitivo é objeto: arrays, funções, datas, o próprio document. Você vai trabalhar com arrays e objetos na Aula 12.

typeof

O operador typeof devolve, como string, o tipo de um valor:

JavaScript
typeof "texto";       // "string"
typeof 42;            // "number"
typeof 3.14;          // "number"
typeof true;          // "boolean"
typeof undefined;     // "undefined"
typeof 10n;           // "bigint"
typeof Symbol("id");  // "symbol"
typeof null;          // "object"   ← bug histórico, mantido por compatibilidade
typeof [1, 2];        // "object"   ← arrays são objetos
typeof {};            // "object"
typeof function () {}; // "function" ← funções são objetos, mas typeof as distingue
typeof NaN;           // "number"   ← sim, "não é um número" é do tipo number

🧠 Você sabia? typeof null === "object" é um bug da primeira implementação de 1995. Os valores eram guardados com uma etiqueta de tipo nos bits mais baixos; a etiqueta 0 significava "objeto", e null era representado pelo ponteiro nulo — que também é 0. Quando o TC39 discutiu corrigir isso (uma proposta chegou a sugerir typeof null === "null"), a resposta foi não: sites demais dependem do comportamento antigo. Para testar se um valor é null, compare diretamente: valor === null.

number: um tipo só para inteiros e decimais

JavaScript não separa int de float: todo número é um number, armazenado como ponto flutuante de 64 bits (padrão IEEE 754). Alguns valores especiais moram nesse tipo:

JavaScript
1 / 0;                    // Infinity
-1 / 0;                   // -Infinity
0 / 0;                    // NaN — "Not a Number", resultado de contas impossíveis
Number("abc");            // NaN
Number.MAX_SAFE_INTEGER;  // 9007199254740991 — maior inteiro representado com exatidão
9007199254740992 + 1;     // 9007199254740992 — acima do limite, a conta erra em silêncio
9007199254740992n + 1n;   // 9007199254740993n — bigint resolve
0.1 + 0.2;                // 0.30000000000000004 — a Aula 11 explica esse resultado

NaN tem uma propriedade única: é o único valor de JavaScript que não é igual a si mesmo. NaN === NaN é false. Para verificar se algo é NaN, use Number.isNaN(valor).

null e undefined: dois jeitos de dizer "nada"

undefined null
Quem atribui a própria linguagem você, o programador
Significado "ainda não recebeu valor" "intencionalmente vazio"
Quando aparece variável declarada sem valor; propriedade inexistente; função sem return quando você escreve = null; querySelector sem resultado
typeof "undefined" "object"

Uma variável declarada sem valor vale undefined. Se quiser dizer explicitamente "este campo está vazio de propósito", use null.

6. Strings e template literals

Três formas de escrever texto

JavaScript
const aspasDuplas = "Semana Acadêmica";
const aspasSimples = 'Semana Acadêmica';
const crase = `Semana Acadêmica`;

As duas primeiras são equivalentes; escolha uma e seja consistente (esta apostila usa aspas duplas). A terceira, com crase (acento grave), é o template literal — e faz coisas que as outras não fazem.

Concatenação × template literal

JavaScript
const nome = "Maria";
const idade = 20;

// Concatenação clássica com + : funciona, mas é fácil esquecer um espaço ou uma aspa
const frase1 = "Olá, " + nome + "! Você tem " + idade + " anos.";

// Template literal: a expressão entre ${ } é avaliada e inserida no texto
const frase2 = `Olá, ${nome}! Você tem ${idade} anos.`;

console.log(frase1 === frase2); // true — o resultado é idêntico

Dentro de ${ } cabe qualquer expressão, não só uma variável:

JavaScript
const vagasTotais = 120;
const inscritos = 87;

console.log(`Restam ${vagasTotais - inscritos} vagas.`);
console.log(`Ocupação: ${((inscritos / vagasTotais) * 100).toFixed(1)}%`);
console.log(`Situação: ${inscritos >= vagasTotais ? "lotado" : "aberto"}`);

Multilinha

Um template literal pode ocupar várias linhas, preservando as quebras — perfeito para montar trechos de HTML, como você fará na Aula 13:

JavaScript
const nome = "Maria";
const idade = 20;

const html = `
  <div class="cartao">
    <h3>${nome}</h3>
    <p>Idade: ${idade}</p>
  </div>
`;

console.log(html);

Com aspas comuns, o mesmo texto exigiria \n para cada quebra e + para cada linha.

Métodos essenciais de string

Uma string sabe fazer várias coisas consigo mesma. Os métodos abaixo cobrem 90% do uso diário. Repare que nenhum deles altera a string original — strings são imutáveis; cada método devolve uma string nova.

JavaScript
const s = "  Desenvolvimento Web  "; // dois espaços em cada ponta

s.length;                     // 23 — quantidade de caracteres (é propriedade, sem parênteses)
s.trim();                     // "Desenvolvimento Web" — remove espaços das pontas
s.trimStart();                // "Desenvolvimento Web  "
s.toUpperCase();              // "  DESENVOLVIMENTO WEB  "
s.toLowerCase();              // "  desenvolvimento web  "
s.includes("Web");            // true — contém?
s.startsWith("  Des");        // true — começa com?
s.trim().endsWith("Web");     // true — termina com?
s.indexOf("Web");             // 18 — posição da primeira ocorrência (ou -1)
s.slice(2, 17);               // "Desenvolvimento" — do índice 2 até antes do 17
s.replace("Web", "Mobile");   // "  Desenvolvimento Mobile  " — só a primeira ocorrência
s.replaceAll("e", "3");       // "  D3s3nvolvim3nto W3b  " — todas
s.trim().split(" ");          // ["Desenvolvimento", "Web"] — quebra em array
"7".padStart(3, "0");         // "007" — completa à esquerda até 3 caracteres
"ab".repeat(3);               // "ababab"
s.trim().charAt(0);           // "D" — caractere na posição 0
s.trim()[0];                  // "D" — mesma coisa, com colchetes
s.trim().at(-1);              // "b" — índice negativo conta do fim

O índice começa em zero: o primeiro caractere é o [0]. Isso vale para strings hoje e para arrays na Aula 12.

Caracteres especiais

JavaScript
const comAspas = "Ela disse \"oi\" e saiu";   // \" escapa a aspa dentro da string
const comAspas2 = 'Ela disse "oi" e saiu';    // ou troque o tipo de aspa
const quebra = "Linha 1\nLinha 2";            // \n é quebra de linha
const tab = "Nome:\tAna";                      // \t é tabulação
const barra = "C:\\pasta\\arquivo";            // \\ é uma barra literal

🔬 Investigue No Console, digite "ação".length e depois "😀".length. A primeira dá 4, como esperado. A segunda dá 2, embora seja um único emoji. JavaScript conta strings em unidades de 16 bits (UTF-16), e emojis modernos ocupam duas. Agora teste [..."😀"].length — o resultado é 1. Guarde esse detalhe: ele explica bugs de "limite de caracteres" em formulários com emoji.

7. Conversão de tipos: explícita e implícita

Conversão explícita

Você pede a conversão chamando a função do tipo de destino:

JavaScript
Number("42");          // 42
Number("42.5");        // 42.5
Number("  42  ");      // 42 — espaços nas pontas são ignorados
Number("");            // 0 — string vazia vira zero (armadilha!)
Number("abc");         // NaN
Number("42px");        // NaN — qualquer letra sobrando invalida tudo
Number(null);          // 0
Number(undefined);     // NaN
Number(true);          // 1

parseInt("42px");      // 42 — lê dígitos até encontrar algo que não é dígito
parseInt("3.99");      // 3 — descarta a parte decimal
parseInt("abc");       // NaN
parseFloat("3.14m");   // 3.14

String(42);            // "42"
String(null);          // "null"
(42).toFixed(2);       // "42.00" — atenção: devolve STRING, com 2 casas decimais
(3.14159).toFixed(2);  // "3.14"

Boolean("");           // false
Boolean("0");          // true — string com conteúdo é verdadeira
Boolean(0);            // false
Boolean([]);           // true — array vazio é verdadeiro

Quando o valor vem de um formulário — e todo valor de formulário chega como string —, converter com Number() é o primeiro passo antes de qualquer conta. Isso será o centro da Aula 14.

Conversão implícita (coerção)

JavaScript converte tipos sozinho quando um operador recebe tipos diferentes. As regras são previsíveis, mas nem sempre intuitivas:

JavaScript
"5" + 3;        // "53"  — + com string vira concatenação
"5" - 3;        // 2     — os outros operadores aritméticos convertem para número
"5" * "2";      // 10
"10" / "4";     // 2.5
true + 1;       // 2     — true vira 1
null + 1;       // 1     — null vira 0
undefined + 1;  // NaN   — undefined vira NaN
"3" + 4 + 5;    // "345" — da esquerda para a direita: "3" + 4 = "34", "34" + 5 = "345"
3 + 4 + "5";    // "75"  — 3 + 4 = 7 (número), 7 + "5" = "75"

A regra de ouro: o + prefere texto; os outros preferem número. Se um dos lados do + é string, o resultado é string.

== e ===

Existem dois operadores de igualdade. O == (igualdade frouxa) converte os tipos antes de comparar; o === (igualdade estrita) compara valor e tipo, sem converter nada.

JavaScript
5 == "5";             // true  — "5" vira 5
5 === "5";            // false — tipos diferentes
null == undefined;    // true
null === undefined;   // false
NaN === NaN;          // false — NaN não é igual a nada, nem a si mesmo
0 == "";              // true  — "" vira 0
0 == "0";             // true
"" == "0";            // false — o == nem é transitivo!
[] == false;          // true
null == 0;            // false

Use sempre === e !==. A coerção do == produz resultados bizarros e elimina uma classe inteira de bugs quando abandonada. Se você precisa comparar um número que chegou como texto, converta primeiro (Number(entrada) === 5), não relaxe a comparação.

Falsy e truthy

Quando JavaScript precisa de um booleano — em um if, em um !, em um Boolean() —, ele converte o valor. Apenas oito valores viram false; todos os outros viram true.

Valores falsy: false, 0, -0, 0n, "" (string vazia), null, undefined, NaN.

Todo o resto é truthy — inclusive "0", "false", " " (espaço), [] e {}.

JavaScript
Boolean("false");   // true — é uma string com conteúdo
Boolean("0");       // true
Boolean(" ");       // true — o espaço é conteúdo
Boolean([]);        // true
Boolean({});        // true
Boolean(0);         // false
Boolean("");        // false
Boolean(NaN);       // false

📌 Vale gravar A lista dos oito valores falsy e a diferença entre == e === aparecem em toda entrevista técnica de front-end. Decore a lista e saiba explicar por que "0" é truthy (é uma string não vazia) enquanto 0 é falsy.

8. Lendo erros no Console

Todo erro de JavaScript aparece em vermelho no Console, com três partes: o tipo do erro, a mensagem e o local (arquivo e linha). Aprender a ler as três é metade da depuração.

Texto
Uncaught ReferenceError: inscrito is not defined
    at app.js:12:13
  • ReferenceError — você usou um nome que não existe. Quase sempre é um erro de digitação (inscrito em vez de inscritos) ou uma variável usada antes de ser declarada.
  • TypeError — o valor existe, mas não é do tipo que a operação exige: reatribuir uma const, chamar algo que não é função, ler uma propriedade de null.
  • SyntaxError — o navegador nem começou a executar; o arquivo tem um erro de escrita. Aspa não fechada, parêntese sobrando, vírgula faltando.
  • RangeError — um número fora da faixa permitida (por exemplo, (1).toFixed(200)).

O app.js:12:13 diz: arquivo app.js, linha 12, coluna 13. Clique nesse link: o DevTools abre a aba Sources exatamente naquela linha.

JavaScript
// SyntaxError — a aspa nunca fecha
const nome = "Ana;
// Uncaught SyntaxError: Invalid or unexpected token

// ReferenceError — a variável se chama nome, não nomes
console.log(nomes);
// Uncaught ReferenceError: nomes is not defined

// TypeError — reatribuição de const
const total = 10;
total = 20;
// Uncaught TypeError: Assignment to constant variable.

// TypeError — chamar algo que não é função
const idade = 20;
idade();
// Uncaught TypeError: idade is not a function

Um SyntaxError impede todo o arquivo de rodar, mesmo as linhas corretas. Se nada acontece e o Console mostra um erro de sintaxe, corrija-o primeiro — o resto do script só será avaliado depois.

💻 Mão na massa — O site do evento ganha um script

O site da Semana Acadêmica de Sistemas de Informação tem cinco páginas prontas em HTML e CSS. Hoje ele recebe dois arquivos JavaScript: um compartilhado por todas as páginas e um só para a página de inscrição, que passa a calcular sozinha as vagas restantes.

Passo 1 — criar js/app.js e incluí-lo em todas as páginas

A pasta js/ já existe desde a Aula 08, quando você colou ali o menu.js do hambúrguer, e na Aula 09 ela ganhou o efeitos.js do IntersectionObserver. Hoje entra o terceiro arquivo: js/app.js, o script compartilhado por todas as páginas.

Inclua-o no <head> das cinco páginas, sempre com defer, logo após a folha de estilo — e sem apagar os dois <script> que já estavam lá:

Arquivo: index.html (repita o mesmo <head> em programacao.html, inscricao.html, palestrantes.html e contato.html, trocando só o <title>)

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Semana Acadêmica de Sistemas de Informação: palestras, minicursos e maratona de programação.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Início — Semana Acadêmica de Sistemas de Informação</title>
  <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@400;600;700&display=swap"
        rel="stylesheet">
  <link rel="stylesheet" href="css/estilo.css">
  <script src="js/menu.js" defer></script>
  <script src="js/efeitos.js" defer></script>
  <script src="js/app.js" defer></script>
</head>

O efeitos.js (Aula 09) foi incluído só em index.html; nas outras quatro páginas ficam apenas menu.js e app.js. A ordem entre eles não importa hoje — nenhum depende do outro —, mas a partir da Aula 12 vai importar muito, e é por isso que os três estão declarados com defer no <head>, e não espalhados pelo <body>.

Estrutura de pastas resultante:

Texto
site-evento/
├── index.html
├── programacao.html
├── inscricao.html
├── palestrantes.html
├── contato.html
├── css/
│   └── estilo.css
├── img/
└── js/
    ├── menu.js       (Aula 08)
    ├── efeitos.js    (Aula 09)
    └── app.js        (hoje)

Passo 2 — a mensagem de boas-vindas e os dados do evento

Arquivo: js/app.js

JavaScript
// app.js — carregado por todas as páginas do site do evento

// Dados gerais do evento. Por enquanto vivem aqui; na Aula 12 viram
// arrays e objetos, e no Nível 2 virão de uma API.
const NOME_EVENTO = "Semana Acadêmica de Sistemas de Informação";
const EDICAO = 12;
const LOCAL_EVENTO = "Auditório Central";
const TRILHAS = "Desenvolvimento Web, Dados, Segurança";

console.log(`Bem-vindo à ${EDICAO}ª ${NOME_EVENTO}!`);
console.info("Página atual:", document.title);

console.group("Dados do evento");
console.log("Local:", LOCAL_EVENTO);
console.log("Trilhas:", TRILHAS);
console.log("Tipo de EDICAO:", typeof EDICAO);
console.log("Tipo de NOME_EVENTO:", typeof NOME_EVENTO);
console.groupEnd();

console.table([
  { pagina: "Início", arquivo: "index.html" },
  { pagina: "Programação", arquivo: "programacao.html" },
  { pagina: "Inscrição", arquivo: "inscricao.html" },
  { pagina: "Palestrantes", arquivo: "palestrantes.html" },
  { pagina: "Contato", arquivo: "contato.html" },
]);

Salve, abra qualquer página no Live Server e confira o Console: a saudação, o grupo com os dados e a tabela devem aparecer. Navegue para outra página — o script roda de novo, com o document.title correto.

Passo 3 — o cálculo de vagas na página de inscrição

A página de inscrição tem, em algum lugar, uma frase como "Restam 33 vagas". Esse número foi digitado à mão. Agora ele será calculado. Localize (ou crie) o parágrafo e dê id aos números:

Arquivo: inscricao.html (trecho dentro do <main>, antes do formulário)

HTML
<section class="vagas" aria-live="polite">
  <h2>Vagas</h2>
  <p>
    Restam <strong id="vagas-restantes"></strong> de
    <span id="vagas-totais"></span> vagas
    (<span id="percentual-ocupacao"></span>% ocupadas).
  </p>
</section>

Os travessões são o conteúdo "enquanto o script não roda" — se o JavaScript falhar, o visitante vê um traço, não um número errado.

Inclua um segundo script só nesta página, depois do app.js:

Arquivo: inscricao.html (trecho do <head>)

HTML
<link rel="stylesheet" href="css/estilo.css">
<script src="js/app.js" defer></script>
<script src="js/inscricao.js" defer></script>

Arquivo: js/inscricao.js

JavaScript
// inscricao.js — carregado apenas por inscricao.html

const VAGAS_TOTAIS = 120;
let inscritos = 87; // let: este número vai mudar quando o formulário funcionar (Aula 14)

const vagasRestantes = VAGAS_TOTAIS - inscritos;
const percentualOcupacao = (inscritos / VAGAS_TOTAIS) * 100;

console.group("Vagas");
console.log("Totais:", VAGAS_TOTAIS);
console.log("Inscritos:", inscritos);
console.log("Restantes:", vagasRestantes);
console.log("Ocupação bruta:", percentualOcupacao);              // 72.5
console.log("Ocupação formatada:", percentualOcupacao.toFixed(1)); // "72.5" (string)
console.groupEnd();

// Sanidade: se alguém digitar mais inscritos do que vagas, o console avisa
console.assert(vagasRestantes >= 0, "Há mais inscritos do que vagas!");

// Escreve os valores na página (detalhes na Aula 13)
document.querySelector("#vagas-restantes").textContent = vagasRestantes;
document.querySelector("#vagas-totais").textContent = VAGAS_TOTAIS;
document.querySelector("#percentual-ocupacao").textContent = percentualOcupacao.toFixed(1);

console.log(`Resumo: ${inscritos} inscritos, ${vagasRestantes} vagas livres.`);

Passo 4 — um toque de estilo para o número

Arquivo: css/estilo.css (acrescente ao fim, antes do bloco prefers-reduced-motion)

CSS
/* ===== Vagas (Aula 10) ===== */
.vagas {
  background: var(--cor-fundo-destaque);
  border-left: 4px solid var(--cor-primaria);
  padding: var(--espaco-medio);
  border-radius: var(--raio-borda);
}

.vagas strong {
  font-size: 1.5em;
  color: var(--cor-primaria);
}

Os nomes das variáveis são os do sistema de design que você montou na Aula 06 (--espaco-medio, --raio-borda, --cor-primaria). A única nova é --cor-fundo-destaque — declare-a no :root, junto das outras, em vez de escrever a cor solta aqui.

Como testar

  1. Abra inscricao.html no Live Server. A frase deve mostrar 33 de 120 vagas (72.5% ocupadas), sem travessões.
  2. No Console, confira a saudação do app.js, o grupo "Vagas" e o resumo. Nenhuma linha vermelha.
  3. Mude inscritos para 130, salve e recarregue: o Console mostra Assertion failed: Há mais inscritos do que vagas! e a página exibe -10. Volte para 87.
  4. Abra index.html: só a saudação e a tabela aparecem — o inscricao.js não é carregado aqui, então não há erro de elemento inexistente.
  5. Tire o defer de inscricao.js, recarregue e observe o TypeError. Devolva o defer.

Resultado esperado: a página de inscrição calcula as vagas sozinha; trocar um único número no .js atualiza os três valores na tela.

🧪 Laboratório

Os exercícios do Nível B pedem funções — a sintaxe completa é assunto da Aula 13. Por ora, copie este esqueleto e preencha o corpo:

JavaScript
function nomeDaFuncao(parametro) {
  // cálculos aqui
  return resultado; // o valor devolvido a quem chamou
}

console.log(nomeDaFuncao(10)); // chamada: o argumento 10 entra em parametro

Nível A — Fixação

A1. Qual o resultado e por quê: 5 == "5", 5 === "5", null == undefined, null === undefined, NaN === NaN? Confirme cada resposta no Console antes de escrever a justificativa.

A2. Liste todos os valores falsy do JavaScript. Depois, explique por que "0" e [] não estão na lista.

A3. Reescreva com template literal: "Aluno: " + nome + " - Média: " + media.

A4. Explique a diferença entre defer e async em uma tag <script>. Em qual situação async seria aceitável?

A5. O que retorna typeof null? Por quê? Como testar corretamente se uma variável vale null?

A6. Onde a tag <script> deve ficar e por quê? Cite as duas alternativas que funcionam e diga qual é a preferida.

A7. Qual a diferença entre console.log, console.table e console.error? Dê um exemplo de dado que fica melhor em console.table.

A8. Por que alert e prompt não são adequados em aplicações reais? Cite três motivos.

A9. O que a aba Sources do DevTools permite fazer que o console.log não permite?

A10. Qual a saída de cada linha? Anote sua previsão e só depois teste no Console.

JavaScript
console.log("2" + 2);
console.log("2" - 2);
console.log(2 + 2 + "2");
console.log("2" + 2 + 2);
console.log(typeof (1 + "1"));
console.log(Number("") + 1);
console.log("10" < "9");

A11. O trecho abaixo tem três erros, um de cada tipo (SyntaxError, ReferenceError, TypeError). Encontre-os sem rodar; depois rode e compare com as mensagens do Console.

JavaScript
const evento = "Semana Acadêmica;
const vagas = 120;
vagas = 100;
console.log(nomeEvento, vagas);

Nível B — Aplicação

B1. Escreva formatarMoeda(valor) que converta 1234.5 em "R$ 1.234,50" usando só toFixed, split, replace e padStart. Depois refaça com Intl.NumberFormat e compare o resultado dos dois com ===.

Resultado esperado: formatarMoeda(1234.5) devolve "R$ 1.234,50"; formatarMoeda(7) devolve "R$ 7,00"; formatarMoeda(1000000) devolve "R$ 1.000.000,00".

Dica

valor.toFixed(2)"1234.50"; split(".") separa inteiro e centavos. Para os pontos de milhar sem laço, comece resolvendo até 999.999 (um ponto só) e observe o que falta. Para a versão com Intl: new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(valor). A comparação com === pode dar false mesmo com textos "iguais" — o Intl usa um espaço especial (não separável, código U+00A0) entre R$ e o número. Inspecione com .charCodeAt(2).

B2. Escreva um script que, ao carregar a página, exiba no Console: a data e a hora formatadas em pt-BR, o tamanho da janela (largura × altura), o navegador em uso e uma tabela com cinco produtos (nome, preço, estoque).

Resultado esperado: quatro blocos no Console; a data no formato "dia/mês/ano", o tamanho como 1366 x 768 (varia por máquina), o texto de navigator.userAgent e uma console.table com cinco linhas.

Dica

new Date().toLocaleDateString("pt-BR") e new Date().toLocaleTimeString("pt-BR") formatam data e hora; window.innerWidth e window.innerHeight dão o tamanho; navigator.userAgent identifica o navegador. Redimensione a janela e recarregue para ver os números mudarem.

B3. Crie um arquivo tipos.js que demonstre, com console.table, o resultado de typeof para dez valores diferentes, e escreva um comentário explicando cada resultado inesperado.

Resultado esperado: uma tabela com colunas valor, tipo e dez linhas, incluindo null, NaN, [], function () {} e 10n; pelo menos três comentários explicando resultados surpreendentes.

Dica

Monte um array de objetos no formato { valor: String(x), tipo: typeof x } — o String(x) evita que o console.table tente expandir arrays e funções. Os resultados inesperados a comentar: typeof null, typeof NaN, typeof [] e typeof function () {}.

B4. Escreva descreverConversao(entrada) que receba uma string e devolva, em uma única frase montada com template literal, o resultado de Number(entrada), parseInt(entrada) e parseFloat(entrada), dizendo qual dos três é NaN.

Resultado esperado: descreverConversao("42px") devolve "Number: NaN | parseInt: 42 | parseFloat: 42 | NaN em: Number"; descreverConversao("3.9kg") devolve "Number: NaN | parseInt: 3 | parseFloat: 3.9 | NaN em: Number"; descreverConversao("abc") indica NaN nos três.

Dica

Number.isNaN(x) diz se x é NaN. Monte a lista de quem deu NaN concatenando strings condicionalmente com o ternário condicao ? "Number " : "" — a Aula 11 formaliza esse operador.

Nível C — Desafio

C1. Crie js/diagnostico.js e inclua-o (com defer) em todas as páginas do site do evento. Ao carregar, o script deve imprimir um único console.group chamado "Diagnóstico" com: o caminho da página (location.pathname), o título, o idioma do navegador (navigator.language), se está online (navigator.onLine), a largura da janela classificada como texto ("estreita" abaixo de 600 px, "média" até 1024 px, "larga" acima — use ternários encadeados) e o tempo, em milissegundos, que o próprio script levou para montar tudo isso (console.time/console.timeEnd). Ao fim, remova o arquivo das páginas — ele foi só um exercício.

Dica

Chame console.time("diagnostico") na primeira linha e console.timeEnd("diagnostico") na última. Para a classificação: const largura = window.innerWidth; const faixa = largura < 600 ? "estreita" : largura <= 1024 ? "média" : "larga";.

🏆 Desafios

⭐ Adivinhe a saída: a coerção posta à prova

javascriptinvestigacao

Você acabou de ver que "5" + 3 é "53" e "5" - 3 é 2. Alguém afirma que "com prática dá para prever qualquer coerção sem testar". Vamos ver. Abaixo estão doze expressões; escreva a sua previsão para cada uma antes de digitar no Console, depois confira e conte os acertos. Cada erro é uma regra que você ainda não internalizou — anote-a.

JavaScript
[] + [];
[] + {};
[1, 2] + [3];
"b" + "a" + +"a" + "a";
true + true;
"3" * "4";
"3" + "4";
1 + null;
1 + undefined;
"" == 0;
"0" == false;
null == false;

Critérios de pronto

  • Um arquivo coercao.js com as doze expressões, cada uma precedida por um comentário com a sua previsão e seguida por um console.log que mostra o resultado real.
  • Ao lado de cada expressão errada, um comentário de uma linha explicando a regra que você não conhecia.
  • Uma linha final no arquivo com a contagem: // Acertos: N de 12.
  • Nenhum == sobreviveu em nenhum dos seus arquivos .js após este exercício — busque no VS Code com Ctrl+Shift+F e troque todos por ===.
Pistas
  1. Procure "Equality comparisons and sameness" na MDN e leia a tabela de ==.
  2. Um array vira string com join(",") antes de qualquer +; um objeto comum vira "[object Object]".
  3. O + unário (o +"a") força conversão para número — e "a" não é um número.
  4. null == false é false porque null só é == a undefined e a ele mesmo; a regra dos falsy não vale para o ==.
⭐⭐

⭐⭐ Caça ao bug: cinco erros em um script de 20 linhas

javascriptbugdevtools

O script abaixo deveria mostrar, na página de inscrição, o nome do evento e as vagas restantes. Ele tem cinco defeitos: um impede o arquivo inteiro de rodar, um faz a página quebrar antes de o HTML existir, um produz NaN, um produz um TypeError e um é silencioso — o resultado aparece, mas está errado. Encontre os cinco usando só as mensagens do Console e a aba Sources, e explique cada um.

Arquivo: bug.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Caça ao bug</title>
  <script src="js/bug.js"></script>
</head>
<body>
  <h1 id="titulo"></h1>
  <p>Restam <strong id="vagas"></strong> vagas.</p>
</body>
</html>

Arquivo: js/bug.js

JavaScript
const NOME_EVENTO = "Semana Acadêmica;
const VAGAS_TOTAIS = "120";
const inscritos = 87;

const vagasRestantes = VAGAS_TOTAIS - inscritos;
const percentual = inscritos / VAGAS_TOTAIS * 100;

console.log("Percentual: " + percentual.toFixed(1) + "%");

const resumo = "Restam " + vagasRestantes + " vagas de " + VAGAS_TOTAIS;
document.querySelector("#titulo").textContent = NOME_EVENTO;
document.querySelector("#vagas").textContent = vagasRestantes;

const totalComExtras = VAGAS_TOTAIS + 10;
console.log("Total com vagas extras:", totalComExtras);

NOME_EVENTO = NOME_EVENTO.toUpperCase();
console.log(resumo);

Critérios de pronto

  • A página mostra o nome do evento no <h1> e 33 no <strong>, sem nenhuma linha vermelha no Console.
  • O console.log de "Total com vagas extras" mostra 130, não "12010".
  • Um arquivo bugs.md com uma tabela de cinco linhas: sintoma observado (mensagem literal do Console ou valor errado), causa e correção.
  • O script continua com const em tudo que não muda — corrigir não é trocar tudo por let.
Pistas
  1. Corrija primeiro o erro que o Console mostra como SyntaxError — enquanto ele existir, nenhuma outra linha roda e você não verá os demais.
  2. O segundo erro não está no .js: releia a tabela de defer da §2.
  3. Passe o mouse sobre VAGAS_TOTAIS na aba Sources com a execução pausada e olhe o tipo do valor. Onde o - funciona e o + não?
  4. O erro silencioso é o mesmo de "5" + 3; o TypeError aparece na última linha por causa de uma palavra-chave da §4.
⭐⭐⭐

⭐⭐⭐ Dinheiro não cabe em ponto flutuante

javascriptinvestigacaobug

Um sistema de inscrição cobra R$ 0,10 de taxa por SMS e envia três. No Console, 0.1 * 30.30000000000000004; o cliente vê "R$ 0,30000000000000004" no boleto e liga furioso. Bancos, lojas e sistemas de ingressos não guardam dinheiro em ponto flutuante — guardam centavos inteiros. Investigue por que 0.1 + 0.2 !== 0.3, descubra pelo menos três operações do dia a dia que quebram com decimais, e escreva js/dinheiro.js, um script que representa valores em centavos e formata com Intl.NumberFormat.

Critérios de pronto

  • Um comentário de até dez linhas no topo do arquivo explicando, com suas palavras, por que 0.1 não tem representação exata em binário (cite a quantidade de bits da mantissa no IEEE 754).
  • Três pares de console.log mostrando uma conta que erra em ponto flutuante e a mesma conta correta em centavos (por exemplo: 0.1 + 0.2, 1.005.toFixed(2), 19.99 * 3).
  • Uma demonstração de que Math.abs(a - b) < Number.EPSILON compara decimais com segurança quando centavos não são opção.
  • A formatação final usa Intl.NumberFormat("pt-BR", …) a partir de um inteiro em centavos, e o resultado de formatar(1999 * 3) é "R$ 59,97".
Pistas
  1. Leia a página https://0.30000000000000004.com/ — ela mostra o mesmo problema em dezenas de linguagens; não é um defeito do JavaScript.
  2. No Console, (0.1).toString(2) mostra a representação binária: repare que é uma dízima periódica, como 1/3 em decimal.
  3. (1.005).toFixed(2)"1.00", não "1.01", porque 1.005 é, na verdade, 1.00499999999999989… — teste (1.005).toPrecision(20).
  4. Para formatar centavos: divida por 100 só na hora de exibir (centavos / 100) e entregue esse número ao format do Intl.NumberFormat.

🐛 Erros comuns

Sintoma Causa Solução
Uncaught TypeError: Cannot read properties of null (reading 'textContent') script no <head> sem defer: o elemento ainda não existe quando o código roda adicione defer à tag <script> (ou confira se o id no HTML é exatamente o mesmo do seletor)
GET http://127.0.0.1:5500/js/app.js net::ERR_ABORTED 404 (Not Found) caminho do src errado, pasta com outro nome ou arquivo salvo em outro lugar confira o caminho relativo à página HTML; abra a aba Network e veja qual URL foi pedida
Uncaught ReferenceError: inscrito is not defined nome digitado errado, variável nunca declarada ou declarada em outro arquivo que não foi incluído corrija a grafia; declare com const/let; confira a ordem dos <script>
Uncaught TypeError: Assignment to constant variable. tentativa de dar novo valor a uma const se o valor precisa mudar, declare com let; se não, remova a reatribuição
Uncaught SyntaxError: Invalid or unexpected token aspa não fechada, caractere estranho colado do Word, crase no lugar de aspa leia a linha indicada; o VS Code sublinha o ponto exato
Uncaught SyntaxError: missing ) after argument list parêntese ou vírgula faltando em uma chamada como console.log("a" "b") feche o parêntese ou coloque a vírgula entre os argumentos
Uncaught ReferenceError: Cannot access 'total' before initialization a variável foi usada em uma linha acima da linha em que foi declarada com let/const declare antes de usar; a Aula 11 explica a "zona morta temporal"
A página mostra NaN no lugar de um número uma conta usou um valor que não é número: string com letras, undefined, campo vazio converta com Number() antes de calcular e verifique com Number.isNaN()
O console mostra [object Object] um objeto foi concatenado com string usando + use vírgula no console.log ou JSON.stringify(objeto)
Nada acontece e nenhum erro aparece o script nem foi carregado (cache do navegador, Live Server parado) ou um SyntaxError em outro arquivo silenciou tudo coloque um console.log("carregou") na primeira linha; recarregue com Ctrl+Shift+R

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). FLANAGAN, D. JavaScript: o guia definitivo, capítulos introdutórios sobre tipos, valores e variáveis. STEFANOV, S. Padrões JavaScript, capítulo de fundamentos (o trecho sobre variáveis globais e var). Na MDN, leia "Tipos e estruturas de dados do JavaScript" (link em Para aprofundar).

Parte 2 — Produção (30 min). Hoje fecha o Marco 2 (instruções completas logo abaixo). Além dele, produza o exercício B4 em um arquivo conversao.js com pelo menos cinco casos de teste no Console. No seu projeto autoral: crie a pasta js, inclua js/app.js com defer em todas as páginas e imprima no Console, com console.table, pelo menos cinco dados do seu domínio (por exemplo, cinco plantas do catálogo, cinco quadras da agenda). Em uma das páginas, calcule e exiba um número derivado de constantes — o equivalente às "vagas restantes" do site do evento.

Critério de pronto: todas as páginas do projeto autoral carregam js/app.js sem erro no Console; uma página exibe um valor calculado por JavaScript no lugar de um número digitado no HTML; o conversao.js roda sem erros e mostra os cinco casos.

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga um resultado surpreendente de comparação ou de operação em JavaScript (diferente dos que apareceram nesta aula), explique tecnicamente por que ele ocorre e como evitá-lo.

Guarde no seu repositório: commit + push (ou a pasta do projeto).

✅ Checkpoint do projeto

Ao fim desta aula, o repositório do seu projeto autoral deve ter:

  • [ ] Pasta js/ com app.js, incluído por <script src="js/app.js" defer> no <head> de todas as páginas.
  • [ ] Nenhum erro vermelho no Console em nenhuma página.
  • [ ] Um console.table com dados do seu domínio ao carregar qualquer página.
  • [ ] Pelo menos uma página com um valor calculado em JavaScript (não digitado no HTML), com o HTML mostrando um travessão quando o script não roda.
  • [ ] Zero ocorrências de var e de == nos seus arquivos .js.
  • [ ] Marco 2 fechado.

🎓 Marco do projeto — Unidade 2

Escopo. Ao fim da Unidade 2, o mesmo site do Marco 1 (com as correções apontadas já aplicadas) precisa estar completamente estilizado: layout, menu, responsividade em três larguras e animações.

Requisitos.

# Requisito Onde foi estudado
1 CSS externo, em arquivo único, organizado por seções comentadas Aula 05
2 Reset ou normalização no topo da folha, com box-sizing: border-box global Aulas 05 e 06
3 Sistema de design em variáveis CSS: cores, espaçamentos, tipografia e raios Aula 06
4 Uso demonstrado de seletores de classe, atributo, combinadores, pseudoclasses e pseudoelementos Aula 06
5 Layout principal com CSS Grid e componentes internos com Flexbox Aula 07
6 Menu de navegação estilizado, com item ativo destacado e link de salto para o conteúdo Aula 07
7 Responsividade mobile first, com no mínimo três breakpoints Aula 08
8 Menu responsivo funcional em telas estreitas Aula 08
9 Imagens e tipografia fluidas Aula 08
10 Estados visuais em todos os elementos interativos: :hover, :focus-visible, :active, :disabled Aulas 06 e 09
11 Transições nos elementos interativos e ao menos uma animação com @keyframes que cumpra uma função Aula 09
12 Bloco prefers-reduced-motion ao final da folha Aula 09
13 Contraste WCAG AA em todos os textos, verificado e documentado em contraste.md Aula 06
14 Formulário estilizado com indicação visual de campo inválido que não dependa só de cor Aulas 06 e 09
15 Sem float para estrutura e sem !important (exceto dentro do bloco prefers-reduced-motion) Aulas 07 e 09

Frameworks CSS (Bootstrap, Tailwind e similares) não entram aqui — o objetivo deste marco é demonstrar domínio do CSS puro.

Checklist de qualidade.

  • Folha de estilo organizada e sistema de design coerente (variáveis, não valores soltos).
  • Domínio de seletores e especificidade — nenhum !important usado como atalho.
  • Layout com Grid e Flexbox aplicados onde cada um faz sentido, não ao acaso.
  • Responsividade real nas três larguras, testada em dispositivo de verdade, não só no simulador.
  • Estados visuais completos e contraste acessível em todo texto.
  • Transições e animações com propósito, nunca só para "parecer moderno".
  • Coerência visual entre as cinco páginas e capricho geral no acabamento.

Como saber que está pronto.

  • Rode o Lighthouse (modo mobile) em cada página: Acessibilidade e Boas práticas ≥ 90.
  • No DevTools, alterne prefers-reduced-motion: reduce e prefers-color-scheme: dark e confira que nada quebra.
  • Redimensione a janela (ou use o modo Responsive) em 360 px, 768 px e 1440 px: nenhuma rolagem horizontal, nenhum texto cortado.
  • Verifique contraste.md contra o WebAIM: todo par texto × fundo em AA.
  • Use IA para tirar dúvida ou revisar sintaxe — não para gerar a folha de estilo inteira. Se você não souber explicar por que escolheu Grid em vez de Flexbox num trecho, ainda não é seu.

📚 Para aprofundar

Na próxima aula, o JavaScript começa a decidir: você vai aprofundar escopo e const/let, dominar os operadores aritméticos, de comparação e lógicos (incluindo ?? e ?.) e escrever as primeiras estruturas condicionais — e a página de inscrição do evento passará a mostrar "Últimas vagas!" sozinha.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. O que é JavaScriptAs três camadas do front-endUma linguagem, quatro adjetivosJavaScript não é JavaECMAScript e as versõesOnde o JavaScript roda2. Como incluir JavaScript em uma páginadefer, async e a posição da tagO primeiro script completo3. O Console e o DevToolsAbrindo o DevToolsOs métodos de consoleEscrevendo direto no ConsoleA aba Sources e os breakpointsalert, prompt e confirm4. Variáveis: const, let e por que não varconst congela a ligação, não o conteúdoNomes de variáveis5. Tipos de dadosOs sete tipos primitivostypeofnumber: um tipo só para inteiros e decimaisnull e undefined: dois jeitos de dizer "nada"6. Strings e template literalsTrês formas de escrever textoConcatenação × template literalMultilinhaMétodos essenciais de stringCaracteres especiais7. Conversão de tipos: explícita e implícitaConversão explícitaConversão implícita (coerção)== e ===Falsy e truthy8. Lendo erros no Console💻 Mão na massa — O site do evento ganha um scriptPasso 1 — criar js/app.js e incluí-lo em todas as páginasPasso 2 — a mensagem de boas-vindas e os dados do eventoPasso 3 — o cálculo de vagas na página de inscriçãoPasso 4 — um toque de estilo para o númeroComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ Adivinhe a saída: a coerção posta à prova⭐⭐ Caça ao bug: cinco erros em um script de 20 linhas⭐⭐⭐ Dinheiro não cabe em ponto flutuante🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto🎓 Marco do projeto — Unidade 2📚 Para aprofundar
Nível 1Unidade 3 · JavaScript e interatividade3 aulas de 50 min + 1 h EAD

Aula 11 — Variáveis, operações aritméticas e estruturas de controle

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Explicar escopo global, de função e de bloco, e justificar por que const e let substituíram var.
  • Reconhecer a zona morta temporal e ler a mensagem Cannot access 'x' before initialization.
  • Aplicar todos os operadores aritméticos, incluindo % e **, e prever o resultado de uma expressão com base na tabela de precedência.
  • Usar o objeto Math para arredondar, limitar e sortear valores, e explicar por que 0.1 + 0.2 não é 0.3.
  • Escolher entre ||, ?? e ?. sabendo exatamente o que cada um testa, e explicar o curto-circuito.
  • Escrever condicionais com if/else if/else, com o operador ternário e com switch, decidindo qual forma cabe em cada problema.
  • Formatar números, moedas e datas para o público brasileiro com Intl.NumberFormat e toLocaleDateString.
  • Fazer a página de inscrição do site do evento decidir sozinha quando avisar "Últimas vagas!".

📋 Pré-requisitos

  • [ ] Site do evento com as cinco páginas funcionando no Live Server e js/app.js incluído com defer no <head> de todas elas.
  • [ ] js/inscricao.js criado na Aula 10, calculando vagas restantes e percentual de ocupação e escrevendo os valores em inscricao.html.
  • [ ] Console do navegador aberto (F12) e sem nenhuma linha vermelha em nenhuma das cinco páginas.
  • [ ] Revisar da Aula 10: const/let, os sete tipos primitivos, typeof, os oito valores falsy, === versus == e template literals.

Na aula passada o JavaScript entrou no site do evento: você criou js/app.js, aprendeu a declarar variáveis, conheceu os tipos primitivos e fez a página de inscrição calcular as vagas restantes em vez de exibir um número digitado à mão. O script, porém, faz sempre a mesma coisa — ele não decide nada. Hoje você aprende a calcular de verdade (com todos os operadores e com o objeto Math), a combinar condições com &&, ||, ?? e ?. e a escrever as primeiras estruturas de decisão. Ao fim da aula, a página de inscrição avisa "Últimas vagas!" sozinha e a página inicial mostra quantos dias faltam para o evento.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Escopo, hoisting e zona morta temporal; operadores aritméticos, precedência e o objeto Math
2 50 min Comparação, operadores lógicos, curto-circuito, ?? e ?.; formatação com Intl e datas
3 50 min if/else if/else, ternário e switch; Mão na massa: a inscrição decide sozinha

1. Variáveis e escopo

1.1 O que você já sabe (e o que falta)

Na Aula 10 você aprendeu a regra prática: declare tudo com const; troque para let só quando o valor precisar mudar; nunca use var. O que ficou faltando é o motivo técnico. Ele tem um nome: escopo — a região do código em que um nome existe.

Existem três escopos em JavaScript:

Escopo Onde o nome existe Criado por
Global o arquivo inteiro (e todos os outros scripts da página) declaração fora de qualquer bloco ou função
De função dentro da função onde foi declarado var, parâmetros, const/let dentro da função
De bloco dentro do par de chaves { } mais próximo const e let

Um bloco é qualquer par de chaves: o corpo de um if, de um for, de uma função — ou um par de chaves solto, escrito só para agrupar código.

JavaScript
// js/exemplos-escopo.js
const nomeEvento = "Semana Acadêmica"; // escopo global: vale no arquivo todo

{
  const local = "Auditório Central";   // escopo de bloco: só existe aqui dentro
  console.log(nomeEvento);             // "Semana Acadêmica" — enxerga o de fora
  console.log(local);                  // "Auditório Central"
}

console.log(local);
// Uncaught ReferenceError: local is not defined

A regra é assimétrica e vale a pena guardar: de dentro você enxerga o de fora; de fora você não enxerga o de dentro. Um bloco é uma sala com janela: quem está dentro vê a rua, quem está na rua não vê a sala.

1.2 Por que var é um problema

var não respeita blocos. Ele só respeita funções. Compare:

JavaScript
// js/exemplos-var.js
if (true) {
  var comVar = "vazou";
  let comLet = "presa";
  const comConst = "presa";
}

console.log(comVar);   // "vazou" — var ignorou as chaves do if
console.log(comLet);   // Uncaught ReferenceError: comLet is not defined
console.log(comConst); // Uncaught ReferenceError: comConst is not defined

A variável declarada com var dentro do if continua viva fora dele. Em um script de dez linhas isso parece inofensivo; em um arquivo de trezentas, é a receita para dois trechos distantes usarem o mesmo nome sem perceber e sobrescreverem os valores um do outro.

Há um segundo problema, ainda pior. var permite redeclarar o mesmo nome em silêncio:

JavaScript
var inscritos = 87;
var inscritos = 120;  // nenhum aviso: o valor anterior simplesmente sumiu
console.log(inscritos); // 120

let vagas = 87;
let vagas = 120;
// Uncaught SyntaxError: Identifier 'vagas' has already been declared

Com let, o mesmo erro de digitação vira um erro de sintaxe que o navegador aponta antes de rodar qualquer coisa. É exatamente o que você quer: falhar cedo, com uma mensagem clara.

🧠 Você sabia? Antes de 2015, var era a única forma de declarar variáveis — e o vazamento de escopo era tão incômodo que a comunidade inventou uma gambiarra famosa, a IIFE (Immediately Invoked Function Expression): embrulhar o código inteiro dentro de uma função anônima executada na hora, escrita como (function () { })(); com o programa todo dentro das chaves, só para criar um escopo. Bibliotecas inteiras (jQuery entre elas) começam assim. Quando let e const chegaram com escopo de bloco, a IIFE perdeu o motivo de existir. Se você abrir um arquivo .js de dez anos atrás e encontrar esse embrulho, agora sabe o que ele está fazendo.

1.3 Hoisting e a zona morta temporal

Antes de executar qualquer linha, o motor JavaScript lê o arquivo inteiro e registra todas as declarações. Esse registro antecipado se chama hoisting (içamento). O que muda entre var e let/const é o que acontece com o nome nesse intervalo:

JavaScript
console.log(comVar);   // undefined — o nome já existe, o valor ainda não
var comVar = 10;

console.log(comLet);
// Uncaught ReferenceError: Cannot access 'comLet' before initialization
let comLet = 10;

var é içado e inicializado com undefined — por isso a leitura não falha, ela devolve lixo. let e const são içados, mas ficam em um estado inacessível desde o começo do bloco até a linha da declaração. Esse intervalo tem nome: zona morta temporal (temporal dead zone). Tocar na variável ali dentro é um ReferenceError — e isso é uma boa notícia, porque um erro visível é infinitamente melhor que um undefined silencioso que só vai causar problema três funções adiante.

⚠️ Atenção As duas mensagens são parecidas e significam coisas diferentes. x is not defined quer dizer "esse nome não existe em lugar nenhum" — geralmente um erro de digitação. Cannot access 'x' before initialization quer dizer "o nome existe, mas você o usou antes da linha que o declara" — mova a declaração para cima.

1.4 Sombreamento

Declarar, dentro de um bloco, um nome que já existe fora dele é sombreamento (shadowing). É permitido, e às vezes útil, mas confunde quem lê:

JavaScript
const trilha = "Desenvolvimento Web";

{
  const trilha = "Segurança";   // sombreia a de fora, só dentro deste bloco
  console.log(trilha);          // "Segurança"
}

console.log(trilha);            // "Desenvolvimento Web" — intacta

Evite sombrear nomes importantes. Se dois valores são coisas diferentes, dê nomes diferentes: trilhaPadrao e trilhaEscolhida dizem mais do que duas variáveis chamadas trilha.

1.5 Constantes de verdade

Uma convenção que vale ouro em projetos pequenos: valores fixos do domínio ficam no topo do arquivo, em MAIUSCULO_COM_UNDERSCORE, e nunca aparecem "soltos" no meio do código.

JavaScript
// js/inscricao.js — topo do arquivo
const VAGAS_TOTAIS = 120;
const LIMITE_ULTIMAS_VAGAS = 20;
const TAXA_INSCRICAO = 25;
const DESCONTO_ESTUDANTE = 0.5;

Um número solto no meio de uma conta (if (restantes < 20)) é o que se chama de número mágico: seis meses depois ninguém lembra de onde veio o 20, e quando o valor mudar você vai ter que caçá-lo em cinco lugares. Com a constante, você troca uma linha e o programa inteiro obedece — e o nome documenta a intenção.

2. Operadores aritméticos

2.1 Os sete operadores

JavaScript
// js/exemplos-aritmetica.js
console.log(5 + 3);    // 8   — soma
console.log(5 - 3);    // 2   — subtração
console.log(5 * 3);    // 15  — multiplicação
console.log(5 / 3);    // 1.6666666666666667 — divisão (sempre com decimais)
console.log(5 % 3);    // 2   — resto da divisão inteira
console.log(5 ** 3);   // 125 — potência (5 elevado a 3)
console.log(-5);       // -5  — negação (o único operador unário da lista)

Duas surpresas para quem vem de outra linguagem: a divisão nunca trunca (5 / 2 é 2.5, não 2) e não existe operador de divisão inteira. Para obter a parte inteira use Math.trunc(5 / 2) ou Math.floor(5 / 2), da seção 3.

2.2 O resto (%) é mais útil do que parece

O operador % devolve o que sobra de uma divisão inteira. Ele resolve quatro problemas que aparecem o tempo todo:

JavaScript
// 1. Par ou ímpar
console.log(10 % 2);       // 0 → par
console.log(7 % 2);        // 1 → ímpar

// 2. Múltiplo de alguma coisa (a cada 5 inscritos, um brinde)
const inscritos = 85;
console.log(inscritos % 5); // 0 → é múltiplo de 5

// 3. Ciclos: dar a volta em uma lista de 3 cores
console.log(0 % 3, 1 % 3, 2 % 3, 3 % 3, 4 % 3); // 0 1 2 0 1

// 4. Quebrar um total em unidades (segundos → minutos e segundos)
const totalSegundos = 4000;
const horas = Math.trunc(totalSegundos / 3600);          // 1
const minutos = Math.trunc((totalSegundos % 3600) / 60); // 6
const segundos = totalSegundos % 60;                      // 40
console.log(`${horas}h ${minutos}min ${segundos}s`);      // "1h 6min 40s"

💡 Dica Com números negativos, % em JavaScript devolve o sinal do dividendo: -7 % 3 é -1, não 2. Se você precisa de um resto sempre positivo (para ciclos, por exemplo), use ((n % m) + m) % m.

2.3 Incremento, decremento e atribuição composta

JavaScript
let contador = 5;

contador++;          // pós-incremento: usa o valor e DEPOIS soma 1
++contador;          // pré-incremento: soma 1 e DEPOIS usa o valor
contador--;          // pós-decremento
--contador;          // pré-decremento

console.log(contador); // 5 — subiu duas vezes e desceu duas

A diferença entre n++ e ++n só aparece quando você usa o resultado da expressão no mesmo lugar:

JavaScript
let a = 5;
console.log(a++);   // 5 — imprime, depois vira 6
console.log(a);     // 6

let b = 5;
console.log(++b);   // 6 — vira 6, depois imprime
console.log(b);     // 6

Na prática, use ++ sozinho em uma linha (é o caso dos laços da Aula 12) e evite combiná-lo com outra operação — código como x = a++ + ++a é um enigma, não um programa.

Os operadores de atribuição composta encurtam a forma "pegue o valor, faça uma conta, guarde de volta":

JavaScript
let inscritos = 87;

inscritos += 3;   // igual a: inscritos = inscritos + 3   → 90
inscritos -= 5;   // 85
inscritos *= 2;   // 170
inscritos /= 4;   // 42.5
inscritos %= 10;  // 2.5
inscritos **= 2;  // 6.25

let titulo = "Semana";
titulo += " Acadêmica";   // funciona com strings: "Semana Acadêmica"

2.4 Precedência: quem faz a conta primeiro

Toda expressão com mais de um operador segue uma ordem, do mais "forte" para o mais "fraco". Esta é a parte da tabela que importa no Nível 1:

Nível Operadores Sentido de avaliação
1 (mais forte) ( ), ., [ ], chamada de função da esquerda para a direita
2 !, - unário, ++, --, typeof da direita para a esquerda
3 ** da direita para a esquerda
4 *, /, % da esquerda para a direita
5 +, - da esquerda para a direita
6 <, <=, >, >= da esquerda para a direita
7 ===, !==, ==, != da esquerda para a direita
8 && da esquerda para a direita
9 \|\|, ?? da esquerda para a direita
10 (mais fraco) ? :, =, +=, -= da direita para a esquerda
JavaScript
console.log(2 + 3 * 4);        // 14 — a multiplicação vem antes
console.log((2 + 3) * 4);      // 20 — o parêntese muda tudo
console.log(2 ** 3 ** 2);      // 512 — ** é da direita para a esquerda: 2 ** (3 ** 2)
console.log((-2) ** 2);        // 4 — o parêntese é obrigatório aqui (veja abaixo)
console.log(10 - 4 - 3);       // 3 — da esquerda para a direita: (10 - 4) - 3
console.log(1 + 2 + "3");      // "33" — soma numérica primeiro, depois concatena

Escrever -2 ** 2 sem parênteses é Uncaught SyntaxError: Unary operator used immediately before exponentiation expression. A linguagem se recusa a adivinhar se você quis (-2) ** 2 (que dá 4) ou -(2 ** 2) (que dá −4) — e faz bem.

💡 Dica Não decore a tabela: use parênteses. Ninguém nunca perdeu tempo lendo uma expressão com parênteses demais; muita gente já perdeu uma tarde caçando um bug em uma expressão com parênteses de menos. Escreva (a + b) / 2, não a + b / 2.

2.5 Onde a aritmética quebra

Você viu na Aula 10 que 0.1 + 0.20.30000000000000004. A causa é a representação binária: números decimais como 0,1 viram dízimas periódicas em base 2, exatamente como 1/3 vira 0,333… em base 10. Como o padrão IEEE 754 guarda 53 bits de precisão, a dízima é cortada — e o erro aparece na décima sétima casa.

JavaScript
console.log(0.1 + 0.2);                    // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);            // false
console.log(1.005 * 100);                   // 100.49999999999999
console.log((1.005).toFixed(2));             // "1.00" — e não "1.01"

// Comparando decimais com segurança: a diferença é desprezível?
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true

Number.EPSILON é a menor diferença representável entre 1 e o próximo número — aproximadamente 2,22 × 10⁻¹⁶. A regra prática: nunca compare decimais com ===; compare a diferença absoluta com uma tolerância. E, para dinheiro, faça o que os bancos fazem: guarde centavos inteiros e divida por 100 só na hora de exibir.

Outro ponto de atenção é o que acontece quando um valor não numérico entra na conta:

JavaScript
console.log("120" - 87);        // 33 — o - converte a string
console.log("120" + 87);        // "12087" — o + concatena
console.log(Number("120") + 87); // 207 — a forma correta
console.log(undefined + 1);      // NaN
console.log(0 / 0);              // NaN
console.log(Number.isNaN(0 / 0)); // true

Todo valor vindo de um formulário chega como string. Converta com Number() antes de qualquer conta — é o erro número um dos primeiros scripts, e você vai reencontrá-lo na Aula 14.

🔬 Investigue Abra o Console e digite 0.1 + 0.2 — o resultado com o erro na ponta aparece. Agora digite (0.1).toString(2): você vê a representação binária de 0,1, uma dízima periódica com 0011 se repetindo. Compare com (0.5).toString(2), que dá 0.1 exato — porque 0,5 é uma potência de 2. Por fim, teste 0.1 + 0.2 === 0.3 e Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON. Você acabou de ver, em três linhas, por que sistemas financeiros não usam ponto flutuante.

3. O objeto Math

Math é um objeto embutido com constantes e operações matemáticas. Você não cria um Math; ele já existe e você usa direto.

3.1 Arredondamento

JavaScript
console.log(Math.round(4.5));    // 5  — meio para cima
console.log(Math.round(4.4));    // 4
console.log(Math.round(-4.5));   // -4 — atenção: "para cima" é em direção ao +infinito
console.log(Math.floor(4.9));    // 4  — sempre para baixo
console.log(Math.ceil(4.1));     // 5  — sempre para cima
console.log(Math.trunc(4.9));    // 4  — corta a parte decimal, sem arredondar
console.log(Math.trunc(-4.9));   // -4 — floor daria -5

Escolha pelo significado: para quantas páginas cabem 47 itens de 10 em 10, Math.ceil (5, porque a quinta página existe mesmo com 7 itens). Para quantos brindes completos dá para montar, Math.floor. Para exibir uma média, toFixed(1) — lembrando que ele devolve string.

3.2 O resto do arsenal

JavaScript
console.log(Math.abs(-15));          // 15  — valor absoluto
console.log(Math.max(3, 9, 1));      // 9
console.log(Math.min(3, 9, 1));      // 1
console.log(Math.pow(2, 10));        // 1024 — o mesmo que 2 ** 10
console.log(Math.sqrt(144));         // 12
console.log(Math.cbrt(27));          // 3
console.log(Math.sign(-8));          // -1  — -1, 0 ou 1
console.log(Math.hypot(3, 4));       // 5   — raiz de (3² + 4²)
console.log(Math.PI);                // 3.141592653589793
console.log(Math.random());          // número entre 0 (incluso) e 1 (excluso)

Math.max e Math.min recebem números soltos, não um array — para o array você vai usar Math.max(...numeros) com o operador spread da Aula 12.

3.3 Sorteio: a receita que você vai usar sempre

JavaScript
// Um inteiro entre 0 e 9
const sorteioSimples = Math.floor(Math.random() * 10);

// Um inteiro entre minimo e maximo, ambos inclusos
const minimo = 1;
const maximo = 6;
const dado = Math.floor(Math.random() * (maximo - minimo + 1)) + minimo;

console.log("Dado:", dado);

Leia a fórmula de dentro para fora: Math.random() dá algo como 0,7314; multiplicar por (maximo - minimo + 1) espalha o valor pela faixa; Math.floor corta para o inteiro de baixo; somar minimo desloca a faixa para o início certo. O + 1 é o que inclui o maximo no sorteio — sem ele, o 6 nunca sai.

Limitar um valor a uma faixa (clamp) é outro padrão comum, e sai de graça combinando min e max:

JavaScript
const percentual = 137;
const percentualValido = Math.min(100, Math.max(0, percentual)); // 100

4. Comparação

4.1 Relacionais e igualdade

JavaScript
console.log(10 > 9);       // true
console.log(10 >= 10);     // true
console.log(10 < 9);       // false
console.log(10 <= 9);      // false

console.log(10 === 10);    // true  — mesmo valor E mesmo tipo
console.log(10 !== "10");  // true  — tipos diferentes

Você já sabe da Aula 10 que == converte os tipos antes de comparar e produz resultados absurdos (0 == "" é true, "" == "0" é false). A regra desta trilha é definitiva: === e !==, sempre. Se um dos lados chegou como texto, converta explicitamente (Number(entrada) === 5) em vez de relaxar a comparação.

4.2 Comparar strings compara letra por letra

JavaScript
console.log("a" < "b");        // true  — ordem dos códigos Unicode
console.log("Z" < "a");        // true  — maiúsculas vêm antes das minúsculas
console.log("10" < "9");       // true  — comparação de TEXTO: "1" vem antes de "9"
console.log(10 < 9);           // false — comparação de NÚMEROS
console.log("Ana" < "Álvaro"); // true  — "A" (código 65) vem antes de "Á" (código 193)

As três últimas linhas são armadilhas clássicas. Comparar strings com < compara códigos Unicode, não ordem alfabética do português: todas as letras acentuadas ficam depois de todas as não acentuadas, então uma lista ordenada assim joga "Álvaro" para o fim, depois de "Zuleide". Para ordenar nomes em português, use localeCompare, que conhece as regras do idioma:

JavaScript
console.log("Ana".localeCompare("Álvaro", "pt-BR")); // 1  → "Ana" vem DEPOIS de "Álvaro"
console.log("ana".localeCompare("Ana", "pt-BR"));    // -1 → minúscula antes, sem virar outra letra

Repare no resultado oposto: para o operador <, "Ana" vem antes; para o português, "Álvaro" vem antes, porque o acento não muda a letra.

localeCompare devolve um número negativo, zero ou positivo — exatamente o que a função de ordenação de arrays espera, como você verá na Aula 12.

📌 Vale gravar Três resultados costumam confundir: "10" < "9" é true (comparação textual), NaN === NaN é false (o único valor diferente de si mesmo) e 0.1 + 0.2 === 0.3 é false (ponto flutuante). Saiba explicar cada um em uma frase.

5. Operadores lógicos

5.1 &&, || e !

JavaScript
const temVaga = true;
const inscricoesAbertas = false;

console.log(temVaga && inscricoesAbertas); // false — E: só é true se AMBOS forem true
console.log(temVaga || inscricoesAbertas); // true  — OU: é true se ALGUM for true
console.log(!temVaga);                     // false — NÃO: inverte
Expressão && (E) \|\| (OU)
true com true true true
true com false false true
false com true false true
false com false false false

5.2 Curto-circuito: o que eles realmente devolvem

Aqui está a parte que quase ninguém aprende no primeiro contato. && e || não devolvem true ou false — eles devolvem um dos operandos, e param de avaliar assim que o resultado está decidido. Isso se chama curto-circuito.

JavaScript
console.log(true && "texto");     // "texto" — o E precisa checar o segundo; devolve ele
console.log(false && "texto");    // false   — decidido no primeiro; o segundo nem é lido
console.log("primeiro" || "segundo"); // "primeiro" — o OU para no primeiro truthy
console.log("" || "padrão");      // "padrão"  — "" é falsy, então segue adiante
console.log(0 || 100);            // 100
console.log(null && "nunca lido"); // null

Regra em uma frase: && devolve o primeiro valor falsy que encontrar (ou o último, se todos forem truthy); || devolve o primeiro valor truthy (ou o último, se todos forem falsy).

Isso permite dois padrões que você vai ver em todo código JavaScript moderno:

JavaScript
// Valor padrão quando a variável está vazia
const corEscolhida = "";
const cor = corEscolhida || "azul";   // "azul"

// Executar algo apenas se a condição for verdadeira
const modoDebug = true;
modoDebug && console.log("Modo de depuração ligado");

O segundo padrão é engenhoso, mas prefira um if de verdade: ele diz a intenção sem exigir que quem lê conheça o truque.

5.3 ?? — o padrão que respeita o zero

O || tem um defeito grave: ele considera 0 e "" valores "vazios", porque são falsy. Em um formulário, isso corrompe dados legítimos.

JavaScript
const inscritos = 0;

console.log(inscritos || 87);  // 87 — ERRADO: zero inscritos é um dado válido!
console.log(inscritos ?? 87);  // 0  — CERTO

O operador ?? (nullish coalescing, coalescência nula) só usa o valor da direita quando o da esquerda é null ou undefined. Todo o resto — inclusive 0, "" e false — passa direto.

Valor da esquerda \|\| "padrão" ?? "padrão"
undefined "padrão" "padrão"
null "padrão" "padrão"
0 "padrão" 0
"" "padrão" ""
false "padrão" false

Existe também a atribuição ??=, que só escreve se o valor atual for nulo:

JavaScript
let comentario = null;
comentario ??= "Sem observações";
console.log(comentario); // "Sem observações"

⚠️ Atenção Misturar ?? com && ou || na mesma expressão, sem parênteses, é um SyntaxError proposital da linguagem: a || b ?? c não compila. Os projetistas preferiram obrigar você a escrever (a || b) ?? c a deixar a ambiguidade passar. É uma das poucas vezes em que JavaScript é rígido — aproveite.

5.4 ?. — encadeamento opcional

Quando você lê uma propriedade de algo que pode não existir, o erro é fatal:

JavaScript
const inscricao = { nome: "Maria", contato: { email: "maria@exemplo.br" } };
const vazia = {};

console.log(inscricao.contato.email);  // "maria@exemplo.br"
console.log(vazia.contato.email);
// Uncaught TypeError: Cannot read properties of undefined (reading 'email')

O operador ?. interrompe a leitura e devolve undefined em vez de quebrar:

JavaScript
console.log(vazia.contato?.email);        // undefined — nenhum erro
console.log(vazia.contato?.email ?? "não informado"); // "não informado"

Ele funciona em três formas: objeto?.propriedade, objeto?.[chave] e funcao?.(). A combinação ?. com ?? é o par mais útil do JavaScript moderno: "leia com segurança e, se não houver nada, use este padrão".

💡 Dica Não espalhe ?. por todo lado. Use onde o valor pode legitimamente não existir (um campo opcional, um elemento que só existe em uma página). Se o valor deveria existir e não existe, você quer o erro — ele está te avisando de um bug de verdade, e o ?. só o esconderia.

6. Formatando números e datas para o Brasil

O JavaScript nasceu nos Estados Unidos, e por isso (1234.5).toString() devolve "1234.5" — ponto decimal, sem separador de milhar. Mostrar isso para um usuário brasileiro é um erro de produto. A solução padrão é a família Intl.

6.1 Intl.NumberFormat

JavaScript
// js/exemplos-formatacao.js
const formatarReal = new Intl.NumberFormat("pt-BR", {
  style: "currency",
  currency: "BRL",
});

console.log(formatarReal.format(25));       // "R$ 25,00"
console.log(formatarReal.format(1234.5));   // "R$ 1.234,50"
console.log(formatarReal.format(0));        // "R$ 0,00"

const formatarPercentual = new Intl.NumberFormat("pt-BR", {
  style: "percent",
  maximumFractionDigits: 1,
});

console.log(formatarPercentual.format(0.725)); // "72,5%" — recebe a FRAÇÃO, não 72.5

const formatarInteiro = new Intl.NumberFormat("pt-BR");
console.log(formatarInteiro.format(1200000));  // "1.200.000"

Repare que style: "percent" espera a fração (0.725), não o número já multiplicado. É um erro comum mostrar "72500%" na tela por causa disso.

Existe também o atalho toLocaleString, que faz o mesmo sem guardar o formatador:

JavaScript
console.log((1234.5).toLocaleString("pt-BR", { style: "currency", currency: "BRL" }));
// "R$ 1.234,50"

Quando o mesmo formato é usado muitas vezes, criar o Intl.NumberFormat uma vez e reaproveitar é mais rápido.

6.2 Datas com Date

Um objeto Date guarda um instante como a quantidade de milissegundos desde 1º de janeiro de 1970, UTC — a chamada época Unix.

JavaScript
const agora = new Date();

console.log(agora.getFullYear());   // o ano, com quatro dígitos
console.log(agora.getMonth());      // 0 a 11 — janeiro é 0! (armadilha clássica)
console.log(agora.getDate());       // o dia do mês, 1 a 31
console.log(agora.getDay());        // o dia da semana, 0 (domingo) a 6 (sábado)
console.log(agora.getHours());      // a hora, 0 a 23
console.log(agora.getTime());       // milissegundos desde a época Unix

Para exibir, use os métodos toLocale*, que respeitam o idioma:

JavaScript
const agora = new Date();

console.log(agora.toLocaleDateString("pt-BR"));  // dia/mês/ano
console.log(agora.toLocaleTimeString("pt-BR"));  // hora:minuto:segundo

console.log(agora.toLocaleDateString("pt-BR", {
  weekday: "long",
  day: "2-digit",
  month: "long",
}));
// algo como "segunda-feira, 05 de maio"

Subtrair duas datas devolve a diferença em milissegundos — porque o operador - converte cada objeto em número, exatamente a coerção que você estudou na Aula 10:

JavaScript
const MS_POR_DIA = 1000 * 60 * 60 * 24;

const agora = new Date();
const daquiUmaSemana = new Date();
daquiUmaSemana.setDate(agora.getDate() + 7);

const diferenca = daquiUmaSemana - agora;             // número, em milissegundos
console.log(Math.round(diferenca / MS_POR_DIA));      // 7

setDate aceita valores fora da faixa e ajusta o mês sozinho: se hoje é dia 28 e você soma 7, o objeto vira dia 4 do mês seguinte, sem que você precise saber quantos dias tem o mês. É a forma segura de fazer contas com calendário.

🧠 Você sabia? O getMonth() devolver 0 para janeiro não é um capricho: a API de datas do JavaScript foi copiada, em 1995 e às pressas, da classe java.util.Date do Java, que já tinha essa numeração. O erro se espalhou para milhões de linhas de código e nunca pôde ser corrigido. O comitê da linguagem trabalha desde então em uma substituta completa, a API Temporal, com meses de 1 a 12 e objetos imutáveis. Enquanto ela não chega a todos os navegadores, some 1 ao getMonth() — e prefira toLocaleDateString para exibir.

7. Estruturas condicionais

Até agora todo script que você escreveu executava de cima para baixo, sem desvios. A estrutura condicional é o que permite ao programa escolher um caminho.

7.1 if, else if, else

JavaScript
// js/exemplos-condicionais.js
const nota = 7.5;

if (nota >= 6) {
  console.log("Aprovado");
} else if (nota >= 4) {
  console.log("Exame final");
} else {
  console.log("Reprovado");
}

Três regras que evitam a maior parte dos erros:

  1. A condição é convertida para booleano. Qualquer valor serve; os oito falsy da Aula 10 (false, 0, -0, 0n, "", null, undefined, NaN) levam ao else, todo o resto entra no if.
  2. A ordem importa. Os testes são avaliados de cima para baixo e o primeiro verdadeiro vence — os demais nem são lidos. Se você escrever if (nota >= 4) antes de if (nota >= 6), ninguém nunca será aprovado.
  3. Use sempre as chaves. JavaScript permite omiti-las quando há uma instrução só, e essa permissão já causou falhas de segurança famosas:
JavaScript
const temVaga = false;

if (temVaga)
  console.log("Inscrição liberada");
  console.log("Enviando e-mail de confirmação"); // SEMPRE executa: não está no if!

O recuo engana o olho; o interpretador só enxerga a primeira linha como corpo do if. Com chaves, o problema não existe.

7.2 Condições compostas

JavaScript
const vagasRestantes = 12;
const inscricoesAbertas = true;
const idade = 17;

if (inscricoesAbertas && vagasRestantes > 0) {
  console.log("Pode se inscrever");
}

if (idade < 18 || idade >= 65) {
  console.log("Precisa de autorização ou tem gratuidade");
}

// Erro clássico: JavaScript não entende "entre"
// if (0 < vagasRestantes < 20) — não faça isso
if (vagasRestantes > 0 && vagasRestantes < 20) {
  console.log("Últimas vagas");
}

Por que 0 < vagasRestantes < 20 não funciona? Porque a avaliação é da esquerda para a direita: 0 < 12true, e então true < 20 compara 1 < 20, que também dá true — mesmo se vagasRestantes fosse 500. A condição parece certa e está sempre certa, que é o pior tipo de bug.

7.3 O operador ternário

Para escolher um valor entre dois, o ternário é mais direto que um if:

JavaScript
const nota = 7.5;

const situacao = nota >= 6 ? "Aprovado" : "Reprovado";
console.log(situacao);

// Muito usado dentro de template literals
const vagas = 3;
console.log(`Restam ${vagas} ${vagas === 1 ? "vaga" : "vagas"}.`);

Leia condicao ? valorSeVerdadeiro : valorSeFalso como uma pergunta: "a condição é verdadeira? então isto; senão, aquilo".

Ternários podem ser encadeados, e aí a legibilidade depende da formatação:

JavaScript
const ocupacao = 82;

const status =
  ocupacao >= 100 ? "esgotado"
  : ocupacao >= 80 ? "últimas vagas"
  : ocupacao >= 50 ? "enchendo"
  : "vagas à vontade";

console.log(status); // "últimas vagas"

Escrito assim, em uma coluna, o encadeamento vira quase uma tabela e continua legível. Escrito em uma linha só, vira um enigma. Regra: ternário para escolher um valor; if para executar blocos de instruções. Se você precisa fazer três coisas dentro de um ramo, o ternário é a ferramenta errada.

7.4 switch

Quando a decisão é sobre um único valor comparado com várias possibilidades exatas, o switch é mais claro que uma escada de else if:

JavaScript
const trilha = "web";

switch (trilha) {
  case "web":
    console.log("Laboratório 1 — Desenvolvimento Web");
    break;
  case "dados":
    console.log("Laboratório 2 — Ciência de Dados");
    break;
  case "seguranca":
    console.log("Sala 105 — Segurança");
    break;
  default:
    console.log("Local a definir");
}

Quatro pontos essenciais:

  • A comparação de cada case é estrita (===). case "3" não casa com o número 3.
  • break encerra o switch. Sem ele, a execução "vaza" para os casos seguintes e executa tudo até encontrar um break ou o fim do bloco.
  • default é o "senão" — pode ficar em qualquer posição, mas por convenção vai no fim.
  • O vazamento pode ser intencional: casos empilhados sem corpo compartilham o mesmo bloco.
JavaScript
const diaDaSemana = 6;

switch (diaDaSemana) {
  case 0:
  case 6:
    console.log("Fim de semana — campus fechado");
    break;
  case 1:
  case 2:
  case 3:
  case 4:
  case 5:
    console.log("Dia útil — evento das 19h às 22h");
    break;
  default:
    console.log("Dia inválido");
}

Um detalhe de escopo: as chaves do switch formam um único bloco, então declarar const dentro de um case afeta todos os outros. Se precisar de variáveis locais em um caso, embrulhe o corpo dele em chaves próprias:

JavaScript
const tipo = "minicurso";

switch (tipo) {
  case "minicurso": {
    const duracao = 4;
    console.log(`Minicurso de ${duracao} horas`);
    break;
  }
  case "palestra": {
    const duracao = 1;
    console.log(`Palestra de ${duracao} hora`);
    break;
  }
  default:
    console.log("Tipo desconhecido");
}

7.5 Qual usar?

Situação Ferramenta
Escolher um valor entre duas opções ternário
Executar blocos diferentes conforme faixas ou condições compostas if / else if / else
Comparar um valor com várias possibilidades exatas switch
Interromper cedo quando algo não faz sentido if no topo, com aviso no console

🔬 Investigue Cole no Console o switch do dia da semana acima, mas apague todos os break. Rode com diaDaSemana = 0 e observe: as três mensagens aparecem, uma atrás da outra, porque a execução vazou por todos os casos. Agora coloque o break só no primeiro caso e rode de novo com diaDaSemana = 1. Você acabou de reproduzir, de propósito, o bug mais difícil de enxergar em switch — e vai reconhecê-lo em um segundo quando ele acontecer sem querer.

💻 Mão na massa — A página de inscrição decide sozinha

Hoje o site da Semana Acadêmica de Sistemas de Informação ganha três comportamentos novos: o aviso automático de "Últimas vagas!", a taxa de inscrição formatada em reais e uma contagem regressiva na página inicial. Tudo com o que você acabou de aprender — nenhuma linha nova de manipulação da página além do textContent que você já usa desde a Aula 10.

Passo 1 — o parágrafo de aviso na página de inscrição

Abra inscricao.html e acrescente, logo abaixo da seção de vagas criada na Aula 10, um parágrafo vazio para o aviso e a linha da taxa.

site-evento/inscricao.html — trecho dentro do <main>, antes do formulário

HTML
<section class="vagas">
  <h2>Vagas</h2>
  <p>
    Restam <strong id="vagas-restantes"></strong> de
    <span id="vagas-totais"></span> vagas
    (<span id="percentual-ocupacao"></span>% ocupadas).
  </p>

  <p id="aviso-vagas" class="aviso" role="status"></p>

  <p class="taxa">
    Taxa de inscrição: <strong id="valor-taxa"></strong>
    <span id="observacao-taxa"></span>
  </p>
</section>

Repare que o aria-live="polite" saiu do <section class="vagas"> — apague-o do seu HTML. Ele estava ali desde a Aula 10, quando a seção inteira era o que mudava; agora quem muda é o parágrafo #aviso-vagas, e ele já tem role="status", que implica aria-live="polite". Duas regiões vivas aninhadas fazem o leitor de tela anunciar a mesma coisa duas vezes, ou anunciar a seção inteira (título, contagem e taxa) a cada mudança de uma palavra. Uma região viva, no menor elemento que muda — essa é a regra.

O parágrafo #aviso-vagas nasce vazio. Quando o script não tiver nada a dizer, ele continua vazio — e o CSS do Passo 6 esconde parágrafos vazios, então nada aparece na tela. O role="status" faz leitores de tela anunciarem a mensagem quando ela surgir, sem interromper o que o usuário está fazendo.

Passo 2 — as constantes e o cálculo

Substitua o conteúdo de js/inscricao.js pelo arquivo abaixo. As cinco declarações do topo são a única parte que a organização do evento precisa editar durante a semana — todo o resto se ajusta sozinho.

site-evento/js/inscricao.js

JavaScript
// inscricao.js — carregado apenas por inscricao.html

// ===== CONSTANTES DO EVENTO =====
const VAGAS_TOTAIS = 120;
const LIMITE_ULTIMAS_VAGAS = 20;   // a partir daqui, o aviso aparece
const TAXA_INSCRICAO = 25;         // em reais
const DESCONTO_ESTUDANTE = 0.5;    // 50%

let inscritos = 87;                // let: muda quando o formulário funcionar (Aula 14)

// ===== CÁLCULOS =====
// Math.max evita número negativo se alguém digitar mais inscritos que vagas
const vagasRestantes = Math.max(0, VAGAS_TOTAIS - inscritos);
const fracaoOcupada = inscritos / VAGAS_TOTAIS;
const percentualOcupacao = Math.min(100, fracaoOcupada * 100);
const taxaComDesconto = TAXA_INSCRICAO * (1 - DESCONTO_ESTUDANTE);

console.group("Vagas");
console.log("Totais:", VAGAS_TOTAIS);
console.log("Inscritos:", inscritos);
console.log("Restantes:", vagasRestantes);
console.log("Ocupação:", percentualOcupacao.toFixed(1), "%");
console.groupEnd();

Math.max(0, …) e Math.min(100, …) são a defesa contra dados inconsistentes: mesmo que alguém digite 130 inscritos, a página mostra 0 vagas e 100% de ocupação, em vez de "-10 vagas (108,3% ocupadas)".

Passo 3 — a decisão: qual aviso mostrar

Continue o mesmo arquivo. Esta é a parte que decide.

site-evento/js/inscricao.js — continuação

JavaScript
// ===== DECISÃO: qual mensagem o visitante deve ver =====
let mensagem = "";
let situacao = "";

if (vagasRestantes === 0) {
  situacao = "esgotado";
  mensagem = "Inscrições encerradas — todas as vagas foram preenchidas.";
} else if (vagasRestantes <= 5) {
  situacao = "critico";
  mensagem = `Corra! Restam apenas ${vagasRestantes} ${vagasRestantes === 1 ? "vaga" : "vagas"}.`;
} else if (vagasRestantes <= LIMITE_ULTIMAS_VAGAS) {
  situacao = "alerta";
  mensagem = `Últimas vagas! Menos de ${LIMITE_ULTIMAS_VAGAS} lugares disponíveis.`;
} else {
  situacao = "tranquilo";
  mensagem = "";
}

console.log("Situação:", situacao);

// ===== ESCRITA NA PÁGINA =====
document.querySelector("#vagas-restantes").textContent = vagasRestantes;
document.querySelector("#vagas-totais").textContent = VAGAS_TOTAIS;
document.querySelector("#percentual-ocupacao").textContent = percentualOcupacao.toFixed(1);
document.querySelector("#aviso-vagas").textContent = mensagem;

Repare na ordem dos testes: do caso mais restrito (=== 0) para o mais amplo (<= LIMITE_ULTIMAS_VAGAS). Se você invertesse as duas últimas condições, a mensagem "Corra!" nunca apareceria — porque vagasRestantes <= 20 também é verdadeira quando restam 3 vagas, e o primeiro teste verdadeiro vence.

O ternário dentro do template literal resolve o singular e o plural. Essa concordância é o tipo de detalhe que separa uma página feita com capricho de uma que mostra "Restam 1 vagas".

Passo 4 — a taxa formatada em reais

site-evento/js/inscricao.js — continuação

JavaScript
// ===== FORMATAÇÃO EM REAIS =====
const formatarReal = new Intl.NumberFormat("pt-BR", {
  style: "currency",
  currency: "BRL",
});

// Estudantes pagam metade; o aviso só aparece se houver desconto
const temDesconto = DESCONTO_ESTUDANTE > 0;

document.querySelector("#valor-taxa").textContent = formatarReal.format(TAXA_INSCRICAO);
document.querySelector("#observacao-taxa").textContent = temDesconto
  ? `(estudantes: ${formatarReal.format(taxaComDesconto)})`
  : "";

console.log("Taxa cheia:", formatarReal.format(TAXA_INSCRICAO));
console.log("Taxa com desconto:", formatarReal.format(taxaComDesconto));

Passo 5 — a contagem regressiva na página inicial

A contagem vive em js/app.js, que é carregado pelas cinco páginas. Só que o elemento da contagem existe apenas em index.html — nas outras páginas querySelector devolverá null, e tentar escrever em null derruba o script inteiro com Cannot read properties of null. A solução é um if: só escreve se o elemento existir.

site-evento/index.html — trecho dentro do herói do <main>

HTML
<p class="contagem" id="contagem-regressiva" role="status">Calculando…</p>

site-evento/js/app.js — acrescente ao fim do arquivo criado na Aula 10

JavaScript
// ===== CONTAGEM REGRESSIVA PARA A ABERTURA =====
// A data do evento é calculada a partir de hoje, para que o exemplo nunca
// fique no passado. No seu projeto autoral, troque o cálculo abaixo pela
// data real do seu evento: new Date(ano, mes - 1, dia) — lembre do mes - 1.
const DIAS_ATE_A_ABERTURA = 45;
const HORA_DE_ABERTURA = 19;
const MS_POR_DIA = 1000 * 60 * 60 * 24;

// Meia-noite de hoje: queremos contar dias de CALENDÁRIO, não horas corridas
const inicioDeHoje = new Date();
inicioDeHoje.setHours(0, 0, 0, 0);

// new Date(outraData) cria uma cópia; sem isso, alterar uma alteraria a outra
const dataDoEvento = new Date(inicioDeHoje);
dataDoEvento.setDate(inicioDeHoje.getDate() + DIAS_ATE_A_ABERTURA);

// Math.round, não Math.ceil: no dia em que o horário de verão muda, um "dia"
// tem 23 ou 25 horas, e a divisão dá 44,96 ou 45,04 — o round acerta os dois
const diasRestantes = Math.round((dataDoEvento - inicioDeHoje) / MS_POR_DIA);

const dataFormatada = dataDoEvento.toLocaleDateString("pt-BR", {
  weekday: "long",
  day: "2-digit",
  month: "long",
});

let textoContagem = "";

if (diasRestantes > 1) {
  textoContagem = `Faltam ${diasRestantes} dias — abertura ${dataFormatada}, às ${HORA_DE_ABERTURA}h.`;
} else if (diasRestantes === 1) {
  textoContagem = `É amanhã! Abertura ${dataFormatada}, às ${HORA_DE_ABERTURA}h.`;
} else if (diasRestantes === 0) {
  textoContagem = `É hoje! Abertura às ${HORA_DE_ABERTURA}h.`;
} else {
  textoContagem = "Esta edição já aconteceu. Obrigado a quem participou!";
}

// O elemento só existe em index.html — nas outras páginas, querySelector devolve null
const elementoContagem = document.querySelector("#contagem-regressiva");

if (elementoContagem !== null) {
  elementoContagem.textContent = textoContagem;
}

console.log("Dias restantes:", diasRestantes);

Esse if no fim é a lição prática da aula: um script compartilhado por várias páginas precisa verificar se o elemento existe antes de tocá-lo. Você poderia escrever a mesma verificação com curto-circuito (elementoContagem && (elementoContagem.textContent = textoContagem)), mas o if explícito é mais fácil de ler — e legibilidade vale mais que economia de caracteres.

Passo 6 — o estilo do aviso

site-evento/css/estilo.css — acrescente na seção de componentes

CSS
/* ===== Aviso de vagas (Aula 11) ===== */
.aviso {
  margin-top: var(--espaco-pequeno);
  padding: var(--espaco-pequeno) var(--espaco-medio);
  border-left: 4px solid #b42318;
  border-radius: var(--raio-borda);
  background: #fdf2f2;
  color: #8a1b12;
  font-weight: 600;
}

/* Se o script não escreveu nada, o parágrafo some da tela */
.aviso:empty {
  display: none;
}

.taxa {
  margin-top: var(--espaco-medio);
}

.contagem {
  display: inline-block;
  padding: var(--espaco-pequeno) var(--espaco-medio);
  border-radius: 999px;
  background: var(--cor-primaria);
  color: #fff;
  font-weight: 600;
}

A pseudoclasse :empty casa com elementos que não têm nenhum filho, nem texto. É a forma mais limpa de esconder um aviso que ainda não tem conteúdo, sem precisar de JavaScript para mexer em classes — isso fica para a Aula 13.

Use os nomes de variáveis do seu sistema de design da Aula 06; os acima seguem os da Mão na massa da Aula 07.

Como testar

  1. Abra inscricao.html no Live Server com inscritos = 87. A página mostra 33 de 120 vagas, 72.5% de ocupação, taxa R$ 25,00 com observação de R$ 12,50 — e nenhum aviso (33 é maior que o limite de 20).
  2. Troque para inscritos = 105, salve e recarregue: aparece a faixa vermelha com "Últimas vagas! Menos de 20 lugares disponíveis."
  3. Troque para inscritos = 119: a mensagem vira "Corra! Restam apenas 1 vaga." — no singular.
  4. Troque para inscritos = 130: a página mostra 0 vagas, 100.0% e "Inscrições encerradas". Nenhum número negativo aparece. Volte para 87.
  5. Abra index.html: o balão azul mostra "Faltam 45 dias — abertura …, às 19h.", com o dia da semana e o mês por extenso em português.
  6. Abra contato.html e olhe o Console: a linha "Dias restantes: 45" aparece e nenhum erro vermelho — porque o if protegeu o script na página que não tem o elemento da contagem.
  7. Comente o if (deixando só a atribuição) e recarregue contato.html: aparece Uncaught TypeError: Cannot read properties of null (reading 'textContent'). Desfaça.

Resultado esperado: a página de inscrição muda de comportamento sozinha ao trocar um único número, e a página inicial mostra a contagem sem que ninguém precise editar o HTML.

🧪 Laboratório

Alguns exercícios do Nível B pedem funções. A sintaxe completa é assunto da Aula 13; por ora, use o mesmo esqueleto da Aula 10:

JavaScript
function nomeDaFuncao(parametro) {
  const resultado = parametro * 2;
  return resultado;
}

console.log(nomeDaFuncao(10)); // 20

Nível A — Fixação

A1. Diferencie let, const e var quanto a três critérios: escopo, reatribuição e redeclaração. Escreva um exemplo curto de cada diferença.

A2. Por que const lista = [1, 2]; lista.push(3); funciona, mas lista = [3] produz Uncaught TypeError: Assignment to constant variable.? Explique com suas palavras o que exatamente const congela.

A3. Explique escopo de bloco com um exemplo que produza Uncaught ReferenceError: x is not defined. Depois altere o exemplo para produzir Uncaught ReferenceError: Cannot access 'x' before initialization e explique a diferença entre as duas mensagens.

A4. Escreva um ternário que atribua "Par" ou "Ímpar" a uma variável paridade, conforme o valor de n. Teste com n = 0, n = 7 e n = -4.

A5. Qual a diferença entre || e ??? Dê um caso concreto do site do evento em que os dois produzem resultados diferentes, e diga qual é o correto.

A6. Qual o resultado de 0.1 + 0.2 === 0.3? Explique a causa e escreva a forma correta de comparar esses dois valores.

A7. Qual a diferença entre % e /? Escreva a expressão que verifica se um número é par e a que descobre quantas horas completas há em 7300 segundos.

A8. Diferencie ++i de i++ com um exemplo em que os dois produzem saídas diferentes no console.log.

A9. Escreva a condicional encadeada que converte uma média em conceito: >= 9 vira A, >= 7 vira B, >= 6 vira C, o resto vira D. Depois reescreva com ternário encadeado e diga qual das duas versões você prefere e por quê.

A10. Quando switch é preferível a uma escada de else if? Por que o break é praticamente obrigatório, e em que situação omiti-lo é intencional?

A11. Anote sua previsão para cada linha e só depois teste no Console:

JavaScript
console.log(2 + 3 * 4);
console.log("10" < "9");
console.log(10 < 9);
console.log(0 || "padrão");
console.log(0 ?? "padrão");
console.log(null && "nunca");
console.log(Math.round(-4.5));
console.log(Math.trunc(-4.9));
console.log(5 % 3, -5 % 3);

A12. O trecho abaixo deveria mostrar "Últimas vagas", mas mostra sempre a mesma coisa, para qualquer valor. Encontre o erro sem rodar e explique por que a condição é sempre verdadeira.

JavaScript
const vagas = 500;

if (0 < vagas < 20) {
  console.log("Últimas vagas");
} else {
  console.log("Vagas à vontade");
}

Nível B — Aplicação

B1. Escreva validarSenha(senha), que devolva um objeto { valida: true, erros: [] } verificando quatro regras: mínimo de 8 caracteres, ao menos uma letra maiúscula, ao menos um dígito e ao menos um caractere que não seja letra nem dígito. Sem expressões regulares (elas são da Aula 14) — use os métodos de string da Aula 10 e comparações de caractere.

Resultado esperado: validarSenha("abc") devolve valida: false com três erros descritivos; validarSenha("Semana@Web7") devolve valida: true e lista vazia; cada erro é uma frase que o usuário entenderia, não um código.

Dica

Percorrer a string caractere a caractere é assunto da Aula 12, mas você não precisa disso: senha.toUpperCase() !== senha já indica que existe pelo menos uma minúscula, e senha.toLowerCase() !== senha indica uma maiúscula. Para o dígito, teste se "0123456789".includes(caractere) para os caracteres que você extrair com senha[0], senha.at(-1) e comparações. Monte a lista de erros com um array e push — a Aula 12 formaliza, mas const erros = []; erros.push("texto"); já funciona.

B2. Escreva converterTemperatura(valor, de, para) que aceite "C", "F" e "K" em qualquer combinação. Use switch para identificar a unidade de origem, converta tudo para Celsius internamente e depois para a unidade de destino. Se a unidade for inválida, devolva null e escreva um console.error explicando.

Resultado esperado: converterTemperatura(100, "C", "F") devolve 212; converterTemperatura(32, "F", "C") devolve 0; converterTemperatura(0, "C", "K") devolve 273.15; converterTemperatura(10, "X", "C") devolve null e registra um erro.

Dica

Converter tudo para uma unidade intermediária evita escrever nove fórmulas — bastam seis. As fórmulas: C = (F - 32) * 5 / 9, C = K - 273.15, F = C * 9 / 5 + 32, K = C + 273.15. Use de.toUpperCase() antes do switch para aceitar "c" e "C".

B3. Escreva classificarIMC(peso, altura) que calcule o índice de massa corporal (peso / altura ** 2) e devolva um objeto com o valor arredondado para uma casa e a faixa correspondente (abaixo do peso, adequado, sobrepeso, obesidade). Trate entradas inválidas: valores não numéricos, zero ou negativos devem devolver null com um aviso no console.

Resultado esperado: classificarIMC(70, 1.75) devolve algo como { imc: 22.9, faixa: "Peso adequado" }; classificarIMC(70, 0) devolve null; classificarIMC("setenta", 1.75) devolve null.

Dica

Comece pelas validações, com um if que devolve null logo no início — isso evita aninhar todo o resto do código dentro de um else. Number.isFinite(peso) é melhor que typeof peso === "number", porque NaN e Infinity também são do tipo number. Para arredondar em uma casa e continuar com um número (não uma string), use Math.round(imc * 10) / 10.

B4. Implemente calcularFrete(uf, peso, valorCompra) com regras encadeadas: frete grátis acima de R$ 200; base de R$ 15,00; acréscimo de R$ 10,00 para as UFs do Norte e Centro-Oeste; acréscimo de R$ 2,00 por quilo acima de 10 kg. Devolva o valor já formatado em reais com Intl.NumberFormat.

Resultado esperado: calcularFrete("SP", 5, 100) devolve "R$ 15,00"; calcularFrete("MT", 5, 100) devolve "R$ 25,00"; calcularFrete("MT", 14, 100) devolve "R$ 33,00"; calcularFrete("MT", 14, 250) devolve "R$ 0,00".

Dica

Teste a regra do frete grátis primeiro e devolva imediatamente — as outras regras não precisam ser avaliadas. Para as UFs, "AC AM AP PA RO RR TO DF GO MS MT".includes(uf.toUpperCase()) resolve sem array. O peso excedente é Math.max(0, peso - 10), o que dispensa um if.

B5. Escreva analisarInscricao(nome, idade, tipoIngresso) que valide os três campos e devolva um objeto com { valida, mensagem, valorDevido }. Regras: nome com pelo menos 3 caracteres depois do trim(); idade entre 14 e 120; tipoIngresso só pode ser "estudante", "profissional" ou "apoiador", com valores de R$ 12,50, R$ 25,00 e R$ 50,00. Menores de 18 anos recebem, além do resultado, um aviso de que precisam de autorização.

Resultado esperado: analisarInscricao("Ana", 17, "estudante") devolve valida: true, valorDevido: 12.5 e uma mensagem que menciona a autorização; analisarInscricao(" ", 30, "estudante") devolve valida: false com mensagem sobre o nome; analisarInscricao("Ana", 30, "vip") devolve valida: false com mensagem sobre o tipo de ingresso.

Dica

Use switch para o preço e if para as validações. O aviso de menor de idade é um bom uso do operador && dentro de um template literal: ${idade < 18 ? " Menores precisam de autorização." : ""}. Devolva sempre o mesmo formato de objeto, mesmo quando inválido — quem chama a função não deveria precisar checar se a propriedade existe.

B6. Reescreva o bloco de decisão do Passo 3 da Mão na massa usando apenas ternários encadeados, sem nenhum if. Depois escreva um parágrafo comparando as duas versões: qual é mais fácil de ler? Qual é mais fácil de alterar quando surgir uma quinta faixa?

Resultado esperado: dois arquivos (decisao-if.js e decisao-ternario.js) que produzem exatamente a mesma saída para os valores 0, 3, 15, 33 e 130 de inscritos, e um texto de comparação com pelo menos três argumentos.

Dica

Para que a versão com ternário produza duas variáveis (situacao e mensagem), você precisará de dois encadeamentos — ou de um objeto. Formate o encadeamento em coluna, uma condição por linha, como no exemplo da seção 7.3. Ao comparar, considere também o caso de um ramo precisar executar duas instruções: o que acontece com o ternário nessa hora?

Nível C — Desafio

C1. No seu projeto autoral, crie o equivalente ao painel de vagas do site do evento: um bloco de estado que muda sozinho conforme um único número que você edita no topo do script. Requisitos: pelo menos quatro faixas de decisão (com if/else if), um valor formatado com Intl.NumberFormat (preço, distância, peso — o que fizer sentido no seu domínio), uma data formatada com toLocaleDateString("pt-BR"), concordância correta de singular e plural com ternário, e nenhum número mágico no meio do código (todos os limites devem ser constantes nomeadas no topo).

Dica

Comece escrevendo, em comentários, as quatro faixas em português: "se não há nenhum, então…; se há menos de 5, então…". Só depois traduza para if. Teste cada faixa trocando o número e recarregando — se alguma nunca aparece, a ordem dos testes está errada. Lembre-se de usar Math.max(0, …) para nunca exibir um número negativo.

C2. Escreva js/relogio-evento.js que, ao carregar qualquer página, imprima no Console um único console.group chamado "Relógio do evento" com: a data e a hora atuais formatadas em pt-BR; quantos dias faltam para a abertura (usando a constante DIAS_ATE_A_ABERTURA); a fase do evento como texto ("pré-inscrições" acima de 30 dias, "inscrições abertas" entre 30 e 8, "reta final" entre 7 e 1, "acontecendo" em 0, "encerrado" abaixo de 0), decidida com switch (true); e o tempo restante quebrado em dias, horas e minutos com %.

Dica

switch (true) é uma forma pouco conhecida e perfeitamente válida: cada case recebe uma expressão booleana, e o primeiro case cujo valor for true é executado — case diasRestantes > 30:. Para a quebra em dias/horas/minutos, calcule a diferença total em milissegundos uma vez e vá dividindo: dias com Math.trunc(ms / MS_POR_DIA), horas com Math.trunc((ms % MS_POR_DIA) / MS_POR_HORA), e assim por diante.

🏆 Desafios

⭐ O sorteio honesto

javascriptinvestigacaoprojeto

Quantas vezes você precisa sortear um número de 1 a 6 até que os seis valores tenham saído pelo menos uma vez? A intuição diz "umas dez"; a matemática diz 14,7 em média; e o seu computador pode dizer a verdade em três segundos. Antes de aprender laços (isso é a Aula 12), você consegue investigar isso na mão: um script que sorteia uma vez por recarregamento, guarda o resultado e mostra a contagem. Use o sorteio da seção 3.3 e as condicionais de hoje para construir um mini-experimento honesto — e descubra, de quebra, se Math.random() é tão uniforme quanto promete.

Critérios de pronto

  • Um arquivo sorteio.js que, a cada recarregamento, sorteia um inteiro de 1 a 6 e imprime no Console o valor e um comentário decidido por switch (por exemplo, "número da sorte", "de novo esse não").
  • Uma versão com faixa configurável por duas constantes (MINIMO e MAXIMO) no topo, funcionando corretamente para 1..6, 0..9 e 10..20.
  • Uma verificação, com console.assert, de que o valor sorteado nunca sai da faixa — inclusive quando MINIMO === MAXIMO.
  • Um comentário de três linhas explicando, com suas palavras, por que a fórmula usa Math.floor e por que existe o + 1.
Pistas
  1. Troque Math.floor por Math.round na fórmula e recarregue trinta vezes anotando os resultados: os extremos saem com metade da frequência dos outros. Descubra por quê.
  2. Math.random() nunca devolve exatamente 1 — a documentação da MDN diz "de 0 (inclusivo) até 1 (exclusivo)". É essa exclusão que faz o Math.floor funcionar.
  3. Para guardar a contagem entre recarregamentos, uma linha basta: localStorage. Procure localStorage.getItem na MDN — a Aula 14 aprofunda o assunto.
⭐⭐

⭐⭐ Caça ao bug: o script que decide errado

javascriptbugdevtoolsrefatoracao

O script abaixo deveria classificar as inscrições do evento e mostrar o preço final. Ele roda sem nenhuma linha vermelha no Console — e mesmo assim está errado em cinco pontos: uma faixa nunca é alcançada, um desconto legítimo é ignorado, um preço aparece com quinze casas decimais, uma comparação é sempre verdadeira e um switch executa dois casos de uma vez. Nenhum erro é de sintaxe; todos são de lógica. Encontre os cinco usando o depurador da aba Sources, não console.log espalhado.

js/decisao-com-bug.js

JavaScript
const inscritos = 118;
const VAGAS_TOTAIS = 120;
const vagasRestantes = VAGAS_TOTAIS - inscritos;

const tipo = "estudante";
const cupomDigitado = 0;
const desconto = cupomDigitado || 0.1;

let situacao;

if (vagasRestantes <= 20) {
  situacao = "últimas vagas";
} else if (vagasRestantes <= 5) {
  situacao = "crítico";
} else if (vagasRestantes === 0) {
  situacao = "esgotado";
} else {
  situacao = "tranquilo";
}

let preco;

switch (tipo) {
  case "estudante":
    preco = 25 * 0.5;
  case "profissional":
    preco = 25;
    break;
  default:
    preco = 50;
}

const precoFinal = preco - preco * desconto;

if (0 < vagasRestantes < 10) {
  console.log("Poucas vagas!");
}

console.log(`Situação: ${situacao} | Preço final: R$ ${precoFinal}`);

Critérios de pronto

  • Um arquivo decisao-corrigida.js em que as quatro faixas funcionam: teste com inscritos valendo 0, 100, 115, 119 e 120 e mostre a saída de cada um.
  • O preço final aparece formatado com Intl.NumberFormat, sem casas decimais sobrando.
  • Um cupom de 0 (zero por cento de desconto) é respeitado como zero, e não substituído por 10%.
  • Um arquivo bugs.md com uma tabela de cinco linhas: sintoma observado, causa técnica e correção aplicada.
  • Pelo menos uma captura de tela da aba Sources com um breakpoint parado dentro da cadeia de if, mostrando o painel Scope.
Pistas
  1. Em uma escada de else if, o primeiro teste verdadeiro vence e os demais nem são lidos. Qual ordem faz cada faixa ser alcançável?
  2. Releia a tabela da seção 5.3: qual operador respeita o zero como valor legítimo?
  3. Um case sem break não termina o switch — ele continua executando o próximo. Qual valor sobra em preco no fim?
  4. 0 < x < 10 é avaliado em dois passos, e o resultado do primeiro é um booleano. Escreva a comparação do jeito certo.
  5. Preço em ponto flutuante multiplicado por porcentagem quase sempre gera dízima. A solução da seção 2.5 se aplica aqui.
⭐⭐⭐

⭐⭐⭐ A calculadora de cronograma do evento

javascriptinvestigacaoprojetoacessibilidade

A organização da Semana Acadêmica precisa saber, a qualquer momento, em que fase está: falta mais de um mês, está na reta final, é hoje, ou já acabou? E precisa que a página inicial diga isso em português correto — "falta 1 dia", "faltam 2 dias", "é hoje", "começa em 3 horas". Datas são a área do JavaScript onde mais gente escorrega: fuso horário, mês que começa em zero, mudança de mês, horário de verão. Construa um módulo de cronograma que acerte todos esses casos e prove que acerta.

Critérios de pronto

  • Um arquivo cronograma.js com as constantes do evento no topo (dias até a abertura, hora de início, duração em dias) e nenhum número mágico no restante do código.
  • Uma variável de texto que descreve a situação com concordância correta em seis casos: mais de um dia, exatamente um dia, hoje antes da abertura (mostrando horas e minutos), durante o evento, no último dia e depois do fim.
  • A quebra do tempo restante em dias, horas e minutos usando % e Math.trunc, sem nenhuma biblioteca externa.
  • A data de abertura exibida por extenso em português com toLocaleDateString("pt-BR", …), incluindo o dia da semana.
  • Uma seção de testes no fim do arquivo que força os seis casos alterando a constante de dias (inclusive valores negativos) e imprime o resultado de cada um com console.group.
  • O texto é escrito em um elemento com role="status", para que leitores de tela anunciem a mudança.
Pistas
  1. Comece pelo caso mais difícil: o dia da abertura. Nesse dia, diasRestantes é 0, mas ainda podem faltar horas — e a mensagem "faltam 0 dias" seria absurda.
  2. Para forçar os seis casos sem esperar o calendário, transforme a constante de dias em uma variável e escreva um bloco de testes que a altera e recalcula. É exatamente assim que se testa código dependente de tempo.
  3. Math.ceil e Math.trunc dão respostas diferentes para 0,3 dia restante. Decida qual você quer antes de escrever a condição, e escreva um comentário justificando.
  4. Consulte a página de Intl.DateTimeFormat na MDN e compare com toLocaleDateString: os dois aceitam as mesmas opções, e um deles é mais eficiente quando o formato se repete.
  5. Cuidado com o fuso: uma string ISO só com a data (no formato AAAA-MM-DD) é interpretada como UTC e pode "voltar um dia" no Brasil. Passar os números separados, new Date(ano, mes - 1, dia), evita a armadilha.

🐛 Erros comuns

Sintoma Causa Solução
Uncaught ReferenceError: Cannot access 'total' before initialization a variável let/const foi usada acima da linha que a declara — zona morta temporal mova a declaração para antes do primeiro uso; declare tudo no topo do bloco
Uncaught SyntaxError: Identifier 'vagas' has already been declared o mesmo nome foi declarado duas vezes com let/const no mesmo escopo apague a segunda declaração (provavelmente você queria apenas atribuir)
Uncaught TypeError: Assignment to constant variable. tentativa de reatribuir uma const se o valor muda ao longo do programa, declare com let
Uncaught TypeError: Cannot read properties of null (reading 'textContent') querySelector não achou o elemento: o id está diferente, ou a página atual não tem esse elemento proteja com if (elemento !== null); confira o id no HTML
Uncaught TypeError: Cannot read properties of undefined (reading 'email') leitura de uma propriedade dentro de um objeto que não existe use objeto.parte?.email quando a ausência for legítima
Uncaught SyntaxError: Unexpected token '??' ?? misturado com \|\| ou && sem parênteses escreva (a \|\| b) ?? c — a linguagem exige o parêntese
A página mostra NaN no lugar de um número uma conta usou uma string com letras, undefined ou um campo vazio converta com Number() antes de calcular e verifique com Number.isNaN()
O preço aparece como 22.500000000000004 multiplicação de decimais em ponto flutuante arredonde na exibição com Intl.NumberFormat, ou trabalhe com centavos inteiros
Uma faixa do else if nunca é alcançada as condições estão em ordem errada: uma condição mais ampla vem antes de uma mais restrita ordene do caso mais restrito para o mais amplo e teste todas as faixas
A condição é sempre verdadeira escreveu 0 < x < 20, que JavaScript avalia em dois passos, ou usou = no lugar de === escreva x > 0 && x < 20; use sempre === na comparação
O switch executa dois casos seguidos falta break no fim de um case — a execução "vaza" acrescente break em todos os casos, exceto onde o vazamento for intencional e comentado
Um zero legítimo foi substituído pelo valor padrão uso de \|\|, que trata 0 e "" como vazios troque por ??, que só reage a null e undefined
O texto sai "Restam 1 vagas" falta concordância de singular e plural use um ternário: ${n === 1 ? "vaga" : "vagas"}
Uncaught RangeError: toFixed() digits argument must be between 0 and 100 passou um número inválido para toFixed use de 0 a 100; para dinheiro, toFixed(2) ou Intl.NumberFormat
O mês exibido está um a menos do esperado getMonth() devolve 0 para janeiro some 1 ao exibir, ou use toLocaleDateString com a opção month

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). FLANAGAN, D. JavaScript: o guia definitivo, capítulos sobre expressões, operadores e instruções. Na MDN, leia "Expressões e operadores" e a página do operador de coalescência nula (links em Para aprofundar). Anote dois operadores que existem na MDN e não apareceram nesta aula.

Parte 2 — Produção (30 min). Produza os exercícios B1 (validarSenha) e B4 (calcularFrete) em arquivos .js comentados, cada um com pelo menos cinco casos de teste demonstrados no Console. No seu projeto autoral, produza o exercício C1: o bloco de estado que decide sozinho, com as constantes no topo, quatro faixas e formatação em pt-BR.

Critério de pronto: ao trocar um único número no topo do script do projeto autoral e recarregar, a mensagem e o destaque visual mudam de faixa; nenhuma página do projeto mostra erro vermelho no Console; nenhum número mágico aparece no meio do código; nenhum var e nenhum == sobreviveram nos arquivos .js.

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: defenda, com argumentos técnicos, a prática de declarar tudo como const por padrão e usar let só quando a reatribuição for necessária. Traga um trecho do seu próprio projeto em que a escolha importou.

Guarde no seu repositório: commit + push (ou a pasta do projeto).

✅ Checkpoint do projeto

Ao fim desta aula, o repositório do seu projeto autoral deve ter:

  • [ ] Todas as constantes do domínio declaradas em MAIUSCULO_COM_UNDERSCORE no topo do script, sem números mágicos espalhados pelo código.
  • [ ] Pelo menos uma cadeia if/else if/else com quatro faixas, todas alcançáveis (testadas uma a uma).
  • [ ] Pelo menos um ternário usado para concordância de singular e plural em um texto exibido na tela.
  • [ ] Pelo menos um switch com break em todos os casos e um default.
  • [ ] Um valor monetário (ou numérico) formatado com Intl.NumberFormat("pt-BR", …) e uma data formatada com toLocaleDateString("pt-BR", …).
  • [ ] Um elemento de aviso que aparece e desaparece conforme o estado, sem que o HTML precise ser editado (textContent vazio + :empty no CSS).
  • [ ] Um if (elemento !== null) protegendo qualquer script compartilhado por várias páginas.
  • [ ] Zero ocorrências de var e de == em todos os arquivos .js.
  • [ ] Nenhum erro vermelho no Console em nenhuma das páginas.

📚 Para aprofundar

Na próxima aula, o programa deixa de fazer uma coisa de cada vez: você vai aprender as estruturas de repetição (for, while, do…while, for…of), organizar informação em arrays e objetos e produzir os primeiros relatórios do evento no Console — totais por dia, busca por trilha e a lista de palestras que vai alimentar as páginas nas aulas seguintes.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Variáveis e escopo1.1 O que você já sabe (e o que falta)1.2 Por que var é um problema1.3 Hoisting e a zona morta temporal1.4 Sombreamento1.5 Constantes de verdade2. Operadores aritméticos2.1 Os sete operadores2.2 O resto (%) é mais útil do que parece2.3 Incremento, decremento e atribuição composta2.4 Precedência: quem faz a conta primeiro2.5 Onde a aritmética quebra3. O objeto Math3.1 Arredondamento3.2 O resto do arsenal3.3 Sorteio: a receita que você vai usar sempre4. Comparação4.1 Relacionais e igualdade4.2 Comparar strings compara letra por letra5. Operadores lógicos5.1 &&, || e !5.2 Curto-circuito: o que eles realmente devolvem5.3 ?? — o padrão que respeita o zero5.4 ?. — encadeamento opcional6. Formatando números e datas para o Brasil6.1 Intl.NumberFormat6.2 Datas com Date7. Estruturas condicionais7.1 if, else if, else7.2 Condições compostas7.3 O operador ternário7.4 switch7.5 Qual usar?💻 Mão na massa — A página de inscrição decide sozinhaPasso 1 — o parágrafo de aviso na página de inscriçãoPasso 2 — as constantes e o cálculoPasso 3 — a decisão: qual aviso mostrarPasso 4 — a taxa formatada em reaisPasso 5 — a contagem regressiva na página inicialPasso 6 — o estilo do avisoComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ O sorteio honesto⭐⭐ Caça ao bug: o script que decide errado⭐⭐⭐ A calculadora de cronograma do evento🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 3 · JavaScript e interatividade3 aulas de 50 min + 1 h EAD

Aula 12 — Estruturas sequenciais, condicionais e de repetição

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Distinguir as três estruturas fundamentais de um algoritmo — sequência, decisão e repetição — e reconhecê-las em qualquer programa.
  • Escolher entre for, while, do…while, for…of e for…in conforme o problema, e justificar a escolha.
  • Controlar a execução de um laço com break e continue, e reconhecer (e evitar) um laço infinito.
  • Criar, ler e modificar arrays, distinguindo os métodos que alteram o array original dos que devolvem um novo.
  • Criar e navegar objetos, inclusive aninhados, com notação de ponto, colchetes, desestruturação e Object.keys/values/entries.
  • Resolver problemas com forEach, map, filter, find, reduce, some, every e sort, encadeando-os quando fizer sentido.
  • Modelar o domínio do seu projeto como um array de objetos — a estrutura de dados mais comum da web.
  • Depurar um laço com breakpoints, breakpoints condicionais e o painel Scope do DevTools.

📋 Pré-requisitos

  • [ ] Site do evento com js/app.js (contagem regressiva) e js/inscricao.js (vagas e aviso automático) funcionando, sem erros no Console.
  • [ ] Console do navegador aberto e a aba Sources localizada — hoje ela sai do papel de coadjuvante.
  • [ ] Revisar da Aula 11: const/let e escopo de bloco, operadores aritméticos e lógicos, if/else if/else, ternário e switch.
  • [ ] Revisar da Aula 10: template literals, typeof, valores falsy e console.table.

Na aula passada o seu script aprendeu a decidir: a página de inscrição escolhe sozinha entre quatro mensagens e a página inicial calcula quantos dias faltam para o evento. Mas ele ainda trata um dado de cada vez — um número de inscritos, uma data, uma taxa. Um site de evento real tem doze atividades, seis palestrantes, três dias e quatro trilhas. Hoje você aprende a guardar coleções de informação em arrays e objetos e a percorrê-las com estruturas de repetição, produzindo relatórios completos no Console. Tudo ainda acontece no console — na próxima aula esses mesmos dados viram cartões na tela.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min As três estruturas de um algoritmo; for, while, do…while, for…of; break, continue e laços aninhados
2 50 min Arrays: criação, índices, métodos que alteram e métodos que devolvem novo; objetos e desestruturação
3 50 min Métodos de ordem superior; depuração de laços; Mão na massa: js/dados.js e os relatórios do evento

1. As três estruturas de um algoritmo

Em 1966, dois pesquisadores italianos, Corrado Böhm e Giuseppe Jacopini, provaram um resultado que organizou a programação para sempre: qualquer algoritmo computável pode ser escrito combinando apenas três estruturas.

Estrutura O que faz Em JavaScript
Sequência executa instruções, uma após a outra, na ordem escrita linhas soltas, uma embaixo da outra
Decisão escolhe entre caminhos conforme uma condição if, else if, else, ternário, switch
Repetição repete um trecho enquanto uma condição for verdadeira for, while, do…while, for…of, for…in

Você já domina as duas primeiras. A terceira é a que falta — e é a que muda a escala do que você consegue fazer: com repetição, o mesmo código que trata uma palestra trata mil.

1.1 Sequência: a ordem importa mais do que parece

JavaScript
// js/exemplos-sequencia.js
let inscritos = 87;
const novos = 5;

inscritos = inscritos + novos;      // 92
const percentual = inscritos / 120; // usa o valor JÁ atualizado
console.log(percentual.toFixed(2)); // "0.77"

Se as duas últimas linhas trocassem de lugar, o percentual seria calculado sobre 87 e ficaria errado — sem nenhum erro no Console. Esse é o tipo de bug que a estrutura sequencial esconde: o programa roda, o número aparece, e está errado.

O exemplo clássico de dependência de ordem é a troca de valores entre duas variáveis:

JavaScript
let primeiro = "Ana";
let segundo = "Bruno";

// ERRADO: o valor de primeiro se perde na primeira linha
// primeiro = segundo;
// segundo = primeiro;   // "Bruno" — os dois ficam iguais

// CERTO: uma variável temporária guarda o valor que seria perdido
let temporario = primeiro;
primeiro = segundo;
segundo = temporario;

console.log(primeiro, segundo); // "Bruno" "Ana"

Em JavaScript moderno existe um atalho — a desestruturação de array, que você vê na seção 5.6: [primeiro, segundo] = [segundo, primeiro]. Mas entender por que a variável temporária é necessária vale mais do que decorar o atalho.

2. Estruturas de repetição

2.1 for: quando você sabe quantas vezes

JavaScript
for (let i = 0; i < 5; i++) {
  console.log("Repetição número", i);
}
// 0, 1, 2, 3, 4

O cabeçalho tem três partes separadas por ponto e vírgula, e cada uma roda em um momento diferente:

Parte Quando executa Papel
let i = 0 uma única vez, antes de tudo inicialização do contador
i < 5 antes de cada repetição condição de continuação
i++ depois de cada repetição atualização do contador

Traduzindo para português: "comece com i valendo 0; enquanto i for menor que 5, execute o corpo; depois de cada execução, some 1 a i".

Duas convenções: o contador quase sempre se chama i (de índice), e ele quase sempre começa em zero — porque os índices de arrays começam em zero, como você verá na seção 3.

JavaScript
// Contagem regressiva: o contador pode decrescer
for (let i = 3; i > 0; i--) {
  console.log(i);
}
console.log("Começou!");
// 3, 2, 1, Começou!

// O passo não precisa ser 1
for (let i = 0; i <= 20; i += 5) {
  console.log(i); // 0, 5, 10, 15, 20
}

⚠️ Atenção Declare o contador com let, nunca com const: o i++ reatribui a variável, e com const você recebe Uncaught TypeError: Assignment to constant variable.. E declare dentro do for (for (let i = 0; …)), não antes: assim o i existe só dentro do laço e não polui o resto do arquivo.

2.2 while: quando você não sabe quantas vezes

JavaScript
let saldoVagas = 8;
let fila = 0;

while (saldoVagas > 0) {
  fila++;
  saldoVagas -= 2;   // cada grupo da fila ocupa 2 vagas
}

console.log("Grupos atendidos:", fila); // 4

O while testa antes de executar. Se a condição já começar falsa, o corpo não roda nenhuma vez. Use-o quando a quantidade de repetições depende de algo que muda dentro do laço.

2.3 do…while: pelo menos uma vez

JavaScript
let tentativas = 0;

do {
  tentativas++;
  console.log("Tentativa", tentativas);
} while (tentativas < 3);
// Tentativa 1, Tentativa 2, Tentativa 3

Aqui o teste vem depois, então o corpo executa pelo menos uma vez, mesmo com a condição falsa desde o início:

JavaScript
let contador = 100;

do {
  console.log("Executei mesmo assim");
} while (contador < 5);

É a escolha certa quando a ação precisa acontecer antes de haver o que testar: pedir um dado, sortear um valor, tentar uma conexão. Fora desses casos, o while comum é mais previsível.

2.4 for…of: percorrer valores

Quando você só quer os valores de uma coleção e não se importa com o índice, o for…of é mais curto e mais seguro:

JavaScript
const trilhas = ["Desenvolvimento Web", "Ciência de Dados", "Segurança", "Inteligência Artificial"];

for (const trilha of trilhas) {
  console.log(trilha);
}

Repare no const: cada repetição cria uma variável nova, então não há reatribuição — e o const funciona. Não há contador, não há length, não há como errar o limite.

for…of funciona com tudo que é iterável: arrays, strings, Map, Set e a lista de elementos que você vai obter do DOM na Aula 13.

JavaScript
for (const letra of "WebLab") {
  console.log(letra); // W, e, b, L, a, b
}

Se você precisar do índice e do valor ao mesmo tempo, use entries() com desestruturação:

JavaScript
const trilhas = ["Web", "Dados", "Segurança"];

for (const [indice, trilha] of trilhas.entries()) {
  console.log(`${indice + 1}. ${trilha}`);
}
// 1. Web
// 2. Dados
// 3. Segurança

2.5 for…in: percorrer chaves

O for…in percorre as chaves (nomes de propriedades) de um objeto:

JavaScript
const palestrante = { nome: "Ana Lúcia", instituicao: "Universidade Estadual", area: "ia" };

for (const chave in palestrante) {
  console.log(`${chave}: ${palestrante[chave]}`);
}
// nome: Ana Lúcia
// instituicao: Universidade Estadual
// area: ia

Repare que o valor é lido com palestrante[chave], notação de colchetes — porque o nome da propriedade está em uma variável. Escrever palestrante.chave procuraria uma propriedade literalmente chamada "chave", que não existe.

⚠️ Atenção Não use for…in em arrays. Ele percorre os índices como texto ("0", "1", "2") e também qualquer propriedade extra que alguém tenha adicionado ao array, em ordem não garantida. Para arrays use for…of (valores), for clássico (índices) ou forEach. A regra é fácil de guardar: in para objetos, of para arrays.

2.6 break e continue

JavaScript
const notas = [8, 6, 9, 4, 7, 10];

for (const nota of notas) {
  if (nota < 5) {
    continue;   // pula esta repetição e vai para a próxima
  }
  if (nota === 10) {
    break;      // encerra o laço inteiro
  }
  console.log("Nota válida:", nota);
}
// 8, 6, 9, 7 — o 4 foi pulado e o laço parou antes de imprimir o 10

continue pula o restante do corpo e segue para a próxima repetição. break abandona o laço imediatamente. Os dois deixam o código mais direto quando substituem um if gigante que envolve todo o corpo — mas dois ou três break espalhados no mesmo laço são sinal de que a lógica pede outra estrutura.

2.7 Laços infinitos

JavaScript
// NÃO EXECUTE: o navegador congela
// let i = 0;
// while (i < 10) {
//   console.log(i);   // faltou o i++: a condição nunca fica falsa
// }

Um laço infinito trava a aba inteira, porque o JavaScript do navegador roda em uma única linha de execução: enquanto o laço não termina, nada é desenhado e nenhum clique é atendido. As três causas mais comuns:

  1. Esquecer de atualizar a variável da condição (o caso acima).
  2. Atualizar na direção errada (i-- em um laço que testa i < 10).
  3. Comparar decimais com ===for (let x = 0; x !== 1; x += 0.1) nunca para, porque a soma nunca dá exatamente 1, como você viu na Aula 11.

Se travar: feche a aba (Ctrl+W) ou use o gerenciador de tarefas do navegador (Shift+Esc no Chrome). Depois, antes de rodar de novo, coloque um freio de segurança:

JavaScript
let i = 0;
let voltas = 0;
const LIMITE_DE_SEGURANCA = 1000;

while (i < 10) {
  voltas++;
  if (voltas > LIMITE_DE_SEGURANCA) {
    console.error("Laço passou do limite — provável laço infinito");
    break;
  }
  i++;
}

🧠 Você sabia? A pergunta "este programa vai parar algum dia?" tem nome próprio: problema da parada. Alan Turing provou, em 1936, que é impossível escrever um programa capaz de responder a essa pergunta para qualquer outro programa — não por falta de esforço ou de computador rápido, mas por impossibilidade lógica. É por isso que nem o navegador nem o VS Code conseguem avisar "você escreveu um laço infinito": eles só percebem que a página parou de responder e oferecem encerrá-la. A responsabilidade de garantir que o laço termina é, e sempre será, sua.

2.8 Laços aninhados

Um laço dentro de outro percorre duas dimensões. O de fora anda uma vez para cada volta completa do de dentro:

JavaScript
// Tabuada de 1 a 5
for (let tabuada = 1; tabuada <= 5; tabuada++) {
  console.group(`Tabuada do ${tabuada}`);
  for (let multiplicador = 1; multiplicador <= 10; multiplicador++) {
    console.log(`${tabuada} x ${multiplicador} = ${tabuada * multiplicador}`);
  }
  console.groupEnd();
}

São 5 × 10 = 50 execuções do corpo interno. Esse produto é o que torna laços aninhados perigosos: dois laços sobre uma lista de 1.000 itens dão 1.000.000 de execuções. Com três, um bilhão. Antes de aninhar, pergunte se não existe uma estrutura de dados que evite o segundo laço — no seu projeto, quase sempre existe.

Um uso legítimo e comum: agrupar por categoria, como a programação por dia que você vai montar na Mão na massa.

3. Arrays

Um array é uma lista ordenada de valores, guardada em uma única variável. É a estrutura mais usada em programação para a web.

3.1 Criando e lendo

JavaScript
const notas = [8.5, 6.0, 9.0, 4.5, 7.0];
const vazio = [];
const misturado = ["texto", 42, true, null];  // permitido, mas evite

console.log(notas[0]);              // 8.5  — o primeiro item tem índice 0
console.log(notas[4]);              // 7    — o último de cinco itens
console.log(notas[5]);              // undefined — não existe, e não dá erro
console.log(notas.length);          // 5    — quantidade de itens
console.log(notas[notas.length - 1]); // 7  — forma clássica de pegar o último
console.log(notas.at(-1));          // 7    — forma moderna: índice negativo conta do fim
console.log(notas.at(-2));          // 4.5

O detalhe que mais gera bug: o índice do último item é length - 1, não length. Um laço escrito com i <= notas.length lê uma posição a mais e devolve undefined — o famoso erro "por um" (off-by-one).

JavaScript
// Errado: lê o índice 5, que não existe
for (let i = 0; i <= notas.length; i++) {
  console.log(notas[i]);  // a última linha imprime undefined
}

// Certo
for (let i = 0; i < notas.length; i++) {
  console.log(notas[i]);
}

3.2 Métodos que alteram o array original

JavaScript
const fila = ["Ana", "Bruno", "Carla"];

fila.push("Diego");        // adiciona no fim   → ["Ana","Bruno","Carla","Diego"]
fila.pop();                // remove do fim e devolve "Diego"
fila.unshift("Zuleide");   // adiciona no início → ["Zuleide","Ana","Bruno","Carla"]
fila.shift();              // remove do início e devolve "Zuleide"

fila.splice(1, 1);         // remove 1 item a partir do índice 1 → ["Ana","Carla"]
fila.splice(1, 0, "Beto"); // insere "Beto" no índice 1 sem remover nada
console.log(fila);         // ["Ana", "Beto", "Carla"]

const numeros = [10, 9, 1, 25];
numeros.sort((a, b) => a - b);  // ordena numericamente → [1, 9, 10, 25]
numeros.reverse();               // inverte → [25, 10, 9, 1]
console.log(numeros);

push e pop funcionam no fim do array e são rápidos; shift e unshift funcionam no início e obrigam o motor a reposicionar todos os itens — em listas grandes, prefira o fim.

⚠️ Atenção sort() sem função comparadora converte tudo em texto antes de comparar: [10, 9, 1].sort() devolve [1, 10, 9], porque "10" vem antes de "9" na ordem de texto (você viu isso na Aula 11). Para números, passe sempre (a, b) => a - b (crescente) ou (a, b) => b - a (decrescente). Para textos em português, (a, b) => a.localeCompare(b, "pt-BR").

3.3 Métodos que devolvem um valor novo

Estes não tocam no array original — devolvem outra coisa:

JavaScript
const notas = [8.5, 6.0, 9.0, 4.5, 7.0];

console.log(notas.slice(1, 3));    // [6, 9] — do índice 1 até ANTES do 3
console.log(notas.slice(-2));      // [4.5, 7] — os dois últimos
console.log(notas.concat([10]));   // [8.5, 6, 9, 4.5, 7, 10]
console.log([...notas, 10]);       // idem, com o operador spread
console.log(notas.indexOf(9.0));   // 2 — o índice, ou -1 se não existir
console.log(notas.includes(9.0));  // true — só quer saber se existe
console.log(notas.join(" · "));    // "8.5 · 6 · 9 · 4.5 · 7" — vira string
console.log(notas);                // o original continua intacto

O operador spread (...) "espalha" os itens de um array. Ele é a forma moderna de copiar e juntar listas:

JavaScript
const dia1 = ["Abertura", "Git do zero"];
const dia2 = ["Acessibilidade", "Segurança"];

const tudo = [...dia1, ...dia2];       // junta as duas
const copia = [...dia1];               // cópia independente
const comExtra = [...dia1, "Extra"];   // cópia com um item a mais

copia.push("Só na cópia");
console.log(dia1);   // ["Abertura", "Git do zero"] — o original não mudou
console.log(tudo.length, comExtra.length); // 4 3

Isso importa por causa de uma característica da linguagem: arrays e objetos são guardados por referência. const copia = dia1 não cria uma cópia, cria um segundo nome para a mesma lista — mexer em um muda o outro. O spread resolve.

JavaScript
const original = [1, 2, 3];
const apelido = original;      // MESMA lista, dois nomes
const copiaDeVerdade = [...original];

apelido.push(4);
console.log(original);        // [1, 2, 3, 4] — mudou!
console.log(copiaDeVerdade);  // [1, 2, 3] — independente

🔬 Investigue Cole no Console: const a = [1, 2, 3]; const b = a; const c = [...a]; b.push(99);. Agora inspecione a, b e c. O array a tem o 99, mesmo você tendo alterado b. Faça o mesmo teste com um número: let x = 1; let y = x; y = 99; — aí x continua 1. Você acabou de ver a diferença entre valor (primitivos, copiados) e referência (arrays e objetos, compartilhados). Essa é a causa da maioria dos bugs "eu não mexi nessa lista!" que você vai encontrar no semestre.

4. Métodos de ordem superior

Os métodos a seguir recebem uma função como argumento e a aplicam a cada item. Eles substituem a maior parte dos for que você escreveria — com menos código e menos chance de erro de índice.

A sintaxe (item) => item.preco é uma arrow function, formalizada na Aula 13. Por enquanto, leia assim: "recebe item, devolve item.preco".

4.1 forEach — apenas percorrer

JavaScript
const trilhas = ["Web", "Dados", "Segurança"];

trilhas.forEach((trilha, indice) => {
  console.log(`${indice + 1}. ${trilha}`);
});

forEach não devolve nada (undefined). Use-o quando o objetivo é o efeito colateral: imprimir, somar em uma variável externa, desenhar na tela.

4.2 map — transformar

JavaScript
const precos = [25, 50, 12.5];

const comAumento = precos.map((preco) => preco * 1.1);
console.log(comAumento); // [27.500000000000004, 55.00000000000001, 13.750000000000002]
console.log(precos);     // [25, 50, 12.5] — intacto

const formatados = precos.map((preco) => `R$ ${preco.toFixed(2)}`);
console.log(formatados); // ["R$ 25.00", "R$ 50.00", "R$ 12.50"]

map devolve um array novo, do mesmo tamanho, com cada item transformado. É a ferramenta que, na Aula 13, transforma dados em cartões de HTML.

E olhe de novo para o primeiro resultado: 27.500000000000004, e não 27.5. É o ponto flutuante da Aula 11 aparecendo de novo — 1.1 não tem representação exata em binário, e o erro se propaga item a item. Por isso a segunda linha existe: toFixed(2) (ou o Intl.NumberFormat da Aula 11) é o que você mostra na tela; o número cru fica para as contas.

4.3 filter — selecionar

JavaScript
const notas = [8.5, 6.0, 9.0, 4.5, 7.0];

const aprovadas = notas.filter((nota) => nota >= 6);
console.log(aprovadas);        // [8.5, 6, 9, 7]
console.log(aprovadas.length); // 4

filter devolve um array novo com os itens que passaram no teste — de tamanho menor ou igual ao original. Se ninguém passar, devolve [] (array vazio, que é truthy: teste com length === 0, não com if (!lista)).

4.4 find e findIndex — achar o primeiro

JavaScript
const alunos = [
  { nome: "Ana", nota: 8.5 },
  { nome: "Bruno", nota: 5.0 },
  { nome: "Carla", nota: 9.0 },
];

const ana = alunos.find((aluno) => aluno.nome === "Ana");
console.log(ana);        // { nome: "Ana", nota: 8.5 }

const posicao = alunos.findIndex((aluno) => aluno.nota < 6);
console.log(posicao);    // 1

const ninguem = alunos.find((aluno) => aluno.nota === 10);
console.log(ninguem);    // undefined — sempre teste antes de usar!

find devolve o item (ou undefined); findIndex devolve o índice (ou -1). Confundir os dois é erro comum: if (alunos.findIndex(…)) é sempre verdadeiro quando o índice é diferente de 0, e falso quando o item achado está na primeira posição.

4.5 reduce — condensar em um valor

JavaScript
const notas = [8.5, 6.0, 9.0, 4.5, 7.0];

const soma = notas.reduce((acumulado, nota) => acumulado + nota, 0);
console.log(soma);               // 35
console.log(soma / notas.length); // 7 — a média

reduce é o mais poderoso e o mais confuso à primeira vista. Ele recebe dois argumentos: a função e o valor inicial do acumulador (o 0 no fim). A cada volta, a função recebe o acumulado até agora e o item atual, e devolve o novo acumulado.

Acompanhe passo a passo:

Texto
início:  acumulado = 0
item 8.5: acumulado = 0 + 8.5   = 8.5
item 6.0: acumulado = 8.5 + 6   = 14.5
item 9.0: acumulado = 14.5 + 9  = 23.5
item 4.5: acumulado = 23.5 + 4.5 = 28
item 7.0: acumulado = 28 + 7    = 35
resultado: 35

O acumulador não precisa ser número — pode ser texto, array ou objeto. Contar ocorrências, por exemplo:

JavaScript
const areas = ["web", "dados", "web", "ia", "web"];

const contagem = areas.reduce((conta, area) => {
  conta[area] = (conta[area] ?? 0) + 1;
  return conta;
}, {});

console.log(contagem); // { web: 3, dados: 1, ia: 1 }

Repare no ?? 0 da Aula 11: na primeira vez que uma área aparece, conta[area] é undefined, e o ?? o substitui por zero.

⚠️ Atenção Sempre passe o valor inicial do reduce. Sem ele, o primeiro item vira o acumulador — o que quebra quando o array está vazio (Uncaught TypeError: Reduce of empty array with no initial value) e produz resultados errados quando o acumulador tem tipo diferente dos itens.

4.6 some e every — perguntas de sim ou não

JavaScript
const notas = [8.5, 6.0, 9.0, 4.5];

console.log(notas.some((n) => n === 10));  // false — algum é 10?
console.log(notas.some((n) => n < 5));      // true  — algum está abaixo de 5?
console.log(notas.every((n) => n >= 4));    // true  — todos são >= 4?
console.log(notas.every((n) => n >= 6));    // false

4.7 Encadeamento

Como map e filter devolvem arrays, você pode ligá-los em sequência. Leia sempre da esquerda para a direita, como uma frase:

JavaScript
const alunos = [
  { nome: "Ana", nota: 8.5 },
  { nome: "Bruno", nota: 5.0 },
  { nome: "Carla", nota: 9.0 },
  { nome: "Diego", nota: 3.5 },
];

const aprovadosEmOrdem = alunos
  .filter((aluno) => aluno.nota >= 6)          // fica só quem passou
  .map((aluno) => aluno.nome)                   // vira lista de nomes
  .sort((a, b) => a.localeCompare(b, "pt-BR")) // ordena em português
  .join(", ");                                  // vira uma string

console.log(aprovadosEmOrdem); // "Ana, Carla"

Uma armadilha escondida nesse encadeamento: sort altera o array em que atua. Aqui isso é seguro, porque o array veio de map e é descartável. Mas alunos.sort(…) bagunçaria a ordem original dos seus dados para sempre. Quando quiser ordenar sem estragar o original, ordene uma cópia: [...alunos].sort(…).

4.8 Laço ou método?

Situação Ferramenta
Transformar cada item em outra coisa map
Selecionar alguns itens filter
Achar um item específico find
Somar, contar ou agrupar reduce
Só executar algo para cada item forEach ou for…of
Precisa parar no meio (break) for ou for…offorEach não pode parar
Precisa do índice e da lógica é complexa for clássico

📌 Vale gravar map transforma e devolve um array do mesmo tamanho; filter seleciona e devolve um array de tamanho menor ou igual; forEach percorre e devolve undefined. Usar map só para imprimir no console é erro de intenção — o array criado é jogado fora. Saiba dizer o que cada um devolve.

5. Objetos

Um array guarda uma lista de coisas parecidas. Um objeto guarda as características de uma coisa só, cada uma com um nome.

5.1 Criando e lendo

JavaScript
const palestrante = {
  nome: "Ana Lúcia Ferreira",
  instituicao: "Universidade Estadual — Sinop",
  area: "ia",
  confirmado: true,
  temas: ["redes neurais", "agricultura de precisão"],
  contato: {
    email: "ana.ferreira@exemplo.edu.br",
    cidade: "Sinop",
  },
};

console.log(palestrante.nome);            // "Ana Lúcia Ferreira" — notação de ponto
console.log(palestrante["instituicao"]);  // notação de colchetes
console.log(palestrante.contato.cidade);  // "Sinop" — objeto aninhado
console.log(palestrante.temas[0]);        // "redes neurais" — array dentro de objeto
console.log(palestrante.telefone);        // undefined — propriedade inexistente

Cada par é uma propriedade: um nome (a chave) e um valor. O valor pode ser qualquer coisa — número, texto, booleano, array, outro objeto.

Quando usar colchetes em vez de ponto? Em dois casos: quando o nome da propriedade está em uma variável (como no for…in) e quando o nome tem espaços ou caracteres especiais.

JavaScript
const campo = "area";
console.log(palestrante[campo]);   // "ia" — lê a propriedade cujo nome está na variável
console.log(palestrante.campo);    // undefined — procura uma propriedade chamada "campo"

5.2 Alterando

JavaScript
palestrante.confirmado = false;                  // altera
palestrante.telefone = "(66) 99999-0000";        // adiciona
delete palestrante.telefone;                     // remove

console.log("nome" in palestrante);              // true  — a propriedade existe?
console.log("telefone" in palestrante);          // false

Repare que tudo isso funciona mesmo com palestrante declarado como const: como você viu na Aula 10, const congela a ligação entre o nome e o objeto, não o conteúdo do objeto.

5.3 Percorrendo um objeto

JavaScript
const palestrante = { nome: "Ana Lúcia", instituicao: "Universidade Estadual", area: "ia" };

// Com for...in
for (const chave in palestrante) {
  console.log(chave, "→", palestrante[chave]);
}

// Com os utilitários de Object
console.log(Object.keys(palestrante));    // ["nome", "instituicao", "area"]
console.log(Object.values(palestrante));  // ["Ana Lúcia", "Universidade Estadual", "ia"]
console.log(Object.entries(palestrante)); // [["nome","Ana Lúcia"], ["instituicao","Universidade Estadual"], ["area","ia"]]

// Object.entries + for...of + desestruturação: o padrão mais legível
for (const [chave, valor] of Object.entries(palestrante)) {
  console.log(`${chave}: ${valor}`);
}

Object.keys, values e entries devolvem arrays — o que significa que todos os métodos da seção 4 funcionam com eles. É assim que se filtra ou ordena as propriedades de um objeto.

5.4 Copiando e mesclando

JavaScript
const base = { nome: "Ana", area: "ia", confirmado: false };

const copia = { ...base };                       // cópia independente
const atualizado = { ...base, confirmado: true };  // cópia com uma alteração
const comExtra = { ...base, sala: "Lab 3" };       // cópia com propriedade nova

console.log(atualizado); // { nome: "Ana", area: "ia", confirmado: true }
console.log(base.confirmado); // false — o original não mudou

A ordem importa: propriedades escritas depois do spread sobrescrevem as que vieram dele. Esse padrão — copiar e alterar em vez de modificar no lugar — é a base do funcionamento de Vue e React, que você verá no Nível 3.

🔎 Por baixo do capô O spread faz uma cópia rasa (shallow): as propriedades de primeiro nível são copiadas, mas objetos aninhados continuam sendo a mesma referência. Em const copia = { ...palestrante }, alterar copia.nome não afeta o original, mas alterar copia.contato.cidade afeta, porque contato é o mesmo objeto nos dois. Para uma cópia profunda de dados simples, existe structuredClone(objeto), disponível em todos os navegadores modernos.

5.5 Objetos com funções dentro

Uma propriedade cujo valor é uma função chama-se método. Dentro dele, this aponta para o próprio objeto:

JavaScript
const turma = {
  nome: "Turma A",
  notas: [8.5, 7.0, 9.0],
  calcularMedia() {
    return this.notas.reduce((soma, n) => soma + n, 0) / this.notas.length;
  },
};

console.log(turma.calcularMedia().toFixed(2)); // "8.17"

this é um assunto grande, com armadilhas próprias — a Aula 13 volta a ele ao falar de arrow functions. Por ora, guarde a forma: dentro de um método, this.propriedade acessa o próprio objeto.

5.6 Desestruturação

Desestruturar é extrair propriedades para variáveis soltas, em uma linha:

JavaScript
const palestrante = {
  nome: "Ana Lúcia Ferreira",
  area: "ia",
  contato: { email: "ana@exemplo.edu.br", cidade: "Sinop" },
};

// Sem desestruturação
const nomeAntigo = palestrante.nome;
const areaAntiga = palestrante.area;

// Com desestruturação
const { nome, area } = palestrante;
console.log(nome, area);

// Renomeando e com valor padrão
const { nome: nomeCompleto, telefone = "não informado" } = palestrante;
console.log(nomeCompleto, telefone); // "Ana Lúcia Ferreira" "não informado"

// Aninhada
const { contato: { cidade } } = palestrante;
console.log(cidade); // "Sinop"

// Em arrays, a posição é que manda
const [primeiraNota, segundaNota] = [8.5, 7.0, 9.0];
console.log(primeiraNota, segundaNota); // 8.5 7

// A troca de valores da seção 1.1, agora em uma linha
let a = "Ana";
let b = "Bruno";
[a, b] = [b, a];
console.log(a, b); // "Bruno" "Ana"

Você vai reencontrar a desestruturação em toda parte a partir da Aula 13 — é a forma padrão de receber parâmetros de função e de ler dados de uma API.

6. Array de objetos: a estrutura da web

Junte as duas ideias e você tem a estrutura de dados mais comum do desenvolvimento web: uma lista de registros, cada um com as mesmas propriedades. É o formato de uma tabela de banco de dados, de uma resposta de API em JSON e da lista de produtos de qualquer loja.

JavaScript
const produtos = [
  { id: 1, nome: "Notebook", preco: 3500, categoria: "informatica", estoque: 12 },
  { id: 2, nome: "Mouse", preco: 80, categoria: "informatica", estoque: 0 },
  { id: 3, nome: "Caderno", preco: 25, categoria: "papelaria", estoque: 40 },
];

// Quais estão disponíveis?
const disponiveis = produtos.filter((p) => p.estoque > 0);
console.log(disponiveis.length); // 2

// Quanto vale o estoque inteiro?
const valorTotal = produtos.reduce((total, p) => total + p.preco * p.estoque, 0);
console.log(valorTotal); // 43000

// Do mais barato ao mais caro (sem estragar o original)
const porPreco = [...produtos].sort((a, b) => a.preco - b.preco);
console.log(porPreco.map((p) => p.nome)); // ["Caderno", "Mouse", "Notebook"]

// Achar pelo identificador
const item = produtos.find((p) => p.id === 3);
console.log(item.nome); // "Caderno"

// Busca por nome parcial, ignorando maiúsculas
const termo = "note";
const achados = produtos.filter((p) => p.nome.toLowerCase().includes(termo.toLowerCase()));
console.log(achados.map((p) => p.nome)); // ["Notebook"]

Três convenções que você deve adotar desde já:

  1. Todo registro tem um id único e estável. É por ele que você acha, atualiza e remove — nunca pela posição no array, que muda a cada ordenação.
  2. Todos os registros têm as mesmas propriedades. Se um item não tem valor para uma delas, use null ou "", não omita a propriedade — assim map e filter nunca encontram undefined inesperado.
  3. Valores de categoria são códigos curtos e sem acento ("ia", "web"), com o texto bonito ("Inteligência Artificial") ficando na exibição. Isso evita comparar strings com acento e maiúscula, e é exatamente o que os filtros da Aula 13 vão precisar.

7. Depurando laços no DevTools

console.log dentro de um laço que roda 200 vezes produz 200 linhas — e você perde a linha que importa. A aba Sources resolve isso.

7.1 Breakpoint simples

  1. Abra o DevTools (F12) e vá à aba Sources.
  2. Na árvore de arquivos à esquerda, abra o seu .js.
  3. Clique no número da linha dentro do laço: aparece um marcador azul.
  4. Recarregue a página. A execução para ali, com a linha destacada.
  5. No painel Scope, à direita, veja o valor de cada variável naquele instante — inclusive o contador do laço.
  6. Use os botões do topo: F10 executa a próxima linha, F11 entra em uma função, F8 continua até o próximo breakpoint.

7.2 Breakpoint condicional — o truque que economiza a aula

Parar 200 vezes é inútil. Clique com o botão direito no número da linha e escolha Add conditional breakpoint. Digite uma condição, por exemplo i === 47 ou palestra.inscritos > palestra.vagas. A execução só para quando a condição for verdadeira — e você chega direto no caso problemático.

Há ainda o logpoint (botão direito → Add logpoint): ele imprime uma mensagem sem parar a execução e sem sujar o seu código com console.log que você depois esquece de remover.

7.3 Watch

No painel Watch, clique em + e digite uma expressão — palestras.length, total / contador, palestra.vagas - palestra.inscritos. O DevTools recalcula e mostra o valor a cada passo. É a forma mais rápida de descobrir em qual volta a conta começou a errar.

🔬 Investigue Crie um arquivo com o laço for (let i = 0; i < 100; i++) { const quadrado = i * i; } e ponha um breakpoint condicional em i === 42. Recarregue: a execução para na quadragésima terceira volta e o painel Scope mostra i: 42. Pressione F10 uma vez e veja quadrado aparecer valendo 1764. Agora adicione i * 2 ao painel Watch e pressione F8 — o breakpoint não dispara de novo, porque a condição só é verdadeira uma vez. Compare o esforço disso com o de encontrar a mesma informação em cem linhas de console.log.

💻 Mão na massa — Os dados do evento e os relatórios no Console

Até agora, cada informação do site da Semana Acadêmica de Sistemas de Informação estava digitada no HTML, uma por uma. Hoje você cria a fonte única de dados do projeto — um arquivo com dois arrays de objetos — e escreve os primeiros relatórios sobre eles. Na Aula 13 esses mesmos dados deixam o console e viram cartões na tela.

Passo 1 — criar js/dados.js e carregá-lo antes dos outros

Crie o arquivo js/dados.js e inclua-o no <head> das cinco páginas, antes de js/app.js. A ordem importa: com defer, os scripts executam na ordem em que aparecem no HTML, e o app.js precisa que os dados já existam.

site-evento/index.html — trecho do <head> (repita nas cinco páginas)

HTML
<link rel="stylesheet" href="css/estilo.css">
<script src="js/dados.js" defer></script>
<script src="js/app.js" defer></script>

Na página de inscrição, o js/inscricao.js continua sendo o último da lista:

site-evento/inscricao.html — trecho do <head>

HTML
<link rel="stylesheet" href="css/estilo.css">
<script src="js/dados.js" defer></script>
<script src="js/app.js" defer></script>
<script src="js/inscricao.js" defer></script>

Estrutura de pastas resultante:

Texto
site-evento/
├── index.html
├── programacao.html
├── inscricao.html
├── palestrantes.html
├── contato.html
├── css/
│   └── estilo.css
├── img/
└── js/
    ├── dados.js
    ├── app.js
    ├── inscricao.js
    └── relatorios.js

Passo 2 — os palestrantes

site-evento/js/dados.js

JavaScript
// dados.js — fonte única de dados do site do evento.
// Carregado antes de todos os outros scripts, em todas as páginas.
// A partir da Aula 13, estas listas alimentam as páginas de verdade.

const palestrantes = [
  {
    id: 1,
    nome: "Ana Lúcia Ferreira",
    instituicao: "Universidade Estadual — Sinop",
    area: "ia",
    tema: "Redes neurais para prever a safra de soja",
    foto: "img/palestrante-01.jpg",
  },
  {
    id: 2,
    nome: "Bruno Takahashi",
    instituicao: "Startup AgroData",
    area: "dados",
    tema: "Dashboards que os produtores realmente usam",
    foto: "img/palestrante-02.jpg",
  },
  {
    id: 3,
    nome: "Carla Mendes",
    instituicao: "UFMT",
    area: "seguranca",
    tema: "O que um ataque de phishing ensina sobre UX",
    foto: "img/palestrante-03.jpg",
  },
  {
    id: 4,
    nome: "Diego Nascimento",
    instituicao: "Prefeitura de Sinop",
    area: "web",
    tema: "Acessibilidade em portais públicos: erros que vimos",
    foto: "img/palestrante-04.jpg",
  },
  {
    id: 5,
    nome: "Eduarda Ribeiro",
    instituicao: "Universidade Estadual — Sinop",
    area: "web",
    tema: "Do HTML ao deploy: o caminho do estudante",
    foto: "img/palestrante-05.jpg",
  },
  {
    id: 6,
    nome: "Felipe Arruda",
    instituicao: "Cooperativa Coopercana",
    area: "ia",
    tema: "Visão computacional no controle de pragas",
    foto: "img/palestrante-06.jpg",
  },
];

Repare que area guarda um código curto e sem acento. O nome bonito de cada área vive em um objeto separado, que funciona como um dicionário de tradução:

site-evento/js/dados.js — continuação

JavaScript
// Dicionário de áreas: código → nome de exibição
const nomesDasAreas = {
  web: "Desenvolvimento Web",
  dados: "Ciência de Dados",
  seguranca: "Segurança",
  ia: "Inteligência Artificial",
};

Passo 3 — a programação

site-evento/js/dados.js — continuação

JavaScript
const palestras = [
  {
    id: 1,
    titulo: "Abertura e palestra magna: o futuro do desenvolvimento web",
    tipo: "palestra",
    area: "web",
    dia: 1,
    hora: "19:00",
    local: "Auditório Central",
    vagas: 300,
    inscritos: 212,
    palestranteId: 5,
  },
  {
    id: 2,
    titulo: "Minicurso: Git e GitHub do zero",
    tipo: "minicurso",
    area: "web",
    dia: 1,
    hora: "20:00",
    local: "Laboratório 2",
    vagas: 30,
    inscritos: 30,
    palestranteId: 4,
  },
  {
    id: 3,
    titulo: "Mesa-redonda: mercado de trabalho em Sinop",
    tipo: "mesa",
    area: "web",
    dia: 1,
    hora: "20:00",
    local: "Sala 105",
    vagas: 80,
    inscritos: 47,
    palestranteId: 5,
  },
  {
    id: 4,
    titulo: "Dashboards que os produtores realmente usam",
    tipo: "palestra",
    area: "dados",
    dia: 1,
    hora: "21:00",
    local: "Auditório Central",
    vagas: 300,
    inscritos: 96,
    palestranteId: 2,
  },
  {
    id: 5,
    titulo: "Minicurso: acessibilidade na prática",
    tipo: "minicurso",
    area: "web",
    dia: 2,
    hora: "19:00",
    local: "Laboratório 1",
    vagas: 30,
    inscritos: 28,
    palestranteId: 4,
  },
  {
    id: 6,
    titulo: "Minicurso: primeiros passos com redes neurais",
    tipo: "minicurso",
    area: "ia",
    dia: 2,
    hora: "19:00",
    local: "Laboratório 3",
    vagas: 25,
    inscritos: 25,
    palestranteId: 1,
  },
  {
    id: 7,
    titulo: "Segurança em aplicações web: dez erros comuns",
    tipo: "palestra",
    area: "seguranca",
    dia: 2,
    hora: "20:30",
    local: "Auditório Central",
    vagas: 300,
    inscritos: 154,
    palestranteId: 3,
  },
  {
    id: 8,
    titulo: "Visão computacional no controle de pragas",
    tipo: "palestra",
    area: "ia",
    dia: 2,
    hora: "21:00",
    local: "Sala 105",
    vagas: 80,
    inscritos: 61,
    palestranteId: 6,
  },
  {
    id: 9,
    titulo: "Maratona de programação",
    tipo: "maratona",
    area: "web",
    dia: 3,
    hora: "18:30",
    local: "Laboratórios 1 e 2",
    vagas: 60,
    inscritos: 45,
    palestranteId: 5,
  },
  {
    id: 10,
    titulo: "Minicurso: phishing e engenharia social",
    tipo: "minicurso",
    area: "seguranca",
    dia: 3,
    hora: "19:00",
    local: "Laboratório 3",
    vagas: 25,
    inscritos: 19,
    palestranteId: 3,
  },
  {
    id: 11,
    titulo: "Dados abertos e cidades inteligentes",
    tipo: "palestra",
    area: "dados",
    dia: 3,
    hora: "20:00",
    local: "Auditório Central",
    vagas: 300,
    inscritos: 88,
    palestranteId: 2,
  },
  {
    id: 12,
    titulo: "Encerramento e premiação",
    tipo: "palestra",
    area: "web",
    dia: 3,
    hora: "22:00",
    local: "Auditório Central",
    vagas: 300,
    inscritos: 130,
    palestranteId: 5,
  },
];

console.log(`dados.js carregado: ${palestras.length} atividades, ${palestrantes.length} palestrantes.`);

O campo palestranteId é uma referência: em vez de repetir nome, instituição e foto em cada palestra, guardamos só o identificador e buscamos o resto quando precisar. É exatamente o que uma chave estrangeira faz em um banco de dados, assunto do Nível 2.

Passo 4 — os relatórios

Crie js/relatorios.js e inclua-o só em programacao.html, depois dos outros scripts. Ele não altera a página: escreve tudo no Console.

site-evento/programacao.html — trecho do <head>

HTML
<link rel="stylesheet" href="css/estilo.css">
<script src="js/dados.js" defer></script>
<script src="js/app.js" defer></script>
<script src="js/relatorios.js" defer></script>

site-evento/js/relatorios.js

JavaScript
// relatorios.js — relatórios do evento no Console.
// Depende de js/dados.js, que precisa ser carregado antes.

const DIAS_DO_EVENTO = 3;

const formatarPercentual = new Intl.NumberFormat("pt-BR", {
  style: "percent",
  maximumFractionDigits: 1,
});

// ===== 1. A programação completa em tabela =====
console.group("Programação completa");
console.table(palestras, ["dia", "hora", "titulo", "local"]);
console.groupEnd();

// ===== 2. Totais gerais (reduce) =====
const totalVagas = palestras.reduce((soma, p) => soma + p.vagas, 0);
const totalInscritos = palestras.reduce((soma, p) => soma + p.inscritos, 0);

console.group("Totais gerais");
console.log("Atividades:", palestras.length);
console.log("Vagas oferecidas:", totalVagas);
console.log("Inscrições feitas:", totalInscritos);
console.log("Ocupação geral:", formatarPercentual.format(totalInscritos / totalVagas));
console.groupEnd();

// ===== 3. Resumo por dia (laço aninhado: dias × atividades) =====
console.group("Resumo por dia");
for (let dia = 1; dia <= DIAS_DO_EVENTO; dia++) {
  const doDia = palestras.filter((p) => p.dia === dia);
  const inscritosDoDia = doDia.reduce((soma, p) => soma + p.inscritos, 0);

  console.group(`Dia ${dia}${doDia.length} atividades, ${inscritosDoDia} inscrições`);
  for (const palestra of doDia) {
    console.log(`${palestra.hora} · ${palestra.titulo} (${palestra.local})`);
  }
  console.groupEnd();
}
console.groupEnd();

// ===== 4. Contagem por área (reduce com objeto acumulador) =====
const porArea = palestras.reduce((conta, p) => {
  conta[p.area] = (conta[p.area] ?? 0) + 1;
  return conta;
}, {});

console.group("Atividades por área");
for (const [codigo, quantidade] of Object.entries(porArea)) {
  console.log(`${nomesDasAreas[codigo]}: ${quantidade}`);
}
console.groupEnd();

// ===== 5. Atividades esgotadas e as mais concorridas =====
const esgotadas = palestras.filter((p) => p.inscritos >= p.vagas);
const maisConcorrida = palestras.reduce((maior, p) =>
  p.inscritos / p.vagas > maior.inscritos / maior.vagas ? p : maior
);

console.group("Alertas");
console.log("Esgotadas:", esgotadas.map((p) => p.titulo));
console.log("Mais concorrida:", maisConcorrida.titulo,
  formatarPercentual.format(maisConcorrida.inscritos / maisConcorrida.vagas));
console.log("Alguma atividade sem inscritos?", palestras.some((p) => p.inscritos === 0));
console.log("Todas têm local definido?", palestras.every((p) => p.local !== ""));
console.groupEnd();

// ===== 6. Busca por trilha (filter + includes) =====
const trilhaProcurada = "seguranca";
const daTrilha = palestras.filter((p) => p.area === trilhaProcurada);

console.group(`Trilha: ${nomesDasAreas[trilhaProcurada]}`);
if (daTrilha.length === 0) {
  console.log("Nenhuma atividade nesta trilha.");
} else {
  daTrilha.forEach((p) => console.log(`Dia ${p.dia}, ${p.hora}${p.titulo}`));
}
console.groupEnd();

// ===== 7. Juntando as duas listas (find) =====
console.group("Quem apresenta o quê");
for (const palestra of palestras) {
  const responsavel = palestrantes.find((pessoa) => pessoa.id === palestra.palestranteId);
  const nome = responsavel?.nome ?? "A definir";
  console.log(`${palestra.titulo}${nome}`);
}
console.groupEnd();

// ===== 8. Palestras ordenadas por ocupação (cópia + sort) =====
const porOcupacao = [...palestras]
  .sort((a, b) => b.inscritos / b.vagas - a.inscritos / a.vagas)
  .map((p) => ({
    titulo: p.titulo,
    ocupacao: formatarPercentual.format(p.inscritos / p.vagas),
  }));

console.group("Ranking de ocupação");
console.table(porOcupacao);
console.groupEnd();

Três detalhes valem atenção:

  • console.table(palestras, ["dia", "hora", "titulo", "local"]) — o segundo argumento escolhe quais colunas mostrar. Sem ele, a tabela sai com as dez propriedades e fica ilegível na projeção.
  • responsavel?.nome ?? "A definir" — se algum palestranteId apontar para um identificador inexistente, find devolve undefined; o ?. evita o TypeError e o ?? fornece o texto padrão. É a dupla da Aula 11 trabalhando junto com o find de hoje.
  • [...palestras].sort(…) — a cópia protege a ordem original. Sem os três pontinhos, o array palestras ficaria reordenado para todos os scripts que rodarem depois.

Como testar

  1. Abra programacao.html no Live Server e o Console (F12). Devem aparecer oito grupos, todos recolhíveis, e nenhuma linha vermelha.
  2. A primeira linha do Console deve ser dados.js carregado: 12 atividades, 6 palestrantes., provando que a ordem dos scripts está certa.
  3. Em "Totais gerais": 1.830 vagas, 935 inscrições e ocupação de 51,1%.
  4. Em "Resumo por dia": 4 atividades em cada um dos três dias.
  5. Em "Alertas": duas atividades esgotadas (o minicurso de Git e o de redes neurais) e a resposta false para "Alguma atividade sem inscritos?".
  6. Em "Quem apresenta o quê": nenhum "A definir". Troque o palestranteId de uma palestra para 99, recarregue e confira que aparece "A definir" — sem erro. Desfaça.
  7. Troque trilhaProcurada para "ia" e recarregue: o grupo passa a listar duas atividades. Troque para "robotica": aparece "Nenhuma atividade nesta trilha." Volte para "seguranca".
  8. Ponha um breakpoint condicional na linha do for (const palestra of palestras) com a condição palestra.dia === 3 e recarregue: a execução para na primeira atividade do terceiro dia. Confira no painel Scope.

Resultado esperado: toda a informação do evento vive em um único arquivo de dados, e qualquer relatório novo é uma linha de filter, map ou reduce — nenhuma informação repetida no HTML.

🧪 Laboratório

Os exercícios do Nível B pedem funções. A sintaxe completa é assunto da Aula 13; por ora, use o mesmo esqueleto das Aulas 10 e 11:

JavaScript
function nomeDaFuncao(lista) {
  const resultado = lista.length;
  return resultado;
}

console.log(nomeDaFuncao([1, 2, 3])); // 3

Nível A — Fixação

A1. Qual a diferença entre for…of e for…in? Quando usar cada um? O que acontece se você usar for…in em um array?

A2. Qual a diferença entre map, filter e forEach? O que cada um devolve? Dê um exemplo em que usar map é erro de intenção.

A3. Dado const n = [4, 8, 15, 16, 23, 42], escreva expressões que devolvam: (a) só os pares; (b) cada valor dobrado; (c) a soma total; (d) o primeiro maior que 20; (e) true se todos forem positivos; (f) os três maiores, em ordem decrescente.

A4. Dado const p = { nome: "Ana", end: { cidade: "Sinop", uf: "MT" } }, escreva: (a) o acesso à cidade; (b) a desestruturação de nome; (c) uma cópia com a propriedade fase valendo 3; (d) um array com todas as chaves de p; (e) o acesso à cidade usando notação de colchetes com o nome da propriedade em uma variável.

A5. O que é uma estrutura sequencial? Reescreva o trecho abaixo na ordem correta e explique por que a ordem original produz um resultado errado sem gerar nenhum erro.

JavaScript
const media = (soma / 3).toFixed(1);
const soma = 8 + 7 + 9;
console.log(media);

A6. Escreva o mesmo laço, que imprime os números de 1 a 10, nas quatro formas: for, while, do…while e for…of (neste último, sobre um array criado com [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).

A7. Qual a diferença entre break e continue? Escreva um laço que percorra [3, 7, 0, 5, 9] e imprima os valores, pulando os zeros e parando ao encontrar um número maior que 8.

A8. Escreva um laço infinito acidental (sem executá-lo), explique por que ele nunca termina e mostre as duas maneiras de corrigi-lo.

A9. Quando do…while é a escolha correta? Dê um exemplo do site do evento em que ele seria mais adequado que while.

A10. Escreva um laço aninhado que gere a tabuada de 1 a 5, agrupada com console.group. Quantas vezes o corpo interno executa? Mostre a conta.

A11. Anote sua previsão para cada linha e só depois teste no Console:

JavaScript
const a = [10, 9, 1];
console.log(a.sort());
console.log([1, 2, 3].map((n) => n * 2));
console.log([1, 2, 3].forEach((n) => n * 2));
console.log([1, 2, 3].filter((n) => n > 5));
console.log([].reduce((s, n) => s + n, 0));
console.log([1, 2, 3].includes("2"));
console.log(Object.keys({ b: 1, a: 2 }));

A12. O trecho abaixo deveria imprimir os cinco nomes, mas imprime seis linhas e a última é undefined. Encontre o erro, corrija e explique o que é um erro "por um".

JavaScript
const nomes = ["Ana", "Bruno", "Carla", "Diego", "Eduarda"];

for (let i = 0; i <= nomes.length; i++) {
  console.log(nomes[i]);
}

Nível B — Aplicação

B1. Usando o array palestras da Mão na massa, escreva funções que devolvam: (a) o total de vagas ainda livres no evento; (b) as atividades sem nenhuma vaga livre; (c) a atividade com mais inscritos; (d) um objeto com a média de ocupação por área; (e) a lista ordenada por horário dentro de cada dia; (f) uma busca por título parcial, ignorando maiúsculas e acentos.

Resultado esperado: seis funções testadas no Console, cada uma com pelo menos duas chamadas de exemplo; a busca por "git" encontra "Minicurso: Git e GitHub do zero"; a busca por "programacao" (sem cedilha nem til) encontra "Maratona de programação".

Dica

Para (d), use reduce com um objeto acumulador guardando soma e contagem por área, e só depois divida. Para (f), normalize os dois lados da comparação com texto.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase() — a expressão regular será formalizada na Aula 14, mas você pode usá-la como receita e explicar em um comentário o que ela faz.

B2. Escreva contarPalavras(texto) que devolva um objeto com cada palavra e sua frequência, ignorando maiúsculas e pontuação. Depois escreva topPalavras(texto, n) que devolva as n mais frequentes, em ordem decrescente, como um array de objetos { palavra, vezes }.

Resultado esperado: contarPalavras("O evento é bom. O evento é gratuito!") devolve { o: 2, evento: 2, é: 2, bom: 1, gratuito: 1 }; topPalavras(texto, 2) devolve os dois primeiros em ordem decrescente de frequência.

Dica

texto.toLowerCase().split(/[^a-zà-ú]+/) quebra o texto em palavras descartando pontuação; filtre as strings vazias que sobram nas pontas. Para ordenar o resultado, Object.entries(contagem) transforma o objeto em array de pares — e aí sort e map funcionam normalmente.

B3. Escreva gerarTabuada(n) que devolva uma string formatada com a tabuada de 1 a 10 de n (uma linha por multiplicação), e gerarTodasTabuadas() que devolva as tabuadas de 1 a 10 concatenadas, separadas por uma linha em branco. Use apenas laços e template literals.

Resultado esperado: gerarTabuada(7) devolve uma string de dez linhas começando por "7 x 1 = 7"; console.log(gerarTodasTabuadas()) imprime 10 blocos legíveis no Console.

Dica

Acumule com texto += `${n} x ${i} = ${n * i}\n`. O \n é a quebra de linha da Aula 10 — e o console.log de uma string com \n já imprime em várias linhas. Uma alternativa mais elegante: monte um array de linhas e use join("\n") no fim.

B4. Usando o array palestrantes, escreva um único encadeamento (filter + map + sort + join) que produza uma string com os nomes dos palestrantes das áreas de web e de IA, em ordem alfabética portuguesa, separados por vírgula.

Resultado esperado: uma linha de código (quebrada em várias por legibilidade) que devolve exatamente "Ana Lúcia Ferreira, Diego Nascimento, Eduarda Ribeiro, Felipe Arruda".

Dica

O filtro precisa aceitar duas áreas: ["web", "ia"].includes(pessoa.area) é mais limpo que duas comparações com ||. Na ordenação, localeCompare(b, "pt-BR") é obrigatório para que "Ana Lúcia" apareça antes de "Diego" mesmo com acentos.

B5. Escreva analisarTurma(alunos) que receba um array de objetos { nome, notas: [] } e devolva um objeto com: a média de cada aluno, a média geral da turma, o melhor e o pior desempenho e a quantidade de aprovados (média maior ou igual a 6). Use apenas métodos de ordem superior, sem nenhum for.

Resultado esperado: para uma turma de cinco alunos, um objeto com medias (array de { nome, media }), mediaGeral, melhor, pior e aprovados; todas as médias arredondadas em uma casa decimal.

Dica

Comece com map para calcular a média de cada aluno (um reduce dentro do map). Com esse array pronto, mediaGeral é outro reduce, melhor e pior saem de um reduce comparativo (ou de um sort sobre uma cópia) e aprovados é filter(...).length. Arredonde só no fim, nunca no meio — arredondar antes de somar distorce o resultado.

B6. Escreva agruparPorDia(palestras) que devolva um objeto no formato { 1: [...], 2: [...], 3: [...] }, com as atividades de cada dia já ordenadas por horário. Depois imprima o resultado com for…in e console.group, uma seção por dia.

Resultado esperado: um objeto com três chaves; agruparPorDia(palestras)[2].length devolve 4; dentro de cada dia, a primeira atividade é a de horário mais cedo.

Dica

Um reduce com objeto acumulador resolve o agrupamento: se a chave do dia ainda não existe, crie um array vazio antes de dar push. Como hora é uma string no formato "HH:MM", a comparação de texto já ordena corretamente — "09:00" < "19:00" é true. Isso só funciona porque o formato tem dois dígitos sempre; explique isso em um comentário.

Nível C — Desafio

C1. Modele o seu projeto autoral como dados. Crie js/dados.js com pelo menos doze registros em um array de objetos, cada um com no mínimo seis propriedades (incluindo id único, um campo de categoria em código curto, um campo numérico e um campo de texto longo). Crie também o objeto-dicionário que traduz os códigos de categoria em nomes de exibição. Depois escreva js/relatorios.js com pelo menos seis relatórios diferentes no Console: um console.table filtrado, um total com reduce, uma contagem por categoria, um ranking ordenado, uma busca por texto parcial e um relatório que combine duas listas com find.

Dica

Escolha as propriedades pensando no que a página vai mostrar na Aula 13 — se um cartão precisa de foto, o registro precisa do caminho da imagem. Doze registros parecem muitos, mas são o mínimo para que filtros e ordenações mostrem alguma coisa interessante; com três, todo relatório fica igual. Copie a estrutura do dados.js do evento e troque o domínio, não a arquitetura.

C2. Escreva primos(n) que devolva todos os números primos até n usando o crivo de Eratóstenes: crie um array de n + 1 posições marcadas como true, percorra a partir do 2 e marque como false todos os múltiplos de cada número que ainda estiver marcado. Meça o tempo com console.time para n = 100, n = 10.000 e n = 1.000.000 e compare com a abordagem ingênua (testar a divisibilidade de cada número por todos os anteriores).

Dica

new Array(n + 1).fill(true) cria o array inicial. O laço externo só precisa ir até Math.sqrt(n) — todo múltiplo maior já terá sido marcado por um fator menor. O laço interno pode começar em i * i, não em i * 2, pelo mesmo motivo. A diferença de tempo entre as duas abordagens em um milhão de números é da ordem de mil vezes; anote os números medidos.

🏆 Desafios

⭐ O relatório que ninguém pediu

javascriptinvestigacaoprojeto

Todo conjunto de dados esconde uma informação que ninguém tinha pensado em perguntar. Com os arrays palestras e palestrantes da Mão na massa, quantas perguntas interessantes você consegue responder em uma linha cada? Qual instituição tem mais representantes? Qual horário concentra mais inscrições? Existe alguma sala ociosa? Descubra três fatos sobre o evento que não aparecem em nenhum relatório do Passo 4 — e prove cada um com código, não com opinião.

Critérios de pronto

  • Um arquivo curiosidades.js com três relatórios novos, cada um resolvido com uma única expressão encadeada (filter, map, reduce, sort), acompanhada de um comentário de uma linha explicando a pergunta que ele responde.
  • Cada relatório imprime o resultado com console.group e um título em português.
  • Pelo menos um dos três usa Object.entries para percorrer o resultado de um agrupamento.
  • Um comentário no fim do arquivo com a descoberta mais surpreendente e o que a organização do evento deveria fazer a respeito.
Pistas
  1. Agrupar é sempre reduce com um objeto acumulador: conta[chave] = (conta[chave] ?? 0) + 1.
  2. Para "qual é o maior", reduce comparativo evita ordenar a lista inteira — e é mais rápido em listas grandes.
  3. Duas listas se cruzam com find (um item) ou filter (vários): "quantas atividades cada palestrante tem" é um map sobre palestrantes com um filter sobre palestras dentro.
⭐⭐

⭐⭐ Caça ao bug: o laço que conta errado

javascriptbugdevtoolsrefatoracao

O script abaixo deveria montar o resumo da programação. Ele roda, imprime números — e todos estão errados. São seis defeitos: um erro "por um", um for…in usado onde deveria ser for…of, um sort que ordena como texto, um reduce sem valor inicial que quebra com lista vazia, um array modificado durante a iteração e um find cujo resultado é usado sem verificação. Encontre os seis usando breakpoints condicionais — não console.log espalhado.

js/resumo-com-bug.js

JavaScript
const atividades = [
  { id: 1, titulo: "Abertura", dia: 1, inscritos: 212 },
  { id: 2, titulo: "Git do zero", dia: 1, inscritos: 30 },
  { id: 3, titulo: "Acessibilidade", dia: 2, inscritos: 28 },
  { id: 4, titulo: "Segurança", dia: 2, inscritos: 154 },
  { id: 5, titulo: "Maratona", dia: 3, inscritos: 45 },
];

let total = 0;
for (let i = 0; i <= atividades.length; i++) {
  total += atividades[i].inscritos;
}
console.log("Total de inscritos:", total);

for (const atividade in atividades) {
  console.log("Atividade:", atividade.titulo);
}

const porInscritos = atividades.sort((a, b) => a.inscritos > b.inscritos);
console.log("Mais concorrida:", porInscritos[0].titulo);

const vazias = [];
const media = vazias.reduce((soma, a) => soma + a.inscritos);
console.log("Média das vazias:", media);

atividades.forEach((a) => {
  if (a.inscritos < 50) {
    atividades.splice(atividades.indexOf(a), 1);
  }
});
console.log("Sobraram:", atividades.length);

const procurada = atividades.find((a) => a.id === 99);
console.log("Procurada:", procurada.titulo);

Critérios de pronto

  • Um arquivo resumo-corrigido.js que imprime: total de 469 inscritos, os cinco títulos, "Segurança" como mais concorrida, 0 como média da lista vazia, a contagem correta das atividades com menos de 50 inscritos e uma mensagem amigável quando o find não acha nada.
  • O array original atividades continua com cinco itens ao fim do script (nenhum método destrutivo agiu sobre ele).
  • Um arquivo bugs.md com uma tabela de seis linhas: sintoma, causa e correção.
  • Pelo menos um breakpoint condicional usado durante a investigação, documentado com a condição que você digitou.
Pistas
  1. O primeiro erro produz Uncaught TypeError: Cannot read properties of undefined (reading 'inscritos') — e a linha do erro diz exatamente qual índice foi longe demais.
  2. for…in sobre um array entrega as chaves como texto; imprima typeof atividade dentro do laço para se convencer.
  3. Uma função comparadora de sort precisa devolver um número (negativo, zero ou positivo), não um booleano. true vira 1 e false vira 0 — a ordenação fica indefinida.
  4. Remover itens de um array enquanto o percorre faz o índice interno pular posições. Prefira filter, que constrói uma lista nova.
  5. Todo find pode devolver undefined. A dupla ?. e ?? da Aula 11 resolve em uma linha.
⭐⭐⭐

⭐⭐⭐ O agrupador genérico

javascriptrefatoracaoinvestigacaoprojeto

Você já agrupou atividades por dia e contou por área. Agora repare: os dois códigos são quase idênticos — muda apenas a propriedade usada como chave. Programadores experientes sentem coceira ao ver isso e escrevem uma função que resolve os dois casos. Construa agruparPor(lista, chave), que funcione com qualquer array de objetos e qualquer propriedade, e depois estenda-a para aceitar também uma função que calcula a chave — o que permite agrupar por faixas ("lotado", "meio cheio", "vazio") e não só por valores existentes. Ao terminar, compare a sua versão com o método nativo Object.groupBy e descubra por que ele ainda não pode ser usado em qualquer projeto.

Critérios de pronto

  • agruparPor(palestras, "dia") devolve { 1: [...], 2: [...], 3: [...] }, e agruparPor(palestrantes, "area") devolve as quatro áreas — com a mesma função, sem nenhum if sobre o nome da propriedade.
  • agruparPor(palestras, (p) => p.inscritos >= p.vagas ? "esgotada" : "com vagas") devolve dois grupos, provando que a função aceita tanto um nome de propriedade quanto uma função de classificação.
  • Uma função contarPor(lista, chave) construída sobre agruparPor, sem repetir a lógica de agrupamento.
  • Um bloco de testes com pelo menos seis casos, incluindo lista vazia e uma chave inexistente (que deve gerar um único grupo "undefined", e não um erro).
  • Um comentário de até dez linhas comparando a sua implementação com Object.groupBy da documentação da MDN: o que muda na assinatura, o que muda no tipo devolvido e qual é a situação de suporte nos navegadores.
Pistas
  1. typeof chave === "function" distingue os dois modos de uso — a mesma técnica que bibliotecas famosas usam para aceitar parâmetros flexíveis.
  2. O corpo é um reduce de cinco linhas: calcule a chave, crie o array se ele não existir (acumulado[valor] ??= []), dê push, devolva o acumulado.
  3. contarPor pode ser Object.entries(agruparPor(lista, chave)).map(([k, itens]) => [k, itens.length]) — e depois Object.fromEntries monta o objeto de volta.
  4. Object.groupBy devolve um objeto sem protótipo e aceita apenas função (nunca o nome da propriedade). Procure a tabela de compatibilidade na MDN e veja a partir de qual versão de cada navegador ele existe.

🐛 Erros comuns

Sintoma Causa Solução
Uncaught TypeError: Cannot read properties of undefined (reading 'nome') dentro de um laço erro "por um": a condição usa i <= lista.length e lê uma posição inexistente use i < lista.length, ou troque por for…of, que não erra o limite
Uncaught ReferenceError: palestras is not defined js/dados.js não foi incluído na página, ou foi incluído depois do script que o usa inclua dados.js antes dos demais no <head>, todos com defer
Uncaught TypeError: Assignment to constant variable. no cabeçalho do for contador declarado com const use let no contador; const só em for…of
[10, 9, 1].sort() devolve [1, 10, 9] sem função comparadora, sort converte tudo em texto passe (a, b) => a - b para números e (a, b) => a.localeCompare(b, "pt-BR") para textos
Uncaught TypeError: Reduce of empty array with no initial value reduce sem o segundo argumento em um array vazio passe sempre o valor inicial: reduce((s, x) => s + x, 0)
A ordem original dos dados mudou sozinha sort e reverse alteram o array em que atuam ordene uma cópia: [...lista].sort(…)
O laço não termina e a aba congela a variável da condição nunca muda, muda na direção errada, ou compara decimais com === atualize o contador; use </> em vez de ===; feche a aba com Shift+Esc
forEach percorre a lista errada depois de remover itens o array foi modificado durante a própria iteração construa uma lista nova com filter em vez de remover no lugar
for…in sobre um array imprime 0, 1, 2 como texto for…in percorre chaves, não valores use for…of para valores ou for clássico para índices
Um map foi usado só para imprimir e o retorno virou lixo erro de intenção: map é para transformar use forEach ou for…of quando não houver transformação
if (lista.find(…)) entra no ramo errado find devolve o item ou undefined; se o item for 0 ou "", ele é falsy compare explicitamente: if (lista.find(…) !== undefined)
if (lista.findIndex(…)) erra quando o item é o primeiro findIndex devolve 0 para a primeira posição, e 0 é falsy compare com -1: if (indice !== -1)
Alterar uma cópia alterou o original const copia = original cria outro nome, não uma cópia copie com spread: [...lista] ou { ...objeto }
console.table sai ilegível, com dez colunas a tabela mostra todas as propriedades por padrão passe o array de colunas: console.table(lista, ["dia", "hora", "titulo"])

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (20 min). FLANAGAN, D. JavaScript: o guia definitivo, capítulos de estruturas de controle e de arrays. STEFANOV, S. Padrões JavaScript, capítulo sobre arrays e objetos. Na MDN, leia "Laços e iterações" e a página de Array (links em Para aprofundar). Anote dois métodos de array que existem na MDN e não apareceram nesta aula.

Parte 2 — Produção (30 min). Produza os exercícios B2 (contarPalavras e topPalavras) e B5 (analisarTurma) em arquivos .js comentados, com pelo menos cinco casos de teste cada, demonstrados no Console. Produza também o exercício C1: o js/dados.js do seu projeto autoral, com doze registros, e o js/relatorios.js com seis relatórios.

Critério de pronto: o dados.js do projeto autoral carrega em todas as páginas sem erro e imprime, na primeira linha do Console, a contagem de registros; os seis relatórios rodam sem nenhuma linha vermelha; nenhum relatório altera a ordem original do array de dados; nenhum dado do domínio continua digitado à mão no HTML das páginas que serão renderizadas na próxima aula.

Parte 3 — Discussão (10 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: discuta quando o for clássico ainda é preferível a map/filter/reduce, com um exemplo concreto de cada situação (um em que o laço vence, um em que o método vence).

Guarde no seu repositório: commit + push (ou a pasta do projeto).

✅ Checkpoint do projeto

Ao fim desta aula, o repositório do seu projeto autoral deve ter:

  • [ ] js/dados.js com pelo menos doze registros em um array de objetos, cada um com id único e no mínimo seis propriedades.
  • [ ] Um objeto-dicionário que traduz códigos de categoria em nomes de exibição.
  • [ ] js/dados.js incluído com defer antes dos demais scripts em todas as páginas, e a contagem de registros impressa no Console ao carregar.
  • [ ] js/relatorios.js com pelo menos seis relatórios: console.table com colunas escolhidas, total com reduce, contagem por categoria, ranking ordenado, busca por texto parcial e junção de duas listas com find.
  • [ ] Pelo menos um laço aninhado usado para agrupar (por dia, por categoria ou por faixa).
  • [ ] Nenhuma ordenação feita diretamente sobre o array de dados (sempre [...lista].sort(…)).
  • [ ] Nenhum for…in sobre arrays e nenhum sort de números sem função comparadora.
  • [ ] Zero ocorrências de var e de == em todos os arquivos .js.
  • [ ] Nenhum erro vermelho no Console em nenhuma das páginas.

📚 Para aprofundar

Na próxima aula, o JavaScript sai do console e entra na página: você vai escrever as suas próprias funções, conhecer o DOM — a árvore de objetos que o navegador monta a partir do HTML — e reagir a cliques, teclas e envios de formulário. A lista de palestrantes que hoje só existe no dados.js vai virar cartões de verdade na tela, com filtro por área.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. As três estruturas de um algoritmo1.1 Sequência: a ordem importa mais do que parece2. Estruturas de repetição2.1 for: quando você sabe quantas vezes2.2 while: quando você não sabe quantas vezes2.3 do…while: pelo menos uma vez2.4 for…of: percorrer valores2.5 for…in: percorrer chaves2.6 break e continue2.7 Laços infinitos2.8 Laços aninhados3. Arrays3.1 Criando e lendo3.2 Métodos que alteram o array original3.3 Métodos que devolvem um valor novo4. Métodos de ordem superior4.1 forEach — apenas percorrer4.2 map — transformar4.3 filter — selecionar4.4 find e findIndex — achar o primeiro4.5 reduce — condensar em um valor4.6 some e every — perguntas de sim ou não4.7 Encadeamento4.8 Laço ou método?5. Objetos5.1 Criando e lendo5.2 Alterando5.3 Percorrendo um objeto5.4 Copiando e mesclando5.5 Objetos com funções dentro5.6 Desestruturação6. Array de objetos: a estrutura da web7. Depurando laços no DevTools7.1 Breakpoint simples7.2 Breakpoint condicional — o truque que economiza a aula7.3 Watch💻 Mão na massa — Os dados do evento e os relatórios no ConsolePasso 1 — criar js/dados.js e carregá-lo antes dos outrosPasso 2 — os palestrantesPasso 3 — a programaçãoPasso 4 — os relatóriosComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ O relatório que ninguém pediu⭐⭐ Caça ao bug: o laço que conta errado⭐⭐⭐ O agrupador genérico🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 3 · JavaScript e interatividade3 aulas de 50 min + 1 h EAD

Aula 13 — Funções e eventos

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Escrever funções nas três formas (declaração, expressão e arrow) e escolher a adequada para cada situação.
  • Usar parâmetros padrão, rest, spread e objetos como parâmetro, respeitando o escopo de bloco de let e const.
  • Explicar o que é o DOM e por que ele não é o mesmo que o arquivo HTML.
  • Selecionar, ler, alterar, criar e remover elementos com querySelector, textContent, classList, dataset e createElement.
  • Renderizar uma lista na tela a partir de um array de objetos, tratando o estado vazio e evitando XSS.
  • Registrar tratadores de evento com addEventListener, usar o objeto event e explicar as fases de propagação.
  • Aplicar delegação de eventos em listas dinâmicas e controlar a frequência de eventos com debounce e throttle.

📋 Pré-requisitos

  • [ ] VS Code com a extensão Live Server funcionando e o DevTools do navegador aberto na aba Console.
  • [ ] O site do evento acadêmico com as cinco páginas estilizadas e responsivas (Unidade 2) e o js/app.js ligado com defer.
  • [ ] js/dados.js e js/relatorios.js (Aula 12) no projeto, com os arrays palestrantes e palestras e o objeto nomesDasAreas — hoje eles saem do console e vão para a tela.
  • [ ] Conforto com arrays e objetos: forEach, map, filter, find e a notação { chave: valor } (Aula 12).
  • [ ] Seletores CSS (Aula 06): #id, .classe, elemento, [atributo], descendente e :checkedquerySelector usa exatamente essa sintaxe.

Na aula passada você aprendeu a controlar o fluxo do programa (condicionais e laços) e a organizar dados em arrays e objetos — mas tudo aconteceu no console. Hoje o JavaScript sai do console e entra na página: você vai escrever funções reutilizáveis, manipular a árvore do documento e reagir a cliques, teclas e envios de formulário. Na próxima aula, esse mesmo conhecimento valida o formulário de inscrição e cria a busca da programação.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Funções: declaração, expressão, arrow, parâmetros, escopo, funções de ordem superior e boas práticas
2 50 min DOM: seleção, conteúdo, atributos, classes, criação de elementos, navegação e renderização de listas
3 50 min Eventos: modelo, tipos, objeto event, propagação, delegação, debounce; Mão na massa no site do evento

1. Funções

Uma função é um bloco de código com nome, que recebe entradas (parâmetros), faz algo e devolve uma saída (return). Você já usou funções prontas o semestre inteiro — console.log, alert, Number, push — hoje passa a escrever as suas. O motivo é simples: sem funções, todo comportamento repetido vira código copiado e colado, e cada correção precisa ser feita em vários lugares.

Declaração de função

JavaScript
// js/exemplos-funcoes.js
function calcularMedia(a1, a2, a3) {
  return (a1 + a2 + a3) / 3;
}

const media = calcularMedia(8, 7, 9); // 8
console.log(media);

Declarações sofrem hoisting (içamento): o motor JavaScript lê o arquivo inteiro antes de executar e "sobe" as declarações de função para o topo. Por isso é possível chamar calcularMedia em uma linha acima de onde ela foi escrita. Isso é útil para organizar o arquivo com as funções principais no fim e o "programa" no começo.

Expressão de função

JavaScript
const calcularMedia = function (a1, a2, a3) {
  return (a1 + a2 + a3) / 3;
};

Aqui a função é um valor guardado em uma constante. Não sofre hoisting — precisa ser definida antes do uso, como qualquer outra variável.

Arrow function (ES6)

JavaScript
const calcularMedia = (a1, a2, a3) => (a1 + a2 + a3) / 3;

// Um parâmetro: parênteses opcionais
const dobrar = (n) => n * 2;

// Sem parâmetros
const saudacao = () => "Olá!";

// Corpo com várias linhas exige chaves e return explícito
const classificar = (nota) => {
  if (nota >= 6) return "Aprovado";
  if (nota >= 4) return "Exame";
  return "Reprovado";
};

// Retornando um objeto: envolva em parênteses,
// senão as chaves são lidas como corpo da função
const criarAluno = (nome, nota) => ({ nome, nota });

console.log(dobrar(21));                 // 42
console.log(classificar(5.5));           // "Exame"
console.log(criarAluno("Ana", 9.2));     // { nome: "Ana", nota: 9.2 }

⚠️ Atenção Arrow functions não têm this próprio — herdam o do escopo onde foram criadas. Isso as torna ideais como callbacks (funções passadas para forEach, map, addEventListener) e inadequadas como métodos de objeto que precisam acessar o próprio objeto por this.

Forma Hoisting this próprio Quando usar
Declaração function nome() {} Sim Sim Funções principais do arquivo, chamadas de vários lugares
Expressão const nome = function () {} Não Sim Raro em código novo; aparece em código antigo
Arrow const nome = () => {} Não Não (herda) Callbacks, funções curtas, map/filter/reduce

Parâmetros

JavaScript
// Valor padrão: usado quando o argumento não é passado (ou é undefined)
function saudar(nome = "visitante", saudacao = "Olá") {
  return `${saudacao}, ${nome}!`;
}
saudar();                   // "Olá, visitante!"
saudar("Ana", "Bom dia");   // "Bom dia, Ana!"

// Rest (...) no parâmetro: número variável de argumentos vira um array
function somar(...numeros) {
  return numeros.reduce((acumulado, n) => acumulado + n, 0);
}
somar(1, 2, 3, 4, 5);       // 15

// Spread (...) na chamada: "espalha" um array como argumentos separados
const notas = [8, 7, 9];
calcularMedia(...notas);    // o mesmo que calcularMedia(8, 7, 9)

// Objeto como parâmetro: argumentos nomeados, em qualquer ordem
function cadastrar({ nome, email, curso = "Não informado" }) {
  return `${nome} | ${email} | ${curso}`;
}
cadastrar({ email: "a@b.com", nome: "Ana" }); // "Ana | a@b.com | Não informado"

O último padrão — objeto como parâmetro — resolve um problema real: quando a função tem quatro ou cinco parâmetros, ninguém lembra a ordem. Com um objeto, a chamada se autodocumenta.

Escopo

JavaScript
const global = "visível em toda parte";

function exemplo() {
  const local = "só dentro desta função";

  if (true) {
    let bloco = "só dentro deste bloco";
    const tambemBloco = "idem";
    var vazado = "vaza para a função inteira"; // por isso não use var
  }

  console.log(bloco);   // ReferenceError: bloco is not defined
  console.log(vazado);  // funciona — comportamento confuso do var
}

console.log(local);     // ReferenceError: local is not defined

let e const têm escopo de bloco ({}). var tem escopo de função. Regra: cada variável deve existir no menor escopo possível. Se só é usada dentro do if, declare dentro do if.

Funções de ordem superior

Funções que recebem ou retornam outras funções. map, filter e reduce (Aula 12) são exemplos: recebem uma função e a aplicam a cada item. Você também pode criar as suas:

JavaScript
function aplicarDesconto(percentual) {
  return function (preco) {
    return preco * (1 - percentual / 100);
  };
}

const desconto10 = aplicarDesconto(10);
const desconto50 = aplicarDesconto(50);

desconto10(200); // 180
desconto50(200); // 100

aplicarDesconto(10) devolve uma nova função que "lembra" o percentual. Esse "lembrar" tem nome — closure — e é o mecanismo por trás do debounce que você verá na seção 16.

Boas práticas

  1. Uma função, uma responsabilidade. Se o nome tem "e" (salvarEEnviar), provavelmente são duas funções.
  2. Nomes são verbos: calcularMedia, validarEmail, formatarData, renderizarLista.
  3. Evite mais de 3–4 parâmetros. Acima disso, receba um objeto.
  4. Prefira retornar valores a alterar variáveis externas. Funções puras (mesma entrada → mesma saída, sem efeitos colaterais) são fáceis de testar no console.
  5. Retorne cedo para reduzir aninhamento:
JavaScript
// Em vez de aninhar três ifs
function processar(dado) {
  if (!dado) return null;
  if (!dado.ativo) return null;
  return dado.valor * 2;
}

2. O que é o DOM

O DOM (Document Object Model) é a representação em memória do documento, montada pelo navegador ao interpretar o HTML. Ele expõe o documento como uma árvore de objetos que o JavaScript pode ler e modificar.

HTML
<!-- exemplo.html -->
<html>
  <body>
    <h1 id="titulo">Olá</h1>
    <ul class="lista">
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  </body>
</html>
Texto
document
└── html
    └── body
        ├── h1#titulo
        │   └── "Olá"              (nó de texto)
        └── ul.lista
            ├── li → "Item 1"
            └── li → "Item 2"

Três verdades importantes:

  1. O DOM não é o HTML. O HTML é o texto-fonte; o DOM é a estrutura viva em memória. Se o JavaScript adicionar elementos, eles existem no DOM sem existir no arquivo .html. Por isso "Exibir código-fonte" (Ctrl+U) mostra o arquivo original, enquanto a aba Elements do DevTools mostra o DOM atual.
  2. Alterar o DOM altera a tela imediatamente. Não há "salvar" nem "atualizar".
  3. Recarregar a página descarta tudo. Toda alteração feita por JavaScript some — a menos que seja persistida (você verá localStorage na próxima aula).

O objeto de entrada é document, que é filho de window (o objeto global do navegador — alert, setTimeout e location também vivem nele).

🧠 Você sabia? O DOM nasceu de uma guerra. Em 1997, Netscape Navigator 4 e Internet Explorer 4 lançaram, cada um, seu próprio modelo de objetos para "HTML dinâmico" — document.layers de um lado, document.all do outro — e o mesmo script não rodava nos dois. Programadores escreviam tudo em dobro. O W3C publicou o DOM Level 1 em 1998 para acabar com isso, e hoje o padrão é mantido pelo WHATWG como o DOM Living Standard. O querySelector que você vai usar só chegou em 2008, com a especificação Selectors API.

3. Seleção de elementos

Métodos modernos — use estes

JavaScript
// Retorna o PRIMEIRO elemento que casa com o seletor CSS (ou null)
const titulo = document.querySelector("#titulo");
const primeiroItem = document.querySelector(".lista li");
const botao = document.querySelector("button.primario");
const campo = document.querySelector("input[type='email']");

// Retorna TODOS os que casam, como NodeList (mesmo que vazia)
const itens = document.querySelectorAll(".lista li");
const paragrafos = document.querySelectorAll("article p");

querySelector e querySelectorAll aceitam qualquer seletor CSS — combinadores, pseudoclasses, seletores de atributo. É por isso que a Aula 06 é pré-requisito desta.

Métodos clássicos

JavaScript
document.getElementById("titulo");          // sem #
document.getElementsByClassName("lista");   // HTMLCollection AO VIVO
document.getElementsByTagName("li");        // HTMLCollection AO VIVO
document.getElementsByName("turno");        // NodeList
Característica querySelectorAll getElementsBy*
Retorna NodeList estática HTMLCollection ao vivo
Reflete mudanças posteriores no DOM Não Sim
Tem forEach Sim Não (precisa converter)
Aceita seletor CSS Sim Não

⚠️ Atenção A coleção "ao vivo" é uma fonte clássica de bugs: se você remove elementos dentro de um laço que percorre uma HTMLCollection, a coleção encolhe durante a iteração e você pula elementos. Prefira querySelectorAll.

Percorrendo o resultado

JavaScript
const itens = document.querySelectorAll("li");

itens.forEach((item) => console.log(item.textContent));

// Se precisar de métodos de array (map, filter, find), converta:
const textos = Array.from(itens).map((i) => i.textContent);
const textos2 = [...itens].map((i) => i.textContent);

⚠️ Atenção querySelector retorna null quando não encontra. Chamar algo em null gera TypeError: Cannot read properties of null (reading 'addEventListener'). Este é o erro mais frequente de quem está aprendendo DOM. Suas duas causas quase sempre são: (a) seletor errado (#menu quando o id é menu-principal); (b) script rodando antes de o HTML existir — resolvido com defer na tag <script>.

🔬 Investigue Abra qualquer página, vá à aba Elements do DevTools e clique em um elemento. Volte ao Console e digite $0 — o navegador devolve o elemento selecionado. Agora experimente $0.textContent, $0.classList, $0.parentElement e $0.style.outline = "3px solid red". $0 é o último elemento clicado, $1 o anterior, e assim por diante. É a forma mais rápida de testar seletores: $$(".card") equivale a document.querySelectorAll(".card").

4. Lendo e alterando conteúdo

JavaScript
const titulo = document.querySelector("#titulo");

// LEITURA
titulo.textContent; // texto puro, incluindo o de elementos ocultos
titulo.innerText;   // texto como renderizado (respeita CSS, mais lento)
titulo.innerHTML;   // conteúdo HTML como string

// ESCRITA
titulo.textContent = "Novo título";
titulo.innerHTML = "Novo <strong>título</strong>";
Propriedade Interpreta HTML? Uso
textContent Não — tags viram texto literal Padrão. Use sempre que possível
innerText Não Quando importa o texto visível (respeita display: none)
innerHTML Sim Só quando você realmente precisa inserir marcação

⚠️ Atenção — XSS Nunca faça elemento.innerHTML = dadoDoUsuario. Se o usuário digitar <img src=x onerror="alert(document.cookie)"> em um campo e você inserir isso com innerHTML, o código dele executa na sua página — para todo mundo que abrir. Isso se chama Cross-Site Scripting (XSS) e é uma das vulnerabilidades mais exploradas da Web. Regra: dado vindo de usuário → textContent. Marcação que você mesmo escreveu → innerHTML é aceitável.

5. Atributos

JavaScript
const link = document.querySelector("a");

link.getAttribute("href");
link.setAttribute("href", "https://www.wikipedia.org");
link.setAttribute("target", "_blank");
link.hasAttribute("download"); // true/false
link.removeAttribute("target");

// Propriedades diretas para atributos comuns
link.href;
link.id;
img.src;
img.alt;
input.value;           // valor atual do campo
input.checked;         // true/false para checkbox/radio
input.disabled = true;
select.value;

💡 Dica input.value é diferente de input.getAttribute("value"). O primeiro é o valor atual (o que o usuário digitou); o segundo é o valor inicial escrito no HTML. Ao trabalhar com formulários, use sempre .value.

data-* — atributos personalizados

HTML
<button data-id="42" data-acao="excluir" data-nome-produto="Notebook">
  Excluir
</button>
JavaScript
const btn = document.querySelector("button");

btn.dataset.id;          // "42" (sempre string!)
btn.dataset.acao;        // "excluir"
btn.dataset.nomeProduto; // "Notebook" — data-nome-produto vira nomeProduto (camelCase)

btn.dataset.status = "ativo"; // cria data-status="ativo" no HTML

Atributos data-* são a forma padrão de guardar informação de aplicação no HTML. Serão essenciais na delegação de eventos (seção 14) e no CRUD do Boss da Unidade: cada botão de editar/excluir carrega o data-id do registro.

6. Classes e estilos

classList — a forma correta

JavaScript
const card = document.querySelector(".card");

card.classList.add("ativo");
card.classList.add("destaque", "novo");   // várias de uma vez
card.classList.remove("ativo");
card.classList.toggle("aberto");          // adiciona se não tem, remove se tem
card.classList.toggle("aberto", condicao); // força adicionar (true) ou remover (false)
card.classList.contains("ativo");         // true/false
card.classList.replace("antigo", "novo");

⚠️ Atenção Evite card.className = "novo" — isso substitui todas as classes existentes de uma vez.

Estilos inline

JavaScript
card.style.backgroundColor = "#0b3d5c"; // camelCase, não background-color
card.style.fontSize = "18px";           // sempre com unidade
card.style.display = "none";
card.style.setProperty("--cor-tema", "#e74c3c"); // variáveis CSS

// Leitura do estilo COMPUTADO (o que realmente está aplicado, vindo de qualquer folha)
const estilo = getComputedStyle(card);
estilo.backgroundColor;
estilo.width;

💡 Dica Prefira trocar classes a manipular style diretamente. O CSS fica no CSS, o comportamento fica no JS, e você mantém a separação de responsabilidades que construiu na Unidade 2.

JavaScript
// Espalha estilo pelo JavaScript
el.style.display = "none";
el.style.opacity = "0";

// O CSS define o que é "oculto"; o JS só decide quando
el.classList.add("oculto");
CSS
/* css/estilo.css */
.oculto {
  display: none;
}

7. Criando e inserindo elementos

JavaScript
// 1. Criar
const li = document.createElement("li");

// 2. Configurar
li.textContent = "Novo item";
li.classList.add("item");
li.dataset.id = "7";

// 3. Inserir
const lista = document.querySelector("ul");
lista.appendChild(li);               // no fim
lista.append(li, outroLi);           // no fim, aceita vários nós e texto
lista.prepend(li);                   // no início
lista.insertBefore(li, referencia);  // antes de um filho específico

// Inserção posicional em relação a um elemento
elemento.insertAdjacentElement("beforebegin", novo); // antes do elemento
elemento.insertAdjacentElement("afterbegin", novo);  // primeiro filho
elemento.insertAdjacentElement("beforeend", novo);   // último filho
elemento.insertAdjacentElement("afterend", novo);    // depois do elemento

elemento.insertAdjacentHTML("beforeend", "<li>Item</li>"); // com string HTML

Removendo e substituindo

JavaScript
elemento.remove();              // remove a si mesmo
pai.removeChild(filho);         // forma antiga
pai.replaceChild(novo, antigo);
elemento.replaceWith(novo);

lista.innerHTML = "";           // esvazia (rápido e comum)

Clonando

JavaScript
const copia = elemento.cloneNode(true); // true = com descendentes

8. Navegação entre nós

JavaScript
const item = document.querySelector(".item");

// Para cima
item.parentElement;
item.closest(".card"); // ancestral mais próximo que casa com o seletor (inclui o próprio)

// Para baixo
item.children;           // HTMLCollection de elementos filhos
item.firstElementChild;
item.lastElementChild;
item.querySelector(".titulo"); // busca DENTRO do item

// Para os lados
item.nextElementSibling;
item.previousElementSibling;

⚠️ Atenção Existem versões sem Element no nome (parentNode, firstChild, nextSibling) que consideram nós de texto — inclusive espaços e quebras de linha do seu HTML. Isso surpreende: ul.firstChild costuma ser um nó de texto vazio, não o primeiro <li>. Use sempre as versões com Element.

closest() é indispensável: em uma lista, ao clicar num botão dentro de um card, btn.closest(".card") devolve o card inteiro — mesmo que o clique tenha sido em um ícone dentro do botão.

9. Renderizando listas a partir de dados

Este é o padrão central do desenvolvimento front-end e será usado em todas as aulas seguintes: os dados vivem em um array; uma função desenha a tela a partir dele. Quando os dados mudam, chama-se a função de novo.

JavaScript
// js/produtos.js
const produtos = [
  { id: 1, nome: "Notebook", preco: 3500, categoria: "Informática" },
  { id: 2, nome: "Mouse", preco: 80, categoria: "Periféricos" },
  { id: 3, nome: "Teclado", preco: 150, categoria: "Periféricos" },
];

const container = document.querySelector("#lista-produtos");

function renderizar(lista) {
  container.innerHTML = ""; // 1. limpa

  if (lista.length === 0) {  // 2. estado vazio
    const aviso = document.createElement("p");
    aviso.classList.add("vazio");
    aviso.textContent = "Nenhum produto encontrado.";
    container.appendChild(aviso);
    return;
  }

  lista.forEach((produto) => { // 3. cria um nó por item
    const card = document.createElement("article");
    card.classList.add("card-produto");
    card.dataset.id = produto.id;

    const titulo = document.createElement("h3");
    titulo.textContent = produto.nome; // textContent: seguro contra XSS

    const preco = document.createElement("p");
    preco.classList.add("preco");
    preco.textContent = produto.preco.toLocaleString("pt-BR", {
      style: "currency",
      currency: "BRL",
    });

    const btn = document.createElement("button");
    btn.type = "button";
    btn.textContent = "Excluir";
    btn.dataset.id = produto.id;
    btn.classList.add("btn-excluir");

    card.append(titulo, preco, btn);
    container.appendChild(card);
  });
}

renderizar(produtos);

Versão com template literal

Mais concisa, porém exige cuidado com XSS se os dados vierem do usuário:

JavaScript
function renderizar(lista) {
  container.innerHTML = lista
    .map(
      (p) => `
      <article class="card-produto" data-id="${p.id}">
        <h3>${p.nome}</h3>
        <p class="preco">R$ ${p.preco.toFixed(2)}</p>
        <button type="button" class="btn-excluir" data-id="${p.id}">Excluir</button>
      </article>
    `
    )
    .join("");
}

Se p.nome veio de um campo digitado por alguém, essa versão é vulnerável. Uma saída intermediária: montar a marcação fixa com template literal e preencher os textos variáveis com textContent depois.

Performance: DocumentFragment

Cada appendChild direto no documento pode disparar um reflow (recálculo de layout). Para inserir muitos elementos, monte fora do documento e insira uma vez só:

JavaScript
const fragmento = document.createDocumentFragment();
lista.forEach((item) => {
  const li = document.createElement("li");
  li.textContent = item.nome;
  fragmento.appendChild(li); // ainda fora do documento
});
container.appendChild(fragmento); // uma única operação no DOM

🔎 Por baixo do capô O navegador não redesenha a tela a cada linha de JavaScript — ele acumula mudanças e recalcula o layout quando precisa (ou quando você lê uma medida, como offsetHeight, forçando o cálculo). Mesmo assim, inserir 500 nós um a um é mais caro que inserir um fragmento com 500 nós. Para listas de até algumas dezenas de itens, a diferença é imperceptível; para centenas, ela aparece no celular.

10. O modelo de eventos

Um evento é um sinal de que algo aconteceu: um clique, uma tecla, o envio de um formulário, o fim do carregamento. O JavaScript no navegador é orientado a eventos: você registra funções (ouvintes ou listeners) que serão chamadas quando o evento ocorrer. Entre um evento e outro, seu código não faz nada — o navegador fica esperando.

O mínimo para começar

JavaScript
const botao = document.querySelector("#meu-botao");

botao.addEventListener("click", function (evento) {
  console.log("Clicado!", evento.target);
});

// Com arrow function
botao.addEventListener("click", () => {
  document.querySelector("#saida").textContent = "Você clicou!";
});

// Garantir que o DOM está pronto (desnecessário se o script usa defer)
document.addEventListener("DOMContentLoaded", () => {
  console.log("DOM montado");
});

As três formas (e a única correta)

HTML
<!-- 1. Atributo HTML — evite: mistura comportamento com estrutura -->
<button onclick="alert('Oi')">Clique</button>
JavaScript
// 2. Propriedade do elemento — permite apenas UM ouvinte por evento
botao.onclick = function () { console.log("A"); };
botao.onclick = function () { console.log("B"); }; // sobrescreve o anterior!

// 3. addEventListener — a forma correta
botao.addEventListener("click", function () { console.log("A"); });
botao.addEventListener("click", function () { console.log("B"); });
// Ambos executam, na ordem de registro

🧠 Você sabia? addEventListener só passou a funcionar em todos os navegadores em 2009, com o Internet Explorer 9. Até lá, o IE usava um método próprio, attachEvent, com outra assinatura e outro comportamento de this. Uma das razões de a biblioteca jQuery ter dominado a Web por uma década foi justamente esconder essa diferença atrás de um .on("click", fn) único. Hoje o padrão venceu e o jQuery é desnecessário — mas você ainda encontrará onclick e attachEvent em código legado.

Sintaxe completa

JavaScript
elemento.addEventListener(tipo, funcao, opcoes);

// Exemplos
botao.addEventListener("click", tratarClique);
botao.addEventListener("click", () => console.log("Oi"));

botao.addEventListener("click", tratarClique, {
  once: true,     // executa apenas uma vez e se remove
  capture: false, // fase de captura em vez de borbulhamento (seção 13)
  passive: true,  // promete não chamar preventDefault (melhora a rolagem no celular)
});

// Remoção — exige a MESMA referência de função
function tratarClique() { console.log("Oi"); }
botao.addEventListener("click", tratarClique);
botao.removeEventListener("click", tratarClique); // funciona

botao.addEventListener("click", () => {});
botao.removeEventListener("click", () => {});     // não remove nada

⚠️ Atenção Funções anônimas não podem ser removidas, porque cada arrow function escrita é um objeto novo — a segunda () => {} não é a mesma que a primeira. Se precisar remover depois, nomeie a função.

⚠️ Atenção — o erro mais comum de todos botao.addEventListener("click", tratarClique()) — com parênteses — chama a função imediatamente, no momento do registro, e passa o retorno dela (geralmente undefined) como ouvinte. O clique depois não faz nada. Passe a referência: tratarClique, sem parênteses. Se precisar passar argumentos, envolva em uma arrow: () => tratarClique(42).

11. Principais tipos de evento

Mouse

Evento Quando ocorre
click Clique (pressionar e soltar sobre o elemento); também disparado por Enter/Espaço em botões e por toque
dblclick Duplo clique
mousedown / mouseup Botão pressionado / solto
mouseenter / mouseleave Entra / sai do elemento. Não borbulham
mouseover / mouseout Idem, mas borbulham dos filhos
mousemove Movimento do ponteiro (dispara dezenas de vezes por segundo)
contextmenu Botão direito

Teclado

Evento Quando ocorre
keydown Tecla pressionada (repete se segurar)
keyup Tecla solta
keypress Obsoleto — não use
JavaScript
campo.addEventListener("keydown", (e) => {
  console.log(e.key);  // "a", "Enter", "Escape", "ArrowUp", " "
  console.log(e.code); // "KeyA", "Enter", "Space" — posição física da tecla
  console.log(e.ctrlKey, e.shiftKey, e.altKey);

  if (e.key === "Enter") enviar();
  if (e.key === "Escape") fecharModal();
  if (e.ctrlKey && e.key === "s") {
    e.preventDefault(); // impede o "Salvar página" do navegador
    salvar();
  }
});

💡 Dica Use e.key para saber qual caractere; use e.code para saber qual tecla física (importante em jogos e em teclados de layouts diferentes — no teclado ABNT2, e.key da tecla ao lado do Enter é "ç", mas e.code é "Semicolon").

Formulário

Evento Quando ocorre
submit Formulário enviado — registre no <form>, não no botão
input A cada alteração do valor. O mais útil para validação ao vivo
change Ao perder o foco após alterar (ou imediatamente em select/checkbox/radio)
focus / blur Ganha / perde foco. Não borbulham
focusin / focusout Idem, mas borbulham
reset Formulário limpo

Janela e documento

Evento Quando ocorre
DOMContentLoaded HTML lido e DOM montado (não espera imagens)
load Tudo carregado, inclusive imagens e estilos
resize Janela redimensionada
scroll Rolagem
beforeunload Antes de sair da página

Toque

touchstart, touchmove, touchend. Em geral, click já funciona em telas de toque — só use eventos de toque para gestos específicos (arrastar, pinçar).

🔬 Investigue No Console, digite monitorEvents(document.body, "click") e clique em qualquer lugar da página: cada clique é impresso com o objeto do evento. Troque para monitorEvents(document.body, ["keydown", "keyup"]) e digite algo. Quando cansar, unmonitorEvents(document.body). Depois selecione um botão na aba Elements e abra a sub-aba Event Listeners no painel lateral: ela lista todos os ouvintes registrados naquele elemento e nos ancestrais — com o arquivo e a linha onde cada um foi escrito. É assim que você descobre "quem está reagindo a esse clique" em um site que não escreveu.

12. O objeto event

Toda função ouvinte recebe automaticamente um objeto com informações sobre o que ocorreu.

JavaScript
elemento.addEventListener("click", function (event) {
  event.type;          // "click"
  event.target;        // elemento que ORIGINOU o evento
  event.currentTarget; // elemento onde o OUVINTE está registrado
  event.timeStamp;     // milissegundos desde o carregamento da página

  event.clientX, event.clientY; // coordenadas na janela
  event.pageX, event.pageY;     // coordenadas na página (com rolagem)

  event.preventDefault();  // cancela o comportamento padrão
  event.stopPropagation(); // interrompe a propagação
});

⚠️ Atenção — target × currentTarget Se você clica em um <span> dentro de um <button> que tem o ouvinte, target é o <span> e currentTarget é o <button>. Confundir os dois é a causa mais comum de bugs em delegação de eventos.

preventDefault()

JavaScript
// Impede o envio e o recarregamento da página
formulario.addEventListener("submit", (e) => {
  e.preventDefault();
  processarDados();
});

// Impede a navegação de um link
link.addEventListener("click", (e) => {
  e.preventDefault();
  abrirModal();
});

// Bloqueia caracteres não numéricos (teclas de um só caractere)
campo.addEventListener("keydown", (e) => {
  if (!/[0-9]/.test(e.key) && e.key.length === 1) e.preventDefault();
});

Sem e.preventDefault() no submit, a página recarrega e todo o seu JavaScript é reiniciado — os dados somem e parece que "o código não funcionou". É o erro nº 1 da próxima aula.

13. Propagação de eventos

Quando você clica em um elemento aninhado, o evento percorre a árvore em três fases:

Texto
                ┌── document ──┐
   CAPTURA      │              │     BORBULHAMENTO
   (descendo)   ▼              ▲     (subindo)
              <div>          <div>
                ▼              ▲
               <ul>          <ul>
                ▼              ▲
               <li>          <li>
                ▼              ▲
            <button> ── ALVO ──┘
  1. Captura: de document até o alvo.
  2. Alvo: no elemento clicado.
  3. Borbulhamento (bubbling): do alvo de volta até document. É a fase padrão — onde os ouvintes ficam, a menos que você peça o contrário.
JavaScript
document.querySelector("ul").addEventListener("click", () => console.log("UL"));
document.querySelector("li").addEventListener("click", () => console.log("LI"));
// Clicar no LI imprime: "LI", depois "UL"

// Para ouvir na captura:
ul.addEventListener("click", tratar, true);
// ou { capture: true } — agora imprime "UL", depois "LI"
JavaScript
event.stopPropagation();          // impede que continue subindo
event.stopImmediatePropagation(); // também impede outros ouvintes no MESMO elemento

⚠️ Atenção Use stopPropagation com parcimônia. Ele quebra a delegação e cria comportamentos difíceis de rastrear meses depois ("por que o menu não fecha quando clico aqui?"). Antes de usá-lo, pergunte se não é melhor verificar event.target.

14. Delegação de eventos

O problema: você adiciona ouvintes a 50 botões de "excluir". Depois, o JavaScript cria mais 10 botões dinamicamente. Os novos não têm ouvinte — porque addEventListener só afeta elementos que existiam no momento do registro.

A solução: registre um único ouvinte no contêiner pai e descubra, pelo event.target, quem foi realmente clicado. Isso funciona porque os eventos borbulham.

JavaScript
const lista = document.querySelector("#lista-produtos");

lista.addEventListener("click", (event) => {
  // Encontra o botão, mesmo se o clique foi num ícone dentro dele
  const botaoExcluir = event.target.closest(".btn-excluir");
  if (!botaoExcluir) return; // clique fora dos botões: ignora

  const id = Number(botaoExcluir.dataset.id);
  excluirProduto(id);
});

Vantagens:

  1. Funciona para elementos criados depois — sem re-registrar nada.
  2. Um ouvinte em vez de N — menos memória, melhor desempenho.
  3. Código mais simples — a lógica fica concentrada em um lugar.

Padrão para múltiplas ações no mesmo contêiner

HTML
<button type="button" data-acao="editar" data-id="7">Editar</button>
<button type="button" data-acao="excluir" data-id="7">Excluir</button>
JavaScript
lista.addEventListener("click", (e) => {
  const botao = e.target.closest("button[data-acao]");
  if (!botao) return;

  const { acao, id } = botao.dataset;

  switch (acao) {
    case "editar":
      editar(Number(id));
      break;
    case "excluir":
      excluir(Number(id));
      break;
    case "concluir":
      concluir(Number(id));
      break;
  }
});

Este padrão é a espinha dorsal de qualquer CRUD em JavaScript puro — e vai aparecer de novo no Marco 3. Vale a pena entendê-lo agora.

15. Eventos personalizados

Você também pode criar os seus próprios eventos, com dados anexados em detail:

JavaScript
const evento = new CustomEvent("produtoAdicionado", {
  detail: { id: 5, nome: "Mouse" },
  bubbles: true,
});

document.dispatchEvent(evento);

document.addEventListener("produtoAdicionado", (e) => {
  console.log(e.detail.nome); // "Mouse"
});

Útil para desacoplar partes da aplicação: um módulo avisa que algo aconteceu sem conhecer quem vai reagir. No site do evento, por exemplo, o formulário de inscrição pode disparar inscricaoConfirmada e o contador de vagas, em outro arquivo, apenas escuta.

16. Controle de frequência: debounce e throttle

Eventos como input, scroll, mousemove e resize disparam dezenas de vezes por segundo. Executar uma operação pesada (filtrar 500 itens, redesenhar uma lista) em cada disparo trava a interface.

JavaScript
// DEBOUNCE — só executa depois que os disparos PARAM por X ms
function debounce(fn, atraso = 300) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), atraso);
  };
}

const buscar = debounce((texto) => {
  console.log("Buscando por:", texto);
}, 400);

campoBusca.addEventListener("input", (e) => buscar(e.target.value));
JavaScript
// THROTTLE — executa no máximo uma vez a cada X ms
function throttle(fn, intervalo = 200) {
  let podeExecutar = true;
  return function (...args) {
    if (!podeExecutar) return;
    podeExecutar = false;
    fn.apply(this, args);
    setTimeout(() => {
      podeExecutar = true;
    }, intervalo);
  };
}

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log("Rolando");
  }, 200)
);
Aspecto Debounce Throttle
Quando executa Após a pausa nos disparos Em intervalos regulares
Uso típico Busca ao digitar, validação, redimensionamento Rolagem, movimento do mouse, animações

🔎 Por baixo do capô debounce só funciona por causa das closures da seção 1: a variável timer fica "presa" dentro da função retornada e sobrevive entre uma chamada e outra. Cada tecla cancela o setTimeout anterior e agenda outro; só o último sobrevive 300 ms e executa fn.

17. Acessibilidade em eventos

JavaScript
// Só funciona com mouse
div.addEventListener("click", acao);

// Use um <button> de verdade: já é focável, acionável por Enter/Espaço
// e anunciado como botão pelo leitor de tela
botao.addEventListener("click", acao);

Se por algum motivo precisar tornar um elemento genérico interativo:

HTML
<div role="button" tabindex="0" aria-pressed="false">Alternar</div>
JavaScript
div.addEventListener("keydown", (e) => {
  if (e.key === "Enter" || e.key === " ") {
    e.preventDefault();
    acao();
  }
});

Regra: se algo é clicável, use <button>. Se navega para outro lugar, use <a href>. Reinventar controles com <div> é a origem da maioria dos problemas de acessibilidade em sites modernos — e o Lighthouse (Aula 15) penaliza isso.

📌 Vale gravar Vale saber de cor: (1) a diferença entre target e currentTarget; (2) por que addEventListener("click", fn()) não funciona; (3) o que acontece sem preventDefault no submit; (4) por que delegação resolve elementos criados depois; (5) a ordem de impressão em um exemplo de borbulhamento.

18. A arquitetura: estado → dados → renderização → eventos

Antes de ir para o projeto, um esqueleto que vale para qualquer aplicação front-end — do menu de hoje ao CRUD do fim da unidade:

JavaScript
// Esqueleto genérico de organização — não é o js/app.js do projeto.
// Os nomes abaixo (els, renderizar, tarefas) são de exemplo; no seu código,
// use nomes específicos, porque scripts sem type="module" dividem o mesmo
// escopo global e nomes genéricos colidem entre arquivos.
// ===== ESTADO: a fonte única da verdade =====
let tarefas = [];
let filtroAtual = "todas";

// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====
const els = {
  form: document.querySelector("#form-tarefa"),
  input: document.querySelector("#input-tarefa"),
  lista: document.querySelector("#lista-tarefas"),
};

// ===== FUNÇÕES DE DADOS (puras: alteram só o estado) =====
function adicionarTarefa(texto) {
  tarefas.push({ id: Date.now(), texto, concluida: false });
}

function alternarConclusao(id) {
  const tarefa = tarefas.find((t) => t.id === id);
  if (tarefa) tarefa.concluida = !tarefa.concluida;
}

function obterTarefasVisiveis() {
  if (filtroAtual === "pendentes") return tarefas.filter((t) => !t.concluida);
  if (filtroAtual === "concluidas") return tarefas.filter((t) => t.concluida);
  return [...tarefas];
}

// ===== RENDERIZAÇÃO (desenha a tela a partir do estado) =====
function renderizar() {
  els.lista.innerHTML = "";
  obterTarefasVisiveis().forEach((t) => {
    const li = document.createElement("li");
    li.dataset.id = t.id;
    li.classList.toggle("concluida", t.concluida);
    li.textContent = t.texto;
    els.lista.appendChild(li);
  });
}

// ===== EVENTOS (capturam a intenção do usuário) =====
function registrarEventos() {
  els.form.addEventListener("submit", (e) => {
    e.preventDefault();
    const texto = els.input.value.trim();
    if (texto === "") return;
    adicionarTarefa(texto);
    els.input.value = "";
    renderizar();
  });

  els.lista.addEventListener("click", (e) => {
    const li = e.target.closest("li[data-id]");
    if (!li) return;
    alternarConclusao(Number(li.dataset.id));
    renderizar();
  });
}

// ===== INICIALIZAÇÃO =====
function iniciar() {
  registrarEventos();
  renderizar();
}

iniciar();

O fluxo é sempre o mesmo: usuário age → evento → função altera o estado → renderizar() redesenha. Nunca altere o DOM "na mão" para refletir uma mudança de dado: se o usuário conclui uma tarefa, não risque o <li> — mude o array e renderize de novo. Quando DOM e dados divergem, os bugs se tornam impossíveis de rastrear. Frameworks como Vue e React (Nível 3) apenas automatizam essa mesma ideia.

💻 Mão na massa — O menu que você já tinha, agora entendido, e a lista de palestrantes

O site do evento já tem um menu hambúrguer que abre e fecha desde a Aula 08, com oito linhas de JavaScript que você colou "sem entender", e uma página de palestrantes escrita à mão, artigo por artigo. Hoje o js/menu.js cresce com o que você aprendeu (fechar com Esc, fechar ao clicar fora, reagir ao resize), o js/app.js ganha uma seção nova sem perder nada do que já fazia, e a página de palestrantes passa a ser renderizada a partir do js/dados.js da Aula 12, com filtro por área usando delegação.

⚠️ Atenção Três coisas que não acontecem hoje: (1) a marcação do cabeçalho não muda — o <button class="menu-botao"> com o <span class="menu-botao__icone"> e a <ul id="a13-menu-principal" class="menu"> são os da Aula 08, com o .menu__cta de "Inscreva-se" no fim; (2) o CSS do menu não muda — a abertura continua sendo a animação de opacity + visibility + transform da Aula 09, sem display: none; (3) nenhum arquivo é apagado nem reescrito do zero. menu.js, efeitos.js, app.js, dados.js e relatorios.js continuam existindo, cada um com a sua responsabilidade.

Passo 1 — Reler e ampliar o js/menu.js

Abra js/menu.js. Ele tem isto desde a Aula 08:

site-evento/js/menu.js (como está)

JavaScript
// Seleciona o botão e a lista pelo seletor CSS
const botao = document.querySelector('.menu-botao');
const menu = document.querySelector('#menu-principal');

// A cada clique, inverte o valor de aria-expanded
botao.addEventListener('click', () => {
  const aberto = botao.getAttribute('aria-expanded') === 'true';
  botao.setAttribute('aria-expanded', String(!aberto));
});

Agora você consegue ler cada linha: document.querySelector é a seleção da seção 3; addEventListener('click', …) é o registro de tratador da seção 10; a arrow function é a forma da seção 1. E consegue ver o que falta — as três melhorias que ficaram como exercício B3 da Aula 08.

Substitua o conteúdo do arquivo por esta versão. Os nomes botao e menu continuam os mesmos: eles pertencem a este arquivo e a mais nenhum. Se você declarar um const menu também no app.js, o navegador para tudo com Identifier 'menu' has already been declared — dois <script> sem type="module" compartilham o mesmo escopo global.

site-evento/js/menu.js (versão desta aula)

JavaScript
// menu.js — comportamento do menu hambúrguer (marcação e CSS: Aulas 08 e 09)

// ===== ELEMENTOS =====
const botao = document.querySelector(".menu-botao");
const menu = document.querySelector("#menu-principal");
const LARGURA_DESKTOP = 768; // o mesmo ponto de quebra do CSS da Aula 08

// ===== FUNÇÕES DE ESTADO =====
// O estado mora no atributo aria-expanded do botão: uma fonte só, que o CSS
// também lê (.menu-botao[aria-expanded="true"] + .menu). Nada de classe .aberto.
function menuEstaAberto() {
  return botao.getAttribute("aria-expanded") === "true";
}

function definirMenu(aberto) {
  botao.setAttribute("aria-expanded", String(aberto));
}

function fecharMenu() {
  definirMenu(false);
}

function alternarMenu() {
  definirMenu(!menuEstaAberto());
}

// ===== EVENTOS =====
function registrarEventosDoMenu() {
  // 1. Clique no botão: abre ou fecha
  botao.addEventListener("click", alternarMenu);

  // 2. Esc fecha e devolve o foco ao botão (seção 17: nunca perca o foco)
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && menuEstaAberto()) {
      fecharMenu();
      botao.focus();
    }
  });

  // 3. Clique fora do menu e do botão: fecha (um ouvinte só, no document)
  document.addEventListener("click", (e) => {
    const clicouDentro = menu.contains(e.target) || botao.contains(e.target);
    if (!clicouDentro && menuEstaAberto()) {
      fecharMenu();
    }
  });

  // 4. Ao alargar a janela para desktop, garante o estado fechado
  window.addEventListener("resize", () => {
    if (window.innerWidth >= LARGURA_DESKTOP && menuEstaAberto()) {
      fecharMenu();
    }
  });
}

// ===== INICIALIZAÇÃO =====
// Se uma página não tiver cabeçalho (a 404.html da Aula 15, por exemplo),
// querySelector devolve null e o script não deve quebrar.
if (botao && menu) {
  registrarEventosDoMenu();
}

Três observações:

  • O estado continua no aria-expanded. Não existe classe .aberto neste projeto: o CSS das Aulas 08 e 09 já reage ao atributo, e o atributo é o que o leitor de tela anuncia. Um estado, um lugar.
  • O clique fora usa o document — é delegação (seção 14) aplicada à página inteira. Repare que ele funciona porque o clique no botão também chega ao document na fase de bolha; por isso a verificação botao.contains(e.target), senão o menu abriria e fecharia no mesmo clique.
  • resize dispara dezenas de vezes por segundo ao arrastar a janela. Aqui o trabalho é baratíssimo (uma comparação e, no máximo, um setAttribute), então não vale a pena um throttle. Na Aula 14 você vai encontrar um caso em que vale.

Passo 2 — Acrescentar ao js/app.js (sem apagar nada)

O js/app.js já tem, das Aulas 10 e 11: as constantes do evento, a saudação no console, o console.group, o console.table das cinco páginas e a contagem regressiva do index.html. Nada disso sai. Você só organiza o arquivo em blocos e acrescenta uma seção no fim.

Comece pondo os comentários de seção sobre o que já existe, na ordem da seção 18 desta aula:

site-evento/js/app.js (topo do arquivo, comentários acrescentados)

JavaScript
// app.js — comportamento comum a todas as páginas do site do evento

// ===== ESTADO =====
const NOME_EVENTO = "Semana Acadêmica de Sistemas de Informação";
const EDICAO = 12;
const LOCAL_EVENTO = "Auditório Central";
const TRILHAS = "Desenvolvimento Web, Dados, Segurança";

Deixe a saudação, o console.group, o console.table e todo o bloco ===== CONTAGEM REGRESSIVA PARA A ABERTURA ===== exatamente onde estão. Depois, no fim do arquivo, acrescente:

site-evento/js/app.js (acrescente ao fim)

JavaScript
// ===== NAVEGAÇÃO: marcar a página atual =====
/**
 * Marca com aria-current="page" o link do menu que aponta para a página aberta.
 * @param {string} [padrao="index.html"] - arquivo assumido quando a URL termina em "/"
 */
function marcarPaginaAtual(padrao = "index.html") {
  const listaDoMenu = document.querySelector("#menu-principal");
  if (listaDoMenu === null) return;

  const arquivoAtual = location.pathname.split("/").pop() || padrao;

  listaDoMenu.querySelectorAll("a").forEach((link) => {
    const destino = link.getAttribute("href");
    if (destino === arquivoAtual) {
      link.setAttribute("aria-current", "page");
    } else {
      link.removeAttribute("aria-current");
    }
  });
}

marcarPaginaAtual();

⚠️ Atenção Isto é uma troca declarada, não um acréscimo: até a Aula 08 o aria-current="page" era escrito à mão no HTML de cada página. Agora ele passa a ser aplicado pelo JavaScript, então apague o atributo das cinco páginas — se você deixar os dois, um link renomeado vai ficar marcado em dois lugares e a função vai remover o atributo que o HTML afirmava. Uma informação, uma fonte.

O preço dessa troca é real e você precisa conhecê-lo: sem JavaScript, o destaque da página atual some. Ele é aceitável aqui porque aria-current é reforço — o menu continua navegável, os links continuam funcionando e o <title> continua dizendo onde a pessoa está. Se o atributo fosse a única forma de saber a página atual, o certo seria mantê-lo no HTML.

Passo 3 — A página de palestrantes renderizada a partir do dados.js

O js/dados.js da Aula 12 já tem o array palestrantes (seis pessoas, com id, nome, instituicao, area, tema e foto) e o dicionário nomesDasAreas. O js/palestrantes.js de hoje consome os dois: ele não redeclara nem copia nada. Se você redeclarar aqui o array com const, as duas declarações colidem no escopo global e o navegador derruba o script inteiro com Uncaught SyntaxError: Identifier 'palestrantes' has already been declared.

Substitua o <main> de palestrantes.html pelo contêiner vazio, os botões de filtro e o contador — os seis <article> escritos à mão nas Aulas 02 e 04 saem daqui, porque a partir de agora quem os escreve é o JavaScript:

palestrantes.html (trecho: <head> e <main>)

HTML
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Conheça os palestrantes da Semana Acadêmica de Sistemas de Informação.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Palestrantes — Semana Acadêmica de Sistemas de Informação</title>
  <link rel="stylesheet" href="css/estilo.css">
  <script src="js/menu.js" defer></script>
  <script src="js/dados.js" defer></script>
  <script src="js/app.js" defer></script>
  <script src="js/palestrantes.js" defer></script>
</head>
HTML
<main id="conteudo" tabindex="-1" class="container">
  <h1>Palestrantes</h1>
  <p>Conheça quem vai compartilhar conhecimento nesta edição.</p>

  <div id="filtros-area" class="filtros" role="group" aria-label="Filtrar por área">
    <button type="button" class="ativo" data-area="todas" aria-pressed="true">Todas</button>
    <button type="button" data-area="web" aria-pressed="false">Desenvolvimento Web</button>
    <button type="button" data-area="dados" aria-pressed="false">Ciência de Dados</button>
    <button type="button" data-area="seguranca" aria-pressed="false">Segurança</button>
    <button type="button" data-area="ia" aria-pressed="false">Inteligência Artificial</button>
  </div>

  <p id="contador-palestrantes" class="contador" role="status"></p>

  <ul id="lista-palestrantes" class="cartoes"></ul>
</main>

Os quatro data-area são exatamente os códigos de nomesDasAreas (web, dados, seguranca, ia) — os mesmos que a Aula 12 usou nos relatórios do console. Repare também na ordem dos <script> no <head>: dados.js antes de palestrantes.js, senão o array ainda não existe quando o segundo arquivo roda. Com defer, a ordem de execução é a ordem em que os <script> aparecem.

site-evento/js/palestrantes.js

JavaScript
// palestrantes.js — a lista de palestrantes renderizada a partir de js/dados.js.
// Depende de js/dados.js (arrays `palestrantes` e objeto `nomesDasAreas`),
// que precisa ser carregado antes. Nada é redeclarado aqui.

// ===== ESTADO =====
let areaAtual = "todas";

// ===== SELEÇÃO DE ELEMENTOS (uma vez só) =====
const elementosPalestrantes = {
  lista: document.querySelector("#lista-palestrantes"),
  filtros: document.querySelector("#filtros-area"),
  contador: document.querySelector("#contador-palestrantes"),
};

// ===== FUNÇÕES DE DADOS =====
function obterPalestrantesVisiveis() {
  if (areaAtual === "todas") {
    return [...palestrantes];
  }
  return palestrantes.filter((pessoa) => pessoa.area === areaAtual);
}

// ===== RENDERIZAÇÃO =====
/**
 * Monta o cartão de um palestrante.
 * @param {Object} pessoa - um item do array `palestrantes` de dados.js
 * @returns {HTMLLIElement} o <li> pronto para entrar na lista
 */
function criarCartaoDePalestrante(pessoa) {
  const item = document.createElement("li");
  item.classList.add("cartao", "cartao--palestrante");
  item.dataset.id = pessoa.id;

  const foto = document.createElement("img");
  foto.src = pessoa.foto;
  foto.alt = `Foto de ${pessoa.nome}`;
  foto.width = 240;
  foto.height = 240;
  foto.loading = "lazy";

  const nome = document.createElement("h2");
  nome.textContent = pessoa.nome; // textContent: nunca innerHTML para dados

  const instituicao = document.createElement("p");
  instituicao.classList.add("cartao__meta");
  instituicao.textContent = pessoa.instituicao;

  const etiqueta = document.createElement("span");
  etiqueta.classList.add("etiqueta");
  etiqueta.textContent = nomesDasAreas[pessoa.area];

  const tema = document.createElement("p");
  tema.textContent = pessoa.tema;

  item.append(foto, nome, instituicao, etiqueta, tema);
  return item;
}

function renderizarPalestrantes() {
  const visiveis = obterPalestrantesVisiveis();
  elementosPalestrantes.lista.innerHTML = "";

  if (visiveis.length === 0) {
    const aviso = document.createElement("li");
    aviso.classList.add("vazio");
    aviso.textContent = "Nenhum palestrante nesta área ainda. Volte em breve!";
    elementosPalestrantes.lista.appendChild(aviso);
  } else {
    const fragmento = document.createDocumentFragment();
    visiveis.forEach((pessoa) => fragmento.appendChild(criarCartaoDePalestrante(pessoa)));
    elementosPalestrantes.lista.appendChild(fragmento);
  }

  const total = visiveis.length;
  elementosPalestrantes.contador.textContent =
    total === 1 ? "1 palestrante" : `${total} palestrantes`;
}

function atualizarBotoesDeFiltro() {
  elementosPalestrantes.filtros.querySelectorAll("button[data-area]").forEach((botaoFiltro) => {
    const ativo = botaoFiltro.dataset.area === areaAtual;
    botaoFiltro.classList.toggle("ativo", ativo);
    botaoFiltro.setAttribute("aria-pressed", String(ativo));
  });
}

// ===== EVENTOS =====
function registrarEventosDePalestrantes() {
  // Um único ouvinte para todos os botões de filtro (delegação, seção 14)
  elementosPalestrantes.filtros.addEventListener("click", (e) => {
    const botaoFiltro = e.target.closest("button[data-area]");
    if (!botaoFiltro) return;

    areaAtual = botaoFiltro.dataset.area;
    atualizarBotoesDeFiltro();
    renderizarPalestrantes();
  });
}

// ===== INICIALIZAÇÃO =====
function iniciarPalestrantes() {
  registrarEventosDePalestrantes();
  renderizarPalestrantes();
}

iniciarPalestrantes();

Duas escolhas de nome merecem explicação. Primeiro, elementosPalestrantes em vez de um genérico els: como todos os scripts sem type="module" dividem o mesmo escopo global, um nome curto e óbvio é justamente o que vai colidir com o de outro arquivo — na Aula 14 nasce um js/programacao.js que precisaria do mesmo els. Segundo, renderizarPalestrantes em vez de renderizar: pelo mesmo motivo. Nome específico é seguro; nome genérico é uma bomba-relógio.

Passo 4 — O estilo dos filtros e da etiqueta

Os cartões não precisam de CSS novo: <li class="cartao cartao--palestrante"> dentro de <ul class="cartoes"> reaproveita a grade repeat(auto-fit, minmax(280px, 1fr)) da Aula 07 e a foto redonda da Aula 08. Só os filtros, o contador, a etiqueta e o estado vazio são novos.

css/estilo.css (seção 5 — componentes)

CSS
/* ===== Filtros de área (Aula 13) ===== */
.filtros {
  display: flex;
  flex-wrap: wrap;
  gap: var(--espaco-pequeno);
  margin-block: var(--espaco-medio);
}

.filtros button {
  padding: var(--espaco-pequeno) var(--espaco-medio);
  border: 2px solid var(--cor-primaria);
  border-radius: 999px;
  background: transparent;
  color: var(--cor-primaria);
  font: inherit;
  cursor: pointer;
}

.filtros button.ativo {
  background: var(--cor-primaria);
  color: var(--cor-sobre-primaria);
}

.contador {
  color: var(--cor-texto);
  font-size: .875rem;
}

.etiqueta {
  justify-self: center;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--cor-fundo);
  border: 1px solid var(--cor-borda);
  font-size: .8rem;
}

.vazio {
  grid-column: 1 / -1;
  padding: var(--espaco-grande);
  text-align: center;
}

Nenhuma cor escrita à mão: todas saem do sistema de design da Aula 06 mais as variáveis que as Aulas 07 e 08 acrescentaram (--cor-superficie, --cor-borda, --cor-sobre-primaria). O :focus-visible global da Aula 06 já cuida do foco dos botões — não é preciso repeti-lo aqui.

Como testar

  1. Abra index.html com o Live Server e estreite a janela para menos de 768 px (ou use o modo dispositivo do DevTools, Ctrl+Shift+M). O botão "Menu" aparece; a lista, não.
  2. Clique no botão: a lista desliza e aparece com a animação da Aula 09 e, na aba Elements, aria-expanded muda para "true". O ícone gira 90°.
  3. Pressione Esc: o menu fecha e o foco volta ao botão (o anel de foco aparece nele). Abra de novo e clique fora do menu: fecha.
  4. Alargue a janela para além de 768 px: o menu fecha sozinho e o botão desaparece.
  5. Ainda no index.html, confira que a contagem regressiva continua na tela e que o Console ainda mostra a saudação, o grupo "Dados do evento" e a tabela das cinco páginas — se algo sumiu, o app.js foi sobrescrito em vez de ampliado.
  6. Em qualquer página, na aba Elements, o link do menu correspondente à página aberta tem aria-current="page"; os outros quatro, não. Navegue para outra página e confira que o atributo mudou de lugar.
  7. Vá a palestrantes.html: os seis cartões aparecem gerados pelo JavaScript (em Elements o <ul> está cheio; em "Exibir código-fonte", vazio). O contador mostra "6 palestrantes" e as fotos são as img/palestrante-01.jpg a img/palestrante-06.jpg.
  8. Clique em "Segurança": só a Carla Mendes aparece e o contador diz "1 palestrante". Clique em "Desenvolvimento Web": aparecem Diego Nascimento e Eduarda Ribeiro. Clique em "Todas": tudo volta. Navegue pelos filtros com Tab e acione com Enter — funciona igual.
  9. Console limpo nas cinco páginas: nenhuma linha vermelha e, em especial, nenhum Identifier 'palestrantes' has already been declared nem Identifier 'menu' has already been declared.

🧪 Laboratório

Nível A — Fixação

A1. Escreva a mesma função nas três formas (declaração, expressão e arrow): recebe o raio e retorna a área do círculo (Math.PI * raio ** 2).

A2. O que faz o operador rest (...numeros) em um parâmetro de função? E o spread em uma chamada? Dê um exemplo de cada.

A3. O que é o DOM? Ele é o mesmo que o arquivo HTML? Justifique com o que você vê em "Exibir código-fonte" versus a aba Elements.

A4. Qual a diferença entre querySelector e querySelectorAll? O que cada um retorna quando não encontra nada?

A5. Qual a diferença entre textContent, innerText e innerHTML?

A6. Explique o que é XSS e como textContent previne esse ataque.

A7. Escreva o código para: (a) selecionar o elemento de id menu; (b) selecionar todos os <li> dentro de .nav; (c) selecionar o primeiro input do tipo email.

A8. Escreva o código que adiciona a classe ativo, remove inativo e alterna aberto em um elemento.

A9. Escreva os três passos para criar e inserir um novo <li> com texto "Item" em uma <ul> existente.

A10. O que faz closest(".card")? Dê um caso de uso.

A11. Por que é preferível trocar classes a definir element.style diretamente?

A12. Um querySelector retornou null e o código quebrou com Cannot read properties of null. Cite duas causas prováveis e como corrigir cada uma.

A13. Por que addEventListener é preferível a elemento.onclick?

A14. Diferencie event.target de event.currentTarget com um exemplo concreto.

A15. Descreva as três fases da propagação de eventos.

A16. O que faz event.preventDefault()? Dê dois casos de uso.

A17. Qual a diferença entre stopPropagation() e preventDefault()?

A18. O que é delegação de eventos e qual problema ela resolve?

A19. Diferencie os eventos input e change. Quando usar cada um?

A20. Escreva o código que executa uma função quando o usuário pressiona Enter em um campo de texto.

A21. Escreva o código que impede o recarregamento da página ao enviar um formulário.

A22. O que faz a opção { once: true } em addEventListener?

A23. Por que <div onclick=""> é pior que <button> do ponto de vista de acessibilidade? Cite três motivos.

Nível B — Aplicação

B1. Implemente calculadora como um objeto com os métodos somar, subtrair, multiplicar, dividir (tratando divisão por zero) e potencia, todos com validação de tipo dos parâmetros (typeof).

Resultado esperado: calculadora.dividir(10, 0) retorna null (ou lança um erro com mensagem clara); calculadora.somar("2", 3) rejeita a string; calculadora.potencia(2, 10) retorna 1024.

Dica

Crie uma função auxiliar saoNumeros(...valores) que use valores.every((v) => typeof v === "number" && !Number.isNaN(v)) e chame-a no início de cada método. Métodos de objeto precisam de this? Aqui não — cada método só usa seus parâmetros, então arrow functions ou métodos curtos servem.

B2. Dada uma lista <ul> com 10 <li>, escreva o JavaScript que: pinta os ímpares de cinza-claro (via classe); adiciona a classe destaque ao primeiro e ao último; adiciona um número de ordem antes do texto de cada um; e exibe no console o texto de todos.

Resultado esperado: a lista alterna cores, o primeiro e o último ficam em negrito, cada item começa com "1. ", "2. " e assim por diante, e o console mostra os 10 textos.

Dica

querySelectorAll("ul li").forEach((li, indice) => …) — o segundo parâmetro do forEach é o índice. Para o texto, li.textContent = `${indice + 1}. ${li.textContent}`. Para o primeiro e o último, compare indice com 0 e com lista.length - 1.

B3. Construa uma tabela dinâmica: dado um array de alunos com três notas, gere uma <table> completa com <thead>, <tbody>, média calculada e uma coluna "Situação" cuja célula recebe a classe .aprovado, .exame ou .reprovado conforme o valor (≥ 6, ≥ 4, abaixo).

Resultado esperado: uma tabela com uma linha por aluno, a média com duas casas (toFixed(2)) e a célula de situação colorida pelo CSS.

Dica

Crie criarLinha(aluno) que devolve um <tr> com <td> gerados por createElement. Use reduce para a média e a função classificar da seção 1 para a situação. Monte tudo em um DocumentFragment e insira no <tbody> uma vez.

B4. Escreva uma função criarElemento(tag, atributos, filhos) que crie e configure um elemento genericamente. Exemplo de uso:

JavaScript
criarElemento("a", { href: "#", class: "btn", "data-id": 5 }, ["Clique aqui"]);

Use-a para reconstruir a renderização de produtos da seção 9 em menos linhas.

Resultado esperado: criarElemento("li", { class: "item" }, ["Texto", criarElemento("strong", {}, ["!"])]) gera <li class="item">Texto<strong>!</strong></li>.

Dica

Object.entries(atributos).forEach(([nome, valor]) => el.setAttribute(nome, valor)). Para os filhos, se for string use document.createTextNode; se for elemento, appendChild direto — el.append(...filhos) já aceita os dois tipos.

B5. Implemente um modal: botão que abre, botão "×" que fecha, clique no fundo escurecido que fecha, tecla Escape que fecha. Ao abrir, o foco deve ir para dentro do modal; ao fechar, voltar ao botão que o abriu.

Resultado esperado: o modal funciona só com o teclado (Tab até o botão, Enter abre, Esc fecha) e o foco nunca "some".

Dica

Use o elemento nativo <dialog> com dialog.showModal() e dialog.close() — ele já trata Escape e o foco inicial. Para o clique no fundo, ouça click no próprio dialog e feche se e.target === dialog. Guarde document.activeElement antes de abrir para devolver o foco ao fechar.

B6. Crie um menu suspenso (dropdown) que abre ao clicar, fecha ao clicar fora (use delegação em document), fecha com Escape e navega com as setas do teclado.

Resultado esperado: move o foco para o próximo item, para o anterior, Esc fecha e devolve o foco ao botão.

Dica

Guarde os itens em [...menu.querySelectorAll("a")] e o índice do item focado. No keydown, calcule o novo índice com (indice + 1) % itens.length (e (indice - 1 + itens.length) % itens.length para subir) e chame .focus(). O padrão de "clique fora" é o mesmo do Mão na massa.

B7. Implemente um contador de caracteres em um <textarea> com limite de 280: exibe os restantes, fica amarelo abaixo de 40 e vermelho abaixo de 10, e desabilita o botão de envio ao ultrapassar.

Resultado esperado: "280 caracteres restantes" ao carregar; ao digitar, o número cai; com 281 caracteres o botão fica disabled e o texto, vermelho.

Dica

Ouça input no textarea, calcule restantes = LIMITE - textarea.value.length e use classList.toggle("alerta", restantes < 40) e classList.toggle("perigo", restantes < 10). botao.disabled = restantes < 0.

B8. Crie uma lista de 20 itens gerados dinamicamente onde cada item tem botões "subir", "descer" e "excluir", todos tratados por um único ouvinte no contêiner (delegação).

Resultado esperado: clicar em "subir" troca o item de posição com o anterior; "excluir" remove; a lista continua funcionando depois de qualquer operação porque é re-renderizada a partir do array.

Dica

Não mova o <li> no DOM — troque as posições no array com [itens[i - 1], itens[i]] = [itens[i], itens[i - 1]] e chame renderizar(). Use data-acao e data-id nos botões e o switch da seção 14.

Nível C — Desafio

C1. Kanban com arrastar e soltar. Construa um quadro com três colunas (A fazer, Em andamento, Concluído). Cartões podem ser criados, editados, excluídos e arrastados entre colunas usando a API de Drag and Drop (dragstart, dragover, drop). Contadores por coluna. Deve funcionar também por teclado (botões "mover para →" e "← mover para").

Dica

Cada cartão tem draggable="true" e data-id. No dragstart, guarde o id em e.dataTransfer.setData("text/plain", id). A coluna precisa chamar e.preventDefault() no dragover para aceitar o drop. No drop, leia o id, mude o campo coluna do cartão no array e renderize tudo — o mesmo fluxo estado → renderização de sempre. Os botões de teclado fazem exatamente a mesma alteração no array.

🏆 Desafios

⭐ O botão que só funciona uma vez

javascripteventosbugdevtools

Alguém enviou o código abaixo dizendo que "o contador funciona uma vez e depois para, e às vezes nem começa". Rode-o, reproduza os dois sintomas e encontre os quatro bugs plantados — sem reescrever do zero. Cada um é um erro clássico visto nesta aula.

HTML
<!-- bug-contador.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Contador</title>
  <script src="contador.js"></script>
</head>
<body>
  <p>Cliques: <strong id="total">0</strong></p>
  <button id="btn-somar"><span>Somar</span></button>
  <button id="btn-zerar">Zerar</button>
</body>
</html>
JavaScript
// contador.js
let total = 0;
const saida = document.querySelector("#total");
const btnSomar = document.querySelector("#btn-somar");
const btnZerar = document.querySelector("#btn-zerar");

function somar(e) {
  if (e.target !== btnSomar) return;
  total = total + 1;
  saida.textContent = total;
}

btnSomar.addEventListener("click", somar, { once: true });
btnZerar.addEventListener("click", zerar());

function zerar() {
  total = 0;
  saida.textContent = total;
}

Critérios de pronto

  • O contador incrementa a cada clique, indefinidamente, inclusive quando o clique cai no texto "Somar".
  • "Zerar" volta o contador a 0 apenas quando clicado.
  • Nenhum erro no console ao carregar a página.
  • Um comentário acima de cada correção diz qual era o bug e por que acontecia (uma frase cada).
Pistas
  1. Abra o Console antes de qualquer coisa: uma das falhas aparece em vermelho no carregamento e tem a ver com a ordem em que o navegador lê <head> e <body>.
  2. Releia a seção 12 sobre target × currentTarget e pense no <span> dentro do botão.
  3. Uma opção passada ao addEventListener está fazendo exatamente o que promete.
  4. Compare zerar com zerar() — o que está sendo passado como ouvinte?
⭐⭐

⭐⭐ Lista de tarefas interativa

javascriptdomeventosprojeto

Uma lista de tarefas é o "olá, mundo" das interfaces dinâmicas — e o exercício que mais revela se você entendeu o fluxo estado → renderização → eventos. Construa a sua, do zero, seguindo a arquitetura da seção 18, sem alterar o DOM "na mão" em nenhum momento: toda mudança na tela nasce de uma mudança no array seguida de renderizar().

Critérios de pronto

  • Adicionar pelo botão ou pela tecla Enter; texto vazio ou só com espaços é rejeitado com mensagem de erro visível.
  • Concluir: clicar no checkbox alterna concluida no array e re-renderiza.
  • Excluir: botão com data-id, tratado por delegação, com confirmação.
  • Editar: duplo clique no texto o transforma em <input>; Enter confirma, Escape cancela, blur salva.
  • Filtrar: três botões (Todas / Pendentes / Concluídas) alteram uma variável de estado e re-renderizam; o botão ativo recebe a classe .ativo e aria-pressed="true".
  • Buscar: campo de texto com input + debounce de 300 ms, filtrando pelo termo.
  • Ordenar: <select> com as opções mais recentes, mais antigas e alfabética.
  • Limpar concluídas remove todas de uma vez, com confirmação.
  • Contadores de total, pendentes e concluídas, sempre atualizados.
  • Atalhos: Ctrl+Enter adiciona; Escape limpa o campo.
  • O arquivo js/tarefas.js está dividido nos blocos ESTADO, ELEMENTOS, DADOS, RENDERIZAÇÃO, EVENTOS e INICIALIZAÇÃO, nessa ordem.
Pistas
  1. Comece pelo estado: let tarefas = [], let filtroAtual = "todas", let termoBusca = "", let ordenacao = "recentes". Cada tarefa é { id, texto, concluida, criadaEm } com id: Date.now().
  2. Escreva obterTarefasVisiveis() como uma cadeia: copia o array, aplica filter do filtro, filter da busca e sort da ordenação — e só ela é usada por renderizar().
  3. Para editar, guarde idEmEdicao no estado; renderizar() desenha um <input> em vez do texto quando o id bate. Enter/Escape/blur apenas alteram o estado e renderizam.
  4. Um único addEventListener("click") na <ul> resolve checkbox, excluir e iniciar edição — use closest("[data-acao]") e um switch.
⭐⭐⭐

⭐⭐⭐ Quiz com pontuação e cronômetro

javascriptdomeventosacessibilidade

Sites de evento costumam ter um "quiz de conhecimentos" para engajar quem se inscreve. Construa um quiz de 10 perguntas de múltipla escolha sobre a Unidade 3 (você escreve as perguntas), com uma pergunta por tela, cronômetro de 20 segundos por questão, pontuação e um placar final. As perguntas vivem em um array de objetos; a tela é renderizada a partir do estado; não existe uma linha de HTML de pergunta escrita à mão.

Critérios de pronto

  • Uma pergunta por vez, com quatro alternativas em <button>; responder ou esgotar o tempo avança automaticamente.
  • Barra de tempo animada com CSS e atualizada por setInterval, que respeita prefers-reduced-motion.
  • Pontuação: 10 pontos por acerto mais um bônus de 1 ponto por segundo restante; a fórmula está comentada no código.
  • Navegação completa por teclado: teclas 14 selecionam a alternativa; Enter confirma.
  • Ao terminar, um placar mostra acertos, erros, tempo médio e as questões erradas com a resposta certa.
  • O quiz dispara um CustomEvent("quizFinalizado", { detail }) com o resultado, e um script separado escuta esse evento para gravar o melhor placar em localStorage.
  • Nenhum innerHTML recebe texto vindo das perguntas — tudo por textContent.
Pistas
  1. Estado mínimo: indiceAtual, pontuacao, respostas (array), segundosRestantes e o id do setInterval — para poder cancelar com clearInterval ao responder.
  2. Escreva renderizarPergunta() e renderizarPlacar() como funções separadas; avancar() decide qual chamar.
  3. Para as teclas 1–4, ouça keydown em document e use Number(e.key) - 1 como índice da alternativa — mas ignore quando o quiz não estiver em andamento.
  4. localStorage.setItem("quiz:melhor", JSON.stringify(detail)) no ouvinte do evento personalizado; leia com JSON.parse ao carregar. A próxima aula aprofunda o localStorage.

🐛 Erros comuns

Sintoma Causa Solução
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') querySelector não encontrou o elemento: seletor errado ou script no <head> sem defer Confira o seletor no Console com $$("seletor"); coloque defer na tag <script>
Clicar no botão não faz nada e não há erro addEventListener("click", fn()) — a função foi chamada no registro e o retorno (undefined) virou o ouvinte Passe a referência fn, sem parênteses
Página recarrega ao enviar o formulário e "os dados somem" Falta e.preventDefault() no ouvinte de submit Chame e.preventDefault() na primeira linha do ouvinte
Ouvinte registrado no botão, mas Enter no campo envia o formulário sem passar por ele O evento certo é submit no <form>, não click no botão Ouça submit no formulário
Itens criados pelo JavaScript não reagem ao clique Ouvintes foram registrados antes de os elementos existirem Use delegação: um ouvinte no contêiner + closest()
e.target.dataset.id é undefined em alguns cliques O clique caiu em um filho do botão (ícone, <span>) — target é o filho Use e.target.closest("button[data-id]")
if (botao.dataset.id === produto.id) nunca é verdadeiro dataset sempre devolve string; produto.id é número Converta com Number(botao.dataset.id)
removeEventListener não remove nada Foi passada uma função anônima diferente da registrada Nomeie a função e passe a mesma referência nos dois lugares
Busca ao digitar trava a página em listas grandes Um filtro pesado roda a cada tecla Envolva o ouvinte em debounce(fn, 300)
<div onclick> não funciona com Tab/Enter nem é lido pelo leitor de tela div não é focável nem tem semântica de controle Troque por <button type="button">
Menu abre e fecha no mesmo clique O clique no botão borbulha até o ouvinte de "clique fora" em document Verifique botao.contains(e.target) antes de fechar (ou não use stopPropagation)

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (15 min). FLANAGAN, JavaScript: o guia definitivo, capítulos de funções, de scripting de documentos e de eventos. MILETTO & BERTAGNOLLI, Desenvolvimento de software II, capítulo de interação com o usuário. Na MDN em português, o guia "Introdução a eventos" (link em Para aprofundar).

Parte 2 — Entrega (40 min). No seu projeto autoral:

  1. O menu mobile funcionando com clique, Esc e clique fora, com aria-expanded sincronizado (como no Mão na massa).
  2. A listagem principal do seu domínio (produtos, quadras, vagas, pratos, pescarias) renderizada a partir de um array de pelo menos 6 objetos, com estado vazio tratado.
  3. Ao menos um filtro por botões usando delegação de eventos.
  4. Os exercícios B7 (contador de caracteres, aplicado ao seu formulário de contato) e B8 (lista com subir/descer/excluir) em uma pasta exercicios/aula13/.

Critério de pronto: nenhuma página do projeto mostra erro no Console; a listagem some do "Exibir código-fonte" e aparece na aba Elements; o menu abre e fecha só com o teclado.

Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: explique, com um trecho do seu projeto, um problema que só a delegação de eventos resolve bem — e o que aconteceria sem ela.

Guarde no seu repositório: commit + push (ou a pasta do projeto).

✅ Checkpoint do projeto

  • [ ] js/menu.js, js/dados.js e js/app.js carregados com defer em todas as páginas, nessa ordem, sem erro no Console — e nenhum identificador declarado em dois arquivos.
  • [ ] js/app.js ampliado, não reescrito: a saudação e o console.table da Aula 10 e a contagem regressiva da Aula 11 continuam funcionando.
  • [ ] Botão hambúrguer com aria-expanded e aria-controls; menu abre/fecha por clique, Esc, clique fora e resize, sem classe .aberto e sem display: none.
  • [ ] Link da página atual marcado com aria-current="page" pelo JavaScript, e o atributo removido do HTML das cinco páginas.
  • [ ] Listagem principal renderizada a partir de um array de objetos, com textContent (nunca innerHTML para dados) e estado vazio tratado.
  • [ ] Filtro por botões com um único ouvinte (delegação) e aria-pressed atualizado.
  • [ ] Nenhum var, nenhum onclick no HTML, nenhum <div> clicável.
  • [ ] Código organizado nos blocos ESTADO → ELEMENTOS → DADOS → RENDERIZAÇÃO → EVENTOS → INICIALIZAÇÃO.

📚 Para aprofundar

Na próxima aula, o formulário de inscrição do evento ganha validação campo a campo com mensagens acessíveis, expressões regulares para CPF, telefone e e-mail, e a programação passa a ter busca, filtro e ordenação em tempo real — tudo construído sobre as funções, o DOM e os eventos de hoje.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. FunçõesDeclaração de funçãoExpressão de funçãoArrow function (ES6)ParâmetrosEscopoFunções de ordem superiorBoas práticas2. O que é o DOM3. Seleção de elementosMétodos modernos — use estesMétodos clássicosPercorrendo o resultado4. Lendo e alterando conteúdo5. Atributosdata-* — atributos personalizados6. Classes e estilosclassList — a forma corretaEstilos inline7. Criando e inserindo elementosRemovendo e substituindoClonando8. Navegação entre nós9. Renderizando listas a partir de dadosVersão com template literalPerformance: DocumentFragment10. O modelo de eventosO mínimo para começarAs três formas (e a única correta)Sintaxe completa11. Principais tipos de eventoMouseTecladoFormulárioJanela e documentoToque12. O objeto eventpreventDefault()13. Propagação de eventos14. Delegação de eventosPadrão para múltiplas ações no mesmo contêiner15. Eventos personalizados16. Controle de frequência: debounce e throttle17. Acessibilidade em eventos18. A arquitetura: estado → dados → renderização → eventos💻 Mão na massa — O menu que você já tinha, agora entendido, e a lista de palestrantesPasso 1 — Reler e ampliar o js/menu.jsPasso 2 — Acrescentar ao js/app.js (sem apagar nada)Passo 3 — A página de palestrantes renderizada a partir do dados.jsPasso 4 — O estilo dos filtros e da etiquetaComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ O botão que só funciona uma vez⭐⭐ Lista de tarefas interativa⭐⭐⭐ Quiz com pontuação e cronômetro🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 3 · JavaScript e interatividade3 aulas de 50 min + 1 h EAD

Aula 14 — JavaScript para validação de formulários e consultas dinâmicas

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Capturar os valores de um formulário pelas três vias (querySelector, form.elements e FormData) e converter cada tipo de campo corretamente.
  • Escolher o momento certo de validar (input, blur, submit) e implementar a estratégia "valide no blur, corrija no input".
  • Usar a Constraint Validation API (validity, checkValidity, setCustomValidity, novalidate) combinando os atributos do HTML com regras de negócio em JavaScript.
  • Escrever funções validadoras puras, incluindo a verificação dos dígitos verificadores do CPF.
  • Ler e escrever expressões regulares para os formatos brasileiros usuais, reconhecendo o que a regex consegue e o que ela não consegue validar.
  • Exibir mensagens de erro específicas e acessíveis com role="alert", aria-invalid, aria-describedby e aria-live, levando o foco ao primeiro campo inválido.
  • Implementar busca, filtro e ordenação em tempo real sobre uma listagem, com normalização de acentos, debounce e tratamento do estado vazio.

📋 Pré-requisitos

  • [ ] O site do evento acadêmico com as cinco páginas responsivas, js/app.js carregado com defer em todas elas e o menu hambúrguer funcionando (Aula 13).
  • [ ] js/dados.js com os arrays palestras e palestrantes criados na Aula 12.
  • [ ] Domínio de funções, querySelector, classList, dataset, createElement, addEventListener, preventDefault e delegação de eventos (Aula 13).
  • [ ] Domínio de filter, map, find, forEach e sort sobre arrays de objetos (Aula 12).
  • [ ] O formulário de inscricao.html com <label>, <fieldset>, <select> e validação nativa (Aulas 03 e 04).
  • [ ] DevTools aberto na aba Console e o Live Server rodando.

Na aula passada o JavaScript saiu do console e entrou na página: você escreveu funções, manipulou o DOM, registrou ouvintes e renderizou os palestrantes a partir de um array, com filtro por delegação. Hoje o mesmo conjunto de ferramentas resolve os dois problemas que todo site real tem: impedir que dados errados entrem e deixar o usuário encontrar o que procura. O formulário de inscrição ganha validação campo a campo com mensagens acessíveis, e a programação passa a ter busca, filtro e ordenação em tempo real.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Captura dos dados, momento de validar, Constraint Validation API e funções validadoras puras
2 50 min Expressões regulares do zero, padrões brasileiros, máscaras e mensagens acessíveis
3 50 min localStorage, consultas dinâmicas (busca, filtro, ordenação) e Mão na massa no site do evento

1. Capturando os dados do formulário

1.1 O ponto de partida: submit e preventDefault

Todo processamento de formulário em JavaScript começa no mesmo lugar: o evento submit do <form> — nunca o click do botão. O motivo você já viu na Aula 13: Enter em qualquer campo de texto envia o formulário sem passar pelo botão, e um ouvinte de click simplesmente não é acionado.

inscricao.html — estrutura mínima de um campo

HTML
<form id="form-inscricao" novalidate>
  <div class="campo">
    <label for="nome">Nome completo *</label>
    <input type="text" id="nome" name="nome" autocomplete="name" required>
    <span class="erro" id="erro-nome" role="alert"></span>
  </div>

  <button type="submit">Enviar inscrição</button>
</form>

js/inscricao.js — o ouvinte

JavaScript
const form = document.querySelector("#form-inscricao");

form.addEventListener("submit", (evento) => {
  evento.preventDefault(); // SEMPRE: sem isso a página recarrega e tudo some
  console.log("Formulário enviado sem recarregar a página");
});

O atributo novalidate no <form> desliga a validação nativa do navegador. Fazemos isso quando queremos controlar totalmente as mensagens: as caixas amarelas do Chrome não são personalizáveis, mudam de texto entre navegadores, somem sozinhas depois de alguns segundos e não são lidas de forma confiável por leitores de tela. Mesmo desligando a exibição, mantemos os atributos required, type, minlength e pattern no HTML: eles continuam alimentando a Constraint Validation API (seção 3) e documentam as regras para quem lê o HTML.

⚠️ Atenção novalidate desliga a exibição das mensagens nativas, não a validação. campo.validity.valueMissing continua funcionando. É por isso que a combinação "atributos no HTML + mensagens em JavaScript" é a arquitetura recomendada: você não reescreve regras que o navegador já sabe aplicar.

1.2 Três formas de ler os valores

JavaScript
const form = document.querySelector("#form-inscricao");

// 1. Elemento por elemento — explícito, bom quando você precisa do elemento em si
const nome = document.querySelector("#nome").value.trim();

// 2. Pela coleção elements do formulário — usa o atributo name
const email = form.elements.email.value.trim();

// 3. FormData — a forma mais prática quando você quer o objeto inteiro
const dados = Object.fromEntries(new FormData(form).entries());
console.log(dados); // { nome: "Ana Souza", email: "ana@exemplo.br", cpf: "123.456.789-09" }

new FormData(form) monta um objeto iterável com todos os campos que têm name e não estão desabilitados. Object.fromEntries transforma esses pares chave/valor em um objeto JavaScript comum. Duas limitações que valem a pena guardar: FormData ignora campos sem name, e um grupo de checkboxes com o mesmo name fica só com o último valor — para esse caso, use dados.getAll("atividades").

Forma Vantagem Quando usar
querySelector("#id").value Devolve o elemento, não só o valor Quando você vai mexer em classes e atributos do campo
form.elements.nome.value Não depende de id, só de name Validação genérica, percorrendo campos
new FormData(form) Pega tudo de uma vez, pronto para enviar Montar o objeto final depois de validado

1.3 Cada tipo de campo devolve uma coisa diferente

JavaScript
// Texto: SEMPRE aplique trim() — "   " não é um nome preenchido
const nome = document.querySelector("#nome").value.trim();

// Número: value é SEMPRE string, mesmo em type="number"
const idade = Number(document.querySelector("#idade").value);

// Checkbox único: booleano
const aceitouTermos = document.querySelector("#termos").checked;

// Radio: pegue o que está marcado (pode não haver nenhum)
const vinculo = form.querySelector("input[name='vinculo']:checked")?.value ?? "";

// Grupo de checkboxes: transforme a NodeList em array e extraia os valores
const atividades = [...form.querySelectorAll("input[name='atividades']:checked")]
  .map((caixa) => caixa.value);

// Select simples e select múltiplo
const curso = document.querySelector("#curso").value;
const cursos = [...document.querySelector("#cursos").selectedOptions].map((o) => o.value);

// Arquivo: uma lista de File, mesmo sem o atributo multiple
const comprovante = document.querySelector("#comprovante").files[0];

⚠️ Atenção O erro clássico, que aparece o tempo todo: input.value de um type="number" retorna a string "25", não o número 25. Fazer idade + 1 produz "251". Converta sempre com Number() — e lembre-se, da Aula 11, de que Number("") é 0, então teste o campo vazio antes de converter.

O ?. no radio (encadeamento opcional, Aula 11) evita o clássico Cannot read properties of null (reading 'value') quando nenhuma opção está marcada; o ?? completa com uma string vazia.

2. Quando validar

Validar cedo demais irrita; validar tarde demais frustra. O momento certo depende do evento:

Momento Evento Custo de escolher errado
Ao digitar input Acusa "e-mail inválido" na segunda letra digitada
Ao sair do campo blur O usuário só descobre o erro depois de sair
Ao enviar submit Tarde demais como único feedback

A estratégia que dá a melhor experiência combina os três, em quatro regras:

  1. Não valide enquanto o usuário digita pela primeira vez. Ele ainda está escrevendo.
  2. Valide no blur, quando ele sai do campo — o momento natural de "terminei este aqui".
  3. A partir do instante em que o campo já mostrou erro, passe a validar no input. Assim o usuário vê a mensagem sumir enquanto corrige, em vez de precisar sair do campo de novo.
  4. Valide tudo no submit e leve o foco ao primeiro campo inválido.
JavaScript
const campos = [...form.querySelectorAll("input, select, textarea")];

campos.forEach((campo) => {
  campo.addEventListener("blur", () => {
    validarCampo(campo);
    campo.dataset.tocado = "true"; // marca: este campo já foi visitado
  });

  campo.addEventListener("input", () => {
    if (campo.dataset.tocado === "true") validarCampo(campo);
  });
});

O truque está no dataset.tocado (Aula 13): o próprio DOM guarda o estado "este campo já foi visitado", sem precisar de um array paralelo em JavaScript. Como dataset sempre devolve string, a comparação é com "true", não com true.

💡 Dica blur não borbulha, então form.addEventListener("blur", …) não funciona — é por isso que o código acima registra o ouvinte campo a campo. Se você quiser um ouvinte só, use o evento focusout, que é a versão borbulhante do blur.

3. A Constraint Validation API

O navegador já validou o campo antes de você escrever qualquer linha. A Constraint Validation API é a interface que expõe esse resultado — e é ela que faz required, type="email", min, max, minlength e pattern valerem também para o seu código.

3.1 O objeto validity

JavaScript
const campo = document.querySelector("#email");

campo.validity.valid;           // true se o campo passa em todas as restrições
campo.validity.valueMissing;    // required não preenchido
campo.validity.typeMismatch;    // type="email" ou type="url" com formato errado
campo.validity.patternMismatch; // não casou com o atributo pattern
campo.validity.tooShort;        // abaixo do minlength (tooLong: acima do maxlength)
campo.validity.rangeUnderflow;  // abaixo do min (rangeOverflow: acima do max)
campo.validity.customError;     // há uma mensagem definida por setCustomValidity

3.2 Os três métodos

JavaScript
campo.checkValidity();   // true/false, silencioso (não mostra nada na tela)
campo.reportValidity();  // valida E mostra a mensagem nativa do navegador
campo.setCustomValidity("As senhas não coincidem."); // define um erro seu
campo.setCustomValidity("");                          // limpa o erro personalizado

setCustomValidity merece cuidado: enquanto a mensagem estiver definida, o campo é considerado inválido para sempre, mesmo que o usuário corrija o valor. Você precisa limpá-la (setCustomValidity("")) no início de cada nova validação. Esquecer isso produz o bug "o formulário nunca envia" mais comum da aula.

3.3 Traduzindo validity em mensagem útil

Em vez de escrever de novo a regra "e-mail precisa ter arroba", leia o diagnóstico que o navegador já fez e devolva um texto em português decente:

JavaScript
function mensagemNativa(campo) {
  const v = campo.validity;
  const rotulo = campo.labels[0]?.textContent.replace("*", "").trim() || "Este campo";

  if (v.valueMissing) return `${rotulo} é obrigatório.`;
  if (v.typeMismatch) return `${rotulo} está em um formato inválido.`;
  if (v.tooShort) return `${rotulo} deve ter ao menos ${campo.minLength} caracteres.`;
  if (v.tooLong) return `${rotulo} deve ter no máximo ${campo.maxLength} caracteres.`;
  if (v.rangeUnderflow) return `${rotulo} deve ser no mínimo ${campo.min}.`;
  if (v.rangeOverflow) return `${rotulo} deve ser no máximo ${campo.max}.`;
  if (v.patternMismatch) return campo.title || `${rotulo} está em um formato inválido.`;
  return "";
}

campo.labels é uma coleção com todos os <label> associados ao campo — mais uma razão para você ter caprichado no for/id da Aula 03. O ?. protege contra campos sem rótulo, e o campo.title aproveita o atributo title do pattern como mensagem, que é exatamente para isso que ele existe.

A arquitetura final combina duas camadas: os atributos HTML declaram as regras simples e servem de documentação; o JavaScript cuida das regras de negócio (dígitos do CPF, idade mínima, senhas coincidentes, ao menos um minicurso escolhido) e da apresentação das mensagens.

🔬 Investigue Abra inscricao.html com o Live Server e o Console. Digite const e = document.querySelector("#email") e depois e.validity. Expanda o objeto: todos os campos aparecem como false e valid: true porque o campo está vazio e ainda não é required para o navegador… ou aparece valueMissing: true, se você já colocou required. Agora digite e.value = "ana@" e chame e.validity de novo: typeMismatch virou true. Por fim, chame e.reportValidity() e veja a caixa nativa — a mesma que novalidate esconde.

🧠 Você sabia? O atributo pattern existe desde o HTML5, mas as âncoras ^ e $ são implícitas nele: o navegador sempre exige correspondência total. Escrever pattern="^\d{5}-?\d{3}$" funciona por acidente (as âncoras extras não atrapalham), mas escrever pattern="/\d{5}-?\d{3}/" nunca casa, porque as barras viram caracteres literais a serem procurados. É o erro nº 1 de quem aprendeu regex em JavaScript antes de usar pattern.

4. Funções validadoras puras

A melhor forma de organizar validação é escrever funções puras: recebem um valor, devolvem a mensagem de erro (string) ou "" quando está tudo certo. Elas não tocam no DOM, não dependem de variáveis globais e por isso podem ser testadas no console, reaproveitadas em outro projeto e lidas sem sustos.

JavaScript
// js/inscricao.js — validadores (funções puras: entram dados, sai mensagem)

function validarObrigatorio(valor, rotulo = "Este campo") {
  return valor.trim() === "" ? `${rotulo} é obrigatório.` : "";
}

function validarTamanho(valor, minimo, maximo, rotulo) {
  const v = valor.trim();
  if (v.length < minimo) return `${rotulo} deve ter ao menos ${minimo} caracteres.`;
  if (v.length > maximo) return `${rotulo} deve ter no máximo ${maximo} caracteres.`;
  return "";
}

function validarNomeCompleto(valor) {
  const partes = valor.trim().split(/\s+/); // divide por qualquer sequência de espaços
  if (partes.length < 2) return "Informe nome e sobrenome.";
  if (partes.some((parte) => parte.length < 2)) {
    return "Cada parte do nome deve ter ao menos 2 letras.";
  }
  return "";
}

function validarEmail(valor) {
  const v = valor.trim();
  if (v === "") return "O e-mail é obrigatório.";
  if (!v.includes("@")) return "O e-mail deve conter @.";
  const [usuario, dominio] = v.split("@");
  if (!usuario || !dominio) return "Formato de e-mail inválido.";
  if (!dominio.includes(".")) return "O domínio do e-mail está incompleto.";
  return "";
}

Repare no padrão: cada função devolve uma mensagem, a primeira que se aplica. Mensagens boas dizem o que fazer. "Campo inválido" não ajuda ninguém; "A senha deve conter ao menos uma letra maiúscula" resolve o problema do usuário em um segundo.

4.1 Datas digitadas: duas armadilhas do Date

Validar uma data digitada como 31/02/2000 exige mais do que formato. Duas armadilhas do objeto Date explicam por quê:

JavaScript
const data = new Date(2000, 1, 31); // meses vão de 0 a 11: 1 é fevereiro
data.getDate();  // 2   ← o Date "conserta" a data sozinho, sem erro nenhum
data.getMonth(); // 2   ← virou março

A primeira armadilha é que os meses vão de 0 a 11. A segunda é que Date nunca reclama de uma data inexistente: ele a desloca. A defesa é sempre a mesma — reconstruir dia, mês e ano a partir do objeto criado e comparar com o que foi digitado. Se não bate, a data não existe no calendário. A função completa, com o cálculo da idade, está na Mão na massa.

4.2 CPF: o algoritmo de verdade

O CPF tem 11 dígitos, sendo os dois últimos dígitos verificadores calculados a partir dos nove primeiros. Isso significa que um CPF digitado errado é detectável sem consultar nenhum servidor — e é por isso que nenhum site sério aceita 111.111.111-11.

JavaScript
function validarCPF(cpf) {
  const numeros = cpf.replace(/\D/g, ""); // \D = tudo que NÃO é dígito

  if (numeros.length !== 11) return "O CPF deve ter 11 dígitos.";
  if (/^(\d)\1{10}$/.test(numeros)) return "CPF inválido."; // 000…0, 111…1, 999…9

  const calcularDigito = (base, pesoInicial) => {
    let soma = 0;
    for (let i = 0; i < base.length; i++) {
      soma = soma + Number(base[i]) * (pesoInicial - i);
    }
    const resto = (soma * 10) % 11;
    return resto === 10 ? 0 : resto;
  };

  const primeiro = calcularDigito(numeros.slice(0, 9), 10);
  const segundo = calcularDigito(numeros.slice(0, 10), 11);

  if (primeiro !== Number(numeros[9]) || segundo !== Number(numeros[10])) {
    return "CPF inválido. Confira os números digitados.";
  }
  return "";
}

Leia a regex /^(\d)\1{10}$/ com calma, porque ela usa um recurso que a seção 5 vai formalizar: (\d) captura um dígito no grupo 1, e \1{10} exige que o mesmo dígito apareça mais dez vezes. É a forma mais curta de dizer "todos os onze dígitos são iguais".

🧠 Você sabia? O algoritmo do CPF é uma variação do cálculo de dígito verificador módulo 11, o mesmo princípio usado no ISBN de livros, no código de barras dos boletos bancários e no número de matrícula de muitas universidades. A ideia é antiga e simples: some os dígitos multiplicados por pesos decrescentes, tire o resto da divisão por 11, e guarde o resultado no fim do número. Um dígito trocado ou dois dígitos invertidos mudam a soma e o erro é detectado na hora — sem internet, sem banco de dados.

5. Expressões regulares

5.1 O que é e como criar

Uma expressão regular (regex) é um padrão que descreve um conjunto de strings. Com ela você responde perguntas como "este texto tem formato de e-mail?", "quais números aparecem aqui?" ou "troque todo espaço duplo por um só". Regex existe em praticamente todas as linguagens e ferramentas — JavaScript, Python, Java, SQL, grep, a busca do VS Code — e é um dos poucos conhecimentos que você leva inteiro para qualquer tecnologia da carreira.

JavaScript
// 1. Forma literal — preferida quando o padrão é fixo
const padrao = /abc/;

// 2. Construtor — quando o padrão é montado em tempo de execução
const termoDigitado = "café";
const busca = new RegExp(termoDigitado, "gi");

No construtor, a string precisa escapar as barras invertidas em dobro ("\\d" em vez de \d), porque a barra invertida também é especial dentro de strings. Use a forma literal sempre que puder.

5.2 Flags

Flag Nome Efeito
g global Encontra todas as ocorrências, não só a primeira
i insensitive Ignora maiúsculas e minúsculas
m multiline ^ e $ passam a casar em cada linha
s dotAll O ponto passa a casar também com quebra de linha
u unicode Tratamento correto de caracteres fora do ASCII
JavaScript
/casa/gi.test("Minha CASA e a casa dela"); // true, ignorando a caixa

5.3 Classes de caracteres

Texto
.     qualquer caractere, exceto quebra de linha
\d    dígito  [0-9]              \D    NÃO dígito
\w    palavra [A-Za-z0-9_]       \W    NÃO caractere de palavra
\s    espaço, tabulação, quebra  \S    NÃO espaço em branco

[abc]   a, b OU c                [^abc]  qualquer coisa EXCETO a, b, c
[a-z]   letra minúscula          [A-Z]   letra maiúscula
[0-9]   dígito                   [À-ÿ]   letras acentuadas da faixa Latin-1
JavaScript
/[A-Za-zÀ-ÿ]/.test("ção"); // true — letra com ou sem acento
/[^\d]/.test("1234");      // false — só há dígitos

5.4 Quantificadores

Texto
*        0 ou mais
+        1 ou mais
?        0 ou 1 (opcional)
{n}      exatamente n
{n,}     n ou mais
{n,m}    entre n e m
JavaScript
/^\d{3}$/.test("123");     // true — exatamente três dígitos
/^\d{2,4}$/.test("12345"); // false — passou de quatro
/colou?r/.test("colour");  // true — o "u" é opcional

Gulosos e preguiçosos. Por padrão, quantificadores são gulosos: pegam o máximo possível. Acrescentar ? os torna preguiçosos.

JavaScript
const texto = "<b>negrito</b> e <i>itálico</i>";

texto.match(/<.+>/)[0];  // "<b>negrito</b> e <i>itálico</i>"  ← guloso
texto.match(/<.+?>/)[0]; // "<b>"                              ← preguiçoso

5.5 Âncoras e limites

Texto
^     início da string (ou da linha, com a flag m)
$     fim da string
\b    limite de palavra
\B    NÃO limite de palavra
JavaScript
/^abc/.test("abcdef");     // true  — começa com abc
/abc$/.test("xyzabc");     // true  — termina com abc
/^abc$/.test("abc");       // true  — é exatamente abc
/\bgato\b/.test("gatorade"); // false — "gato" precisa ser palavra inteira

📌 Vale gravar A âncora é o que separa busca de validação. /\d{3}/.test("abc1234xyz") é true: encontrou três dígitos em algum lugar. /^\d{3}$/.test("abc1234xyz") é false: a string inteira precisa ser exatamente três dígitos. Toda regex de validação de formato precisa de ^ no início e $ no fim. Essa questão cai todo semestre.

5.6 Grupos, alternância e lookahead

JavaScript
/(abc)+/;                    // grupo de captura: "abc" repetido
/(?:abc)+/;                  // grupo SEM captura (não guarda o trecho)
/(palestra|minicurso|mesa)/; // alternância: um dos três

Grupos nomeados deixam o resultado legível:

JavaScript
const padraoData = /(?<dia>\d{2})\/(?<mes>\d{2})\/(?<ano>\d{4})/;
const resultado = "07/05/1998".match(padraoData);

resultado.groups.dia; // "07"
resultado.groups.mes; // "05"
resultado.groups.ano; // "1998"

Lookahead ((?=…) positivo, (?!…) negativo) e lookbehind ((?<=…) e (?<!…)) verificam o que vem depois ou antes sem consumir os caracteres:

JavaScript
// Senha forte: ao menos 1 minúscula, 1 maiúscula, 1 dígito, 1 especial, mínimo 8
const senhaForte = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/;

/\d+(?=\s*vagas)/.exec("Restam 12 vagas")[0];             // "12"  — seguido de "vagas"
/(?<=R\$\s?)\d+([.,]\d{2})?/.exec("Total: R$ 89,90")[0];  // "89,90" — precedido de R$

Cada lookahead da senha forte é uma verificação independente feita a partir do início da string: "olhando para a frente, existe em algum ponto uma minúscula?". Todas precisam ser verdadeiras, e só então [A-Za-z\d@$!%*?&#]{8,} consome a string de fato.

5.7 Escapando metacaracteres

Estes caracteres têm significado especial e precisam de barra invertida quando você quer o caractere literal:

Texto
.  *  +  ?  ^  $  {  }  (  )  |  [  ]  \  /
JavaScript
/\./;            // ponto literal
/R\$\s?\d+/;     // "R$ 100"
/\(\d{2}\)/;     // "(66)"
/https?:\/\//;   // "http://" ou "https://"

5.8 Os métodos que você vai usar

JavaScript
// test — devolve booleano. O mais usado em validação.
/\d/.test("abc123"); // true

// match — sem a flag g, devolve os grupos capturados
"07/05/1998".match(/(\d{2})\/(\d{2})\/(\d{4})/);
// ["07/05/1998", "07", "05", "1998", index: 0, ...]

// match — com a flag g, devolve só as ocorrências
"a1 b2 c3".match(/\d/g); // ["1", "2", "3"]

// matchAll — precisa da flag g; devolve um iterável com os grupos de cada ocorrência
for (const m of "Ana: 8.5, Bruno: 6.0".matchAll(/(?<nome>\w+): (?<nota>[\d.]+)/g)) {
  console.log(m.groups.nome, m.groups.nota);
}

// replace — $1, $2 referenciam os grupos capturados; uma função permite calcular
"  muitos     espaços  ".replace(/\s+/g, " ").trim();          // "muitos espaços"
"07/05/1998".replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$2-$1"); // "1998-05-07"
"restam 100".replace(/\d+/, (n) => Number(n) * 2);             // "restam 200"

// split aceita regex como separador
"nome; idade , curso".split(/\s*[;,]\s*/); // ["nome", "idade", "curso"]

5.9 Padrões brasileiros de referência

JavaScript
// js/inscricao.js — padrões reutilizáveis
const CPF = /^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$/;
const CEP = /^\d{5}-?\d{3}$/;
const TELEFONE = /^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$/;
const DATA = /^(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;
const HORA = /^([01]\d|2[0-3]):[0-5]\d$/;
const NOME = /^[A-Za-zÀ-ÿ]+(\s[A-Za-zÀ-ÿ]+)+$/;
const URL = /^https?:\/\/[\w.-]+\.[a-z]{2,}(\/\S*)?$/i;
const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;

O padrão TELEFONE merece uma leitura guiada: \(?\d{2}\)? aceita o DDD com ou sem parênteses; \s? permite um espaço; 9? torna opcional o nono dígito dos celulares; \d{4}-?\d{4} aceita o traço ou a ausência dele. Assim (66) 99999-9999, 66999999999 e (66) 3511-1000 passam, e 9999-9999 (sem DDD) não passa.

5.10 O caso do e-mail

JavaScript
// Suficiente para 99,9% dos casos reais
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;

Não existe regex simples que valide e-mail perfeitamente. A especificação que define o formato permite construções tão exóticas (comentários entre parênteses, aspas, caracteres acentuados) que a expressão completa passa de seis mil caracteres e é ilegível. A recomendação profissional é usar uma regex simples de formato e confirmar o endereço por um e-mail de verificação. Uma regex agressiva demais rejeita endereços válidos — problema pior do que aceitar um endereço inválido, porque o usuário desiste do cadastro sem entender por quê.

🧠 Você sabia? As expressões regulares nasceram em 1951, num artigo do matemático Stephen Kleene sobre linguagens regulares — décadas antes da web. O * que você usa em \d* chama-se, até hoje, estrela de Kleene. A notação chegou à programação nos anos 1960, com o editor qed de Ken Thompson, e de lá foi para o grep do Unix (o nome vem de g/re/p, "global / regular expression / print"). Ou seja: você está usando uma ideia de setenta anos para conferir um CPF.

5.11 O que a regex NÃO faz

Regex verifica formato, não validade:

Regex verifica Regex NÃO verifica
O CPF tem 11 dígitos no formato certo Se os dígitos verificadores conferem
A data está em dd/mm/aaaa Se o dia 31/02 existe no calendário
O e-mail tem arroba e domínio Se a caixa postal existe
O cartão tem 16 dígitos Se passa no algoritmo de Luhn

Combine sempre regex (formato) com validação algorítmica (regra), como você fez no CPF da seção 4.2.

ReDoS — travar o navegador com uma regex. Padrões com quantificador dentro de quantificador podem levar tempo exponencial para concluir que algo não casa:

JavaScript
// Perigosa: quantificador aninhado
const perigosa = /^(a+)+$/;
// perigosa.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaX") pode travar a aba

// Reescrita segura, com o mesmo efeito prático
const segura = /^a+$/;

Evite (x+)+, (x*)* e (x|x)*. E, quando um padrão passar de uma linha, decomponha-o em pedaços nomeados e monte com new RegExp: você não vai lembrar o que aquela sequência de símbolos faz daqui a duas semanas. Sempre comente uma regex não trivial.

🔬 Investigue Abra o console e teste: /^\d{3}$/.test("123"), depois /^\d{3}$/.test(" 123") — por que o segundo é false? Agora rode "Ana Paula da Silva".split(/\s+/) e conte os elementos. Por fim, vá a https://regex101.com, cole ^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$ no campo da expressão, escolha o sabor ECMAScript (JavaScript) e teste com (66) 99988-7766, 66999887766 e 9998-7766. O painel da direita explica token por token o que cada símbolo faz — é a melhor forma de aprender regex que existe.

6. Máscaras e formatação

Máscara é a formatação que aparece enquanto o usuário digita: 12345678909 vira 123.456.789-09. Ela reduz erros de digitação e deixa claro qual formato o campo espera.

JavaScript
// js/inscricao.js — máscaras

function mascaraCPF(valor) {
  return valor
    .replace(/\D/g, "")                     // fica só com os dígitos
    .slice(0, 11)                           // no máximo 11
    .replace(/(\d{3})(\d)/, "$1.$2")        // primeiro ponto
    .replace(/(\d{3})(\d)/, "$1.$2")        // segundo ponto
    .replace(/(\d{3})(\d{1,2})$/, "$1-$2"); // traço antes dos verificadores
}

// Aplicação: reescreve o valor do campo a cada tecla
document.querySelector("#cpf").addEventListener("input", (evento) => {
  evento.target.value = mascaraCPF(evento.target.value);
});

A receita é sempre a mesma: limpe tudo o que não é dígito, corte no comprimento máximo e insira os separadores com replace e grupos capturados. As máscaras de telefone e de data seguem exatamente esse molde e estão completas na Mão na massa.

⚠️ Atenção Máscaras atrapalham quem cola um valor já formatado e quem usa leitor de tela (o campo muda sozinho enquanto a pessoa digita). Duas regras compensam isso: sempre remova a formatação (valor.replace(/\D/g, "")) antes de validar e antes de enviar ao servidor, e nunca use a máscara como única indicação do formato — coloque também um texto de ajuda visível, ligado ao campo por aria-describedby.

7. Mostrando o erro de forma acessível

Uma mensagem de erro só cumpre seu papel se todo mundo a percebe: quem enxerga, quem não distingue vermelho de verde e quem usa leitor de tela.

JavaScript
// js/inscricao.js — exibição do estado de um campo

function mostrarErro(campo, mensagem) {
  const alvo = document.querySelector(`#erro-${campo.id}`);

  if (mensagem) {
    campo.classList.add("invalido");
    campo.classList.remove("valido");
    campo.setAttribute("aria-invalid", "true");
    campo.setAttribute("aria-describedby", `erro-${campo.id}`);
    alvo.textContent = mensagem;
  } else {
    campo.classList.remove("invalido");
    campo.classList.add("valido");
    campo.setAttribute("aria-invalid", "false");
    campo.removeAttribute("aria-describedby");
    alvo.textContent = "";
  }
}

css/estilo.css — estados do formulário

CSS
/* ===== Formulário validado ===== */
.campo {
  display: grid;
  gap: 0.25rem;
  margin-bottom: 1.25rem;
}

.campo input,
.campo select,
.campo textarea {
  width: 100%;
  padding: 0.625rem 0.75rem;
  border: 2px solid var(--cor-borda, #ccc);
  border-radius: 6px;
  font: inherit;
  transition: border-color 0.2s;
}

.campo input:focus-visible,
.campo select:focus-visible {
  outline: 3px solid rgba(26, 127, 181, 0.4);
  outline-offset: 1px;
  border-color: var(--cor-secundaria, #1a7fb5);
}

.campo .invalido {
  border-color: #c0392b;
}

.campo .valido {
  border-color: #27ae60;
}

.erro {
  display: block;
  min-height: 1.2em; /* reserva espaço: evita o layout "pular" ao exibir o erro */
  color: #c0392b;
  font-size: 0.875rem;
}

.erro:not(:empty)::before {
  content: "⚠ "; /* o erro não depende só da cor */
}

@media (prefers-reduced-motion: reduce) {
  .campo input,
  .campo select {
    transition: none;
  }
}

Os cinco requisitos de acessibilidade em mensagens de erro:

  1. role="alert" no elemento da mensagem: leitores de tela anunciam o conteúdo assim que ele aparece, sem o usuário precisar navegar até lá.
  2. aria-invalid="true" no campo: o leitor anuncia "inválido" ao entrar no campo.
  3. aria-describedby ligando o campo à mensagem: o texto do erro é lido junto com o rótulo.
  4. Nunca sinalize erro só pela cor. Some texto e um símbolo (o ::before do CSS acima).
  5. Reserve espaço para a mensagem com min-height. Sem isso, o layout salta quando o erro aparece e o usuário perde a referência visual — e quem usa ampliador de tela se perde de vez.

7.1 Validação no envio e o foco no primeiro erro

JavaScript
form.addEventListener("submit", (evento) => {
  evento.preventDefault();
  let primeiroInvalido = null;

  [...form.querySelectorAll("input, select, textarea")].forEach((campo) => {
    const valido = validarCampo(campo);
    if (!valido && !primeiroInvalido) primeiroInvalido = campo;
  });

  if (primeiroInvalido) {
    primeiroInvalido.focus();
    primeiroInvalido.scrollIntoView({ behavior: "smooth", block: "center" });
    return;
  }
  console.log("Dados válidos:", Object.fromEntries(new FormData(form).entries()));
});

Note que o forEach valida todos os campos antes de parar: o usuário vê de uma vez tudo o que precisa corrigir, e não um erro por tentativa. O focus() no primeiro inválido é obrigatório para quem navega por teclado — sem ele, a pessoa aperta "Enviar", nada acontece e ela não faz ideia de onde está o problema.

⚠️ Atenção Toda a validação desta aula acontece no navegador, e o navegador é do usuário. Qualquer pessoa abre o DevTools, apaga o required e envia o formulário; ou envia a requisição direto, sem passar pela sua página. Validação no cliente serve para experiência (avisar cedo, evitar ida e volta ao servidor); a validação que serve para segurança é a do servidor, e você a construirá no Nível 2. Nunca confie em dados que vieram do navegador.

8. Guardando a inscrição com localStorage

O navegador oferece um armazenamento simples, que sobrevive ao fechamento da aba: o localStorage. Ele guarda pares chave/valor, sempre em texto.

JavaScript
// Escrever
localStorage.setItem("inscricao:rascunho", JSON.stringify({ nome: "Ana", curso: "SI" }));

// Ler (devolve null quando a chave não existe)
const bruto = localStorage.getItem("inscricao:rascunho");
const rascunho = bruto ? JSON.parse(bruto) : null;

// Remover uma chave e limpar tudo
localStorage.removeItem("inscricao:rascunho");
localStorage.clear();

Como só há texto, objetos e arrays precisam de JSON.stringify na ida e JSON.parse na volta. Guardar { nome: "Ana" } sem stringify armazena a string "[object Object]" — outro clássico da primeira semana.

Recurso Vive até Cabe
localStorage Ser apagado pelo código ou pelo usuário Cerca de 5 MB por origem
sessionStorage A aba ser fechada Cerca de 5 MB por aba
Variável em JavaScript A página recarregar A memória disponível

⚠️ Atenção localStorage é legível por qualquer script da mesma origem e fica no computador, muitas vezes compartilhado. Nunca guarde senha, CPF, token de acesso ou dado de cartão. No rascunho da inscrição vamos salvar apenas nome, e-mail e curso — CPF e telefone ficam de fora, de propósito.

🔬 Investigue Abra qualquer página do site, vá ao DevTools na aba Application (Chrome) ou Armazenamento (Firefox) e clique em Local Storage → a URL do seu site. Rode no console localStorage.setItem("teste", "olá") e veja a linha aparecer na tabela em tempo real. Feche o navegador inteiro, abra de novo, volte à página e rode localStorage.getItem("teste"): o valor continua lá. Agora abra a mesma página em uma janela anônima e repita o getItem: devolve null, porque a janela anônima é outra área de armazenamento.

9. Consultas dinâmicas: busca, filtro e ordenação

A segunda metade da aula responde a outra pergunta: como deixar o usuário encontrar o que procura numa lista? A resposta é sempre a mesma arquitetura da Aula 13 — estado → dados → renderização → eventos —, com o estado agora guardando o termo buscado, o filtro escolhido e o critério de ordenação.

9.1 O pipeline

A função que produz a lista visível é sempre a mesma sequência: cópia → busca → filtro → ordenação.

JavaScript
// Estado da consulta (os nomes dos campos vêm do js/dados.js da Aula 12)
let termoBusca = "";
let areaAtual = "todas";
let ordenacaoAtual = "hora";

function obterPalestrasVisiveis() {
  let resultado = [...palestras]; // 1. cópia: sort() modifica o array original

  if (termoBusca) {                                       // 2. busca por texto
    const termo = normalizar(termoBusca);
    resultado = resultado.filter((p) => normalizar(p.titulo).includes(termo));
  }

  if (areaAtual !== "todas") {                            // 3. filtro por categoria
    resultado = resultado.filter((p) => p.area === areaAtual);
  }

  const ordenadores = {                                   // 4. ordenação
    hora: (a, b) => a.dia - b.dia || a.hora.localeCompare(b.hora),
    titulo: (a, b) => a.titulo.localeCompare(b.titulo, "pt-BR"),
    vagas: (a, b) => b.vagas - b.inscritos - (a.vagas - a.inscritos),
  };
  resultado.sort(ordenadores[ordenacaoAtual]);

  return resultado;
}

Três detalhes que separam código que funciona de código que funciona sempre:

  • [...palestras] cria uma cópia. sort() reordena o array original; sem a cópia, a ordem dos dados mudaria a cada renderização e o filtro seguinte partiria de outra base. É um bug sutil, difícil de encontrar e frequente.
  • localeCompare(b, "pt-BR") ordena corretamente palavras acentuadas. O sort() puro compara códigos de caractere e coloca "Ávila" depois de "Zampieri".
  • a.dia - b.dia || a.hora.localeCompare(b.hora) é ordenação em dois níveis: quando os dias são iguais, a subtração dá 0 (falso) e o || passa para o critério de desempate. Repare no nome do campo: hora, como no dados.js da Aula 12. Inventar um horario aqui daria undefined.localeCompareTypeError na primeira ordenação.

9.2 Buscar ignorando acentos e caixa

JavaScript
function normalizar(texto) {
  return texto
    .normalize("NFD")               // separa a letra do acento combinante
    .replace(/[̀-ͯ]/g, "") // remove os acentos combinantes
    .toLowerCase()
    .trim();
}

normalizar("Segurança"); // "seguranca"
normalizar("CAFÉ");      // "cafe"

Na forma de normalização NFD, o caractere "é" é decomposto em "e" mais um acento combinante separado. Esses acentos vivem no intervalo Unicode ̀ͯ, e uma única regex com a flag g remove todos de uma vez. Aplique normalizar nos dois lados da comparação: no termo digitado e no texto pesquisado. Sem isso, quem digita "seguranca" no celular não encontra "Segurança" — e desiste.

9.3 debounce: não filtrar a cada tecla

Digitar "acessibilidade" dispara catorze eventos input. Filtrar e redesenhar catorze vezes trava a interface em listas grandes. O debounce da Aula 13 resolve: a função só executa quando os disparos param por um tempo.

JavaScript
function debounce(fn, atraso = 300) {
  let temporizador;
  return function (...args) {
    clearTimeout(temporizador);
    temporizador = setTimeout(() => fn.apply(this, args), atraso);
  };
}

Entre 250 ms e 400 ms é a faixa que soa instantânea sem disparar demais. Abaixo de 150 ms você perde o efeito; acima de 600 ms o usuário sente atraso.

9.4 Estado vazio e contador

Toda listagem filtrável precisa de duas coisas que iniciantes esquecem:

  • Uma mensagem de estado vazio que diga o que fazer ("Nenhuma atividade encontrada para 'xyz'. Tente outro termo ou limpe os filtros."), e não uma tela em branco.
  • Um contador anunciado (aria-live="polite") informando quantos resultados apareceram. Quem usa leitor de tela não vê a lista encolher; sem o contador, digitar na busca não produz nenhum retorno perceptível.
HTML
<p id="contador-programacao" class="contador" aria-live="polite" role="status"></p>

🔎 Por baixo do capô aria-live="polite" cria uma região viva: o leitor de tela passa a observar aquele elemento e anuncia mudanças de conteúdo assim que terminar de ler o que estava lendo. assertive interrompe na hora — reserve para erros graves. A região precisa existir no HTML antes de ser preenchida: se você criar o elemento e já colocar o texto dentro, muitos leitores não anunciam nada, porque não havia região para observar. É por isso que o <p> vazio já vem no HTML.

💻 Mão na massa — Inscrição validada e programação com busca

Ao fim destes oito passos, o site da Semana Acadêmica de Sistemas de Informação terá um formulário de inscrição que valida campo a campo com mensagens acessíveis, salva rascunho e mostra confirmação; e uma página de programação com busca em tempo real, filtro por área e ordenação — tudo sobre o array palestras do js/dados.js da Aula 12, sem mudar uma linha daquele arquivo.

⚠️ Atenção js/dados.js é a fonte única de dados do projeto desde a Aula 12, e o esquema dele é lei: palestras com area, hora e palestranteId; o array palestrantes; o dicionário nomesDasAreas. O js/relatorios.js (Aula 12) e o js/palestrantes.js (Aula 13) já dependem desses nomes. Hoje você adapta a consulta aos dados, não os dados à consulta.

Passo 1 — o formulário de inscricao.html

Primeiro o <head>, com os quatro scripts na ordem que importa — defer executa na ordem das tags, e inscricao.js precisa que debounce (do app.js, Passo 4) já exista:

inscricao.html<head> completo

HTML
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Inscrição gratuita na Semana Acadêmica de Sistemas de Informação.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Inscrição — Semana Acadêmica de Sistemas de Informação</title>
  <link rel="stylesheet" href="css/estilo.css">
  <script src="js/menu.js" defer></script>
  <script src="js/dados.js" defer></script>
  <script src="js/app.js" defer></script>
  <script src="js/inscricao.js" defer></script>
</head>

Agora o <main>. A estrutura repete o mesmo bloco para cada campo: <label>, campo, <span class="erro" role="alert">.

⚠️ Atenção A <section class="vagas"> das Aulas 10 e 11 — com #vagas-restantes, #vagas-totais, #percentual-ocupacao, #aviso-vagas e #valor-taxafica onde está, acima do formulário. É ela que mostra o "Últimas vagas!" exigido no Checkpoint da Aula 11, e o código que a alimenta continua no js/inscricao.js (você o amplia no Passo 3, não o substitui). O que muda de lugar é só o <form>.

inscricao.html — conteúdo do <main>

HTML
<main id="conteudo" tabindex="-1" class="container">
  <h1>Inscrição</h1>
  <p>Preencha os dados abaixo. Os campos marcados com * são obrigatórios.</p>

  <!-- Seção de vagas das Aulas 10 e 11: continua exatamente como estava -->
  <section class="vagas">
    <h2>Vagas</h2>
    <p>
      Restam <strong id="vagas-restantes"></strong> de
      <span id="vagas-totais"></span> vagas
      (<span id="percentual-ocupacao"></span>% ocupadas).
    </p>

    <p id="aviso-vagas" class="aviso" role="status"></p>

    <p class="taxa">
      Taxa de inscrição: <strong id="valor-taxa"></strong>
      <span id="observacao-taxa"></span>
    </p>
  </section>

  <p id="aviso-formulario" class="aviso-formulario" role="status"></p>

  <form id="form-inscricao" novalidate>
    <fieldset>
      <legend>Seus dados</legend>

      <div class="campo">
        <label for="nome">Nome completo *</label>
        <input type="text" id="nome" name="nome" autocomplete="name"
               required minlength="5" maxlength="80">
        <span class="erro" id="erro-nome" role="alert"></span>
      </div>

      <div class="campo">
        <label for="email">E-mail *</label>
        <input type="email" id="email" name="email" autocomplete="email" required>
        <span class="erro" id="erro-email" role="alert"></span>
      </div>

      <div class="campo">
        <label for="cpf">CPF *</label>
        <input type="text" id="cpf" name="cpf" inputmode="numeric"
               maxlength="14" placeholder="000.000.000-00" required>
        <span class="erro" id="erro-cpf" role="alert"></span>
      </div>

      <div class="campo">
        <label for="telefone">Telefone com DDD *</label>
        <input type="text" id="telefone" name="telefone" inputmode="tel"
               maxlength="15" placeholder="(66) 99999-9999" required>
        <span class="erro" id="erro-telefone" role="alert"></span>
      </div>

      <div class="campo">
        <label for="nascimento">Data de nascimento *</label>
        <input type="text" id="nascimento" name="nascimento" inputmode="numeric"
               maxlength="10" placeholder="dd/mm/aaaa" required>
        <span class="erro" id="erro-nascimento" role="alert"></span>
      </div>
    </fieldset>

    <fieldset>
      <legend>Participação</legend>

      <div class="campo">
        <label for="curso">Curso ou vínculo *</label>
        <select id="curso" name="curso" required>
          <option value="">Selecione…</option>
          <option value="si">Sistemas de Informação</option>
          <option value="ads">Análise e Desenvolvimento de Sistemas</option>
          <option value="eng">Engenharias</option>
          <option value="outro">Outro curso</option>
          <option value="externo">Comunidade externa</option>
        </select>
        <span class="erro" id="erro-curso" role="alert"></span>
      </div>

      <div class="campo">
        <span class="rotulo-grupo" id="rotulo-atividades">Atividades de interesse *</span>
        <div class="grupo-caixas" role="group" aria-labelledby="rotulo-atividades">
          <label><input type="checkbox" name="atividades" value="git"> Minicurso de Git</label>
          <label><input type="checkbox" name="atividades" value="acessibilidade"> Minicurso de acessibilidade</label>
          <label><input type="checkbox" name="atividades" value="maratona"> Maratona de programação</label>
          <label><input type="checkbox" name="atividades" value="palestras"> Somente palestras</label>
        </div>
        <span class="erro" id="erro-atividades" role="alert"></span>
      </div>

      <div class="campo">
        <label class="linha"><input type="checkbox" id="termos" name="termos" required>
          Li e aceito o regulamento do evento *</label>
        <span class="erro" id="erro-termos" role="alert"></span>
      </div>
    </fieldset>

    <div class="acoes-formulario">
      <button type="submit" class="botao">Enviar inscrição</button>
      <button type="button" class="botao botao--contorno" id="limpar-rascunho">Limpar rascunho</button>
    </div>
  </form>
</main>

Quatro decisões que valem nota: novalidate no formulário (as mensagens são nossas), inputmode nos campos numéricos (o celular abre o teclado certo sem mudar o type, como na Aula 04), o grupo de checkboxes dentro de um role="group" com aria-labelledby — assim o leitor de tela anuncia "Atividades de interesse, grupo" antes das opções — e a classe .aviso-formulario no parágrafo de resposta do envio. Ela não é a .aviso da Aula 11: aquela é a caixa vermelha de "Últimas vagas!", com .aviso:empty { display: none }, e continua sendo usada pelo #aviso-vagas logo acima. Dois componentes diferentes, dois nomes diferentes — reaproveitar o nome faria as duas regras brigarem no mesmo estilo.css.

Passo 2 — o CSS dos campos

Acrescente ao fim de css/estilo.css o bloco da seção 7 e mais estes complementos:

css/estilo.css — complementos do formulário

CSS
.grupo-caixas {
  display: grid;
  gap: 0.4rem;
}

.grupo-caixas label,
.campo label.linha {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 400;
}

.rotulo-grupo {
  font-weight: 600;
}

.acoes-formulario {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1rem;
}

.aviso-formulario {
  min-height: 1.5em;
}

.aviso-formulario.sucesso {
  padding: 0.75rem 1rem;
  border-left: 4px solid #27ae60;
  background: #eafaf1;
  color: #14532d;
}

.aviso-formulario.falha {
  padding: 0.75rem 1rem;
  border-left: 4px solid #c0392b;
  background: #fdecea;
  color: #7f1d1d;
}

Passo 3 — js/inscricao.js ampliado

O js/inscricao.js já existe desde a Aula 10 e cresceu na Aula 11: é ele que calcula as vagas restantes, o percentual de ocupação, o aviso "Últimas vagas!" e o valor da taxa. Esse bloco não sai — ele continua no topo do arquivo, sob o comentário // ===== VAGAS (Aulas 10 e 11) =====, e tudo o que você escreve hoje vem depois dele.

Este arquivo usa a função debounce para salvar o rascunho; ela é declarada no js/app.js no Passo 4, que toda página já carrega antes deste script.

js/inscricao.js (topo do arquivo — o que já existe, apenas identificado com um comentário)

JavaScript
// js/inscricao.js — vagas (Aulas 10 e 11) e validação do formulário (Aula 14)

// ===== VAGAS (Aulas 10 e 11) =====
// As constantes VAGAS_TOTAIS e INSCRITOS, o cálculo do percentual, o aviso
// de "Últimas vagas!" e o valor da taxa continuam exatamente como estavam.
// Nada abaixo depende deles, e nada deles depende do que vem abaixo.

js/inscricao.js (acrescente a partir daqui)

JavaScript
// ===== ESTADO =====
const CHAVE_RASCUNHO = "inscricao:rascunho";
const CAMPOS_DO_RASCUNHO = ["nome", "email", "curso"]; // sem CPF e sem telefone, de propósito

// ===== ELEMENTOS =====
const els = {
  form: document.querySelector("#form-inscricao"),
  aviso: document.querySelector("#aviso-formulario"),
  limpar: document.querySelector("#limpar-rascunho"),
};

// ===== PADRÕES =====
const PADROES = {
  telefone: /^\(?\d{2}\)?\s?9?\d{4}-?\d{4}$/,
  email: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/,
  data: /^(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/,
};

// ===== VALIDADORES (funções puras) =====
function validarNomeCompleto(valor) {
  const partes = valor.trim().split(/\s+/);
  if (valor.trim() === "") return "O nome completo é obrigatório.";
  if (partes.length < 2) return "Informe nome e sobrenome.";
  if (partes.some((parte) => parte.length < 2)) {
    return "Cada parte do nome deve ter ao menos 2 letras.";
  }
  return "";
}

function validarEmail(valor) {
  const v = valor.trim();
  if (v === "") return "O e-mail é obrigatório.";
  if (!PADROES.email.test(v)) return "Informe um e-mail no formato nome@dominio.br.";
  return "";
}

function validarTelefone(valor) {
  const v = valor.trim();
  if (v === "") return "O telefone é obrigatório.";
  if (!PADROES.telefone.test(v)) return "Informe o telefone com DDD, como (66) 99999-9999.";
  return "";
}

function validarCPF(cpf) {
  const numeros = cpf.replace(/\D/g, "");
  if (numeros === "") return "O CPF é obrigatório.";
  if (numeros.length !== 11) return "O CPF deve ter 11 dígitos.";
  if (/^(\d)\1{10}$/.test(numeros)) return "CPF inválido.";

  const calcularDigito = (base, pesoInicial) => {
    let soma = 0;
    for (let i = 0; i < base.length; i++) {
      soma = soma + Number(base[i]) * (pesoInicial - i);
    }
    const resto = (soma * 10) % 11;
    return resto === 10 ? 0 : resto;
  };

  const primeiro = calcularDigito(numeros.slice(0, 9), 10);
  const segundo = calcularDigito(numeros.slice(0, 10), 11);

  if (primeiro !== Number(numeros[9]) || segundo !== Number(numeros[10])) {
    return "CPF inválido. Confira os números digitados.";
  }
  return "";
}

function calcularIdade(dataTexto) {
  const partes = dataTexto.trim().split("/");
  if (partes.length !== 3) return null;

  const dia = Number(partes[0]);
  const mes = Number(partes[1]);
  const ano = Number(partes[2]);
  const data = new Date(ano, mes - 1, dia);

  const existe =
    data.getFullYear() === ano && data.getMonth() === mes - 1 && data.getDate() === dia;
  if (!existe) return null;

  const hoje = new Date();
  let anos = hoje.getFullYear() - ano;
  const jaFez =
    hoje.getMonth() > mes - 1 || (hoje.getMonth() === mes - 1 && hoje.getDate() >= dia);
  if (!jaFez) anos = anos - 1;
  return anos;
}

function validarNascimento(valor) {
  const v = valor.trim();
  if (v === "") return "A data de nascimento é obrigatória.";
  if (!PADROES.data.test(v)) return "Use o formato dd/mm/aaaa.";
  const anos = calcularIdade(v);
  if (anos === null) return "Essa data não existe no calendário.";
  if (anos < 0) return "A data de nascimento não pode estar no futuro.";
  if (anos < 16) return "É necessário ter ao menos 16 anos para se inscrever.";
  if (anos > 120) return "Confira o ano digitado.";
  return "";
}

function validarCurso(valor) {
  return valor === "" ? "Escolha o seu curso ou vínculo." : "";
}

function validarAtividades() {
  const marcadas = els.form.querySelectorAll("input[name='atividades']:checked");
  return marcadas.length === 0 ? "Escolha ao menos uma atividade." : "";
}

function validarTermos(marcado) {
  return marcado ? "" : "É preciso aceitar o regulamento para se inscrever.";
}

// ===== MÁSCARAS =====
function mascaraCPF(valor) {
  return valor
    .replace(/\D/g, "")
    .slice(0, 11)
    .replace(/(\d{3})(\d)/, "$1.$2")
    .replace(/(\d{3})(\d)/, "$1.$2")
    .replace(/(\d{3})(\d{1,2})$/, "$1-$2");
}

function mascaraTelefone(valor) {
  return valor
    .replace(/\D/g, "")
    .slice(0, 11)
    .replace(/(\d{2})(\d)/, "($1) $2")
    .replace(/(\d{5})(\d)/, "$1-$2");
}

function mascaraData(valor) {
  return valor
    .replace(/\D/g, "")
    .slice(0, 8)
    .replace(/(\d{2})(\d)/, "$1/$2")
    .replace(/(\d{2})(\d)/, "$1/$2");
}

// ===== EXIBIÇÃO =====
function mostrarErro(campo, mensagem) {
  const alvo = document.querySelector(`#erro-${campo.id}`);
  if (!alvo) return;

  if (mensagem) {
    campo.classList.add("invalido");
    campo.classList.remove("valido");
    campo.setAttribute("aria-invalid", "true");
    campo.setAttribute("aria-describedby", `erro-${campo.id}`);
    alvo.textContent = mensagem;
  } else {
    campo.classList.remove("invalido");
    campo.classList.add("valido");
    campo.setAttribute("aria-invalid", "false");
    campo.removeAttribute("aria-describedby");
    alvo.textContent = "";
  }
}

function mostrarAviso(texto, tipo) {
  els.aviso.textContent = texto;
  els.aviso.className = texto ? `aviso-formulario ${tipo}` : "aviso-formulario";
}

// ===== ORQUESTRAÇÃO =====
function validarCampo(campo) {
  let mensagem = "";

  switch (campo.id) {
    case "nome":
      mensagem = validarNomeCompleto(campo.value);
      break;
    case "email":
      mensagem = validarEmail(campo.value);
      break;
    case "cpf":
      mensagem = validarCPF(campo.value);
      break;
    case "telefone":
      mensagem = validarTelefone(campo.value);
      break;
    case "nascimento":
      mensagem = validarNascimento(campo.value);
      break;
    case "curso":
      mensagem = validarCurso(campo.value);
      break;
    case "termos":
      mensagem = validarTermos(campo.checked);
      break;
    default:
      mensagem = "";
  }

  if (campo.name === "atividades") {
    const grupo = document.querySelector("#erro-atividades");
    grupo.textContent = validarAtividades();
    return grupo.textContent === "";
  }

  mostrarErro(campo, mensagem);
  return mensagem === "";
}

function camposValidaveis() {
  return [...els.form.querySelectorAll("#nome, #email, #cpf, #telefone, #nascimento, #curso, #termos")];
}

// ===== RASCUNHO (localStorage) =====
function salvarRascunho() {
  const dados = {};
  CAMPOS_DO_RASCUNHO.forEach((nome) => {
    dados[nome] = els.form.elements[nome].value;
  });
  localStorage.setItem(CHAVE_RASCUNHO, JSON.stringify(dados));
}

function restaurarRascunho() {
  const bruto = localStorage.getItem(CHAVE_RASCUNHO);
  if (!bruto) return;

  const dados = JSON.parse(bruto);
  CAMPOS_DO_RASCUNHO.forEach((nome) => {
    if (dados[nome]) els.form.elements[nome].value = dados[nome];
  });
  mostrarAviso("Recuperamos o rascunho que você tinha começado.", "sucesso");
}

function limparRascunho() {
  localStorage.removeItem(CHAVE_RASCUNHO);
  els.form.reset();
  camposValidaveis().forEach((campo) => {
    campo.classList.remove("valido", "invalido");
    campo.removeAttribute("aria-invalid");
    mostrarErro(campo, "");
  });
  document.querySelector("#erro-atividades").textContent = "";
  mostrarAviso("Rascunho apagado.", "sucesso");
}

// ===== EVENTOS =====
function registrarEventos() {
  // Máscaras
  document.querySelector("#cpf").addEventListener("input", (e) => {
    e.target.value = mascaraCPF(e.target.value);
  });
  document.querySelector("#telefone").addEventListener("input", (e) => {
    e.target.value = mascaraTelefone(e.target.value);
  });
  document.querySelector("#nascimento").addEventListener("input", (e) => {
    e.target.value = mascaraData(e.target.value);
  });

  // Valide no blur; depois do primeiro erro, valide também no input
  camposValidaveis().forEach((campo) => {
    campo.addEventListener("blur", () => {
      validarCampo(campo);
      campo.dataset.tocado = "true";
    });
    campo.addEventListener("input", () => {
      if (campo.dataset.tocado === "true") validarCampo(campo);
    });
  });

  // Grupo de checkboxes: um ouvinte só, por delegação
  els.form.addEventListener("change", (e) => {
    if (e.target.name === "atividades") validarCampo(e.target);
  });

  // Rascunho: salva 500 ms depois de o usuário parar de digitar
  els.form.addEventListener("input", debounce(salvarRascunho, 500));
  els.limpar.addEventListener("click", limparRascunho);

  // Envio
  els.form.addEventListener("submit", (e) => {
    e.preventDefault();

    let primeiroInvalido = null;
    camposValidaveis().forEach((campo) => {
      const ok = validarCampo(campo);
      campo.dataset.tocado = "true";
      if (!ok && !primeiroInvalido) primeiroInvalido = campo;
    });

    const erroAtividades = validarAtividades();
    document.querySelector("#erro-atividades").textContent = erroAtividades;

    if (primeiroInvalido || erroAtividades) {
      mostrarAviso("Confira os campos destacados antes de enviar.", "falha");
      const alvo = primeiroInvalido || document.querySelector("input[name='atividades']");
      alvo.focus();
      alvo.scrollIntoView({ behavior: "smooth", block: "center" });
      return;
    }

    const dados = Object.fromEntries(new FormData(els.form).entries());
    dados.atividades = new FormData(els.form).getAll("atividades");
    dados.cpf = dados.cpf.replace(/\D/g, ""); // sem máscara, como o servidor espera
    dados.telefone = dados.telefone.replace(/\D/g, "");
    console.log("Inscrição válida:", dados);

    localStorage.removeItem(CHAVE_RASCUNHO);
    els.form.reset();
    camposValidaveis().forEach((campo) => campo.classList.remove("valido", "invalido"));
    mostrarAviso(
      `Inscrição de ${dados.nome} registrada! Você receberá a confirmação em ${dados.email}.`,
      "sucesso"
    );
  });
}

// ===== INICIALIZAÇÃO =====
function iniciar() {
  if (!els.form) return;
  registrarEventos();
  restaurarRascunho();
}

iniciar();

Passo 4 — debounce disponível em todas as páginas

O debounce é usado pelo rascunho e, no próximo passo, pela busca. Como todas as páginas carregam js/app.js antes do script específico (os dois com defer, e defer preserva a ordem das tags), basta declarar a função lá uma vez. Confira que o <script src="js/app.js" defer> está mesmo no <head> das cinco páginas — inclusive em programacao.html, que vai usar o debounce no Passo 7. Sem ele, a busca morre no primeiro caractere digitado com Uncaught ReferenceError: debounce is not defined.

js/app.js — acrescente ao fim

JavaScript
// Utilitário compartilhado: adia a execução até os disparos pararem por X ms.
// Como este arquivo é carregado antes dos scripts de página, a função fica
// disponível para inscricao.js e programacao.js.
function debounce(fn, atraso = 300) {
  let temporizador;
  return function (...args) {
    clearTimeout(temporizador);
    temporizador = setTimeout(() => fn.apply(this, args), atraso);
  };
}

Passo 5 — conferir (e não tocar) o js/dados.js

Não há arquivo novo aqui: o js/dados.js da Aula 12 já tem tudo de que a consulta precisa. Abra-o e confirme os três nomes que os próximos passos vão usar:

Nome O que é Campos usados hoje
palestras array com as 12 atividades id, titulo, tipo, area, dia, hora, local, vagas, inscritos, palestranteId
palestrantes array com as 6 pessoas id, nome, instituicao
nomesDasAreas dicionário código → nome web, dados, seguranca, ia

Duas armadilhas que este passo evita:

  • O campo do horário chama hora, não horario. Um p.horario devolve undefined, e undefined.localeCompare(…) derruba a ordenação inteira com TypeError.
  • A palestra não guarda o nome de quem apresenta, só o palestranteId. Para buscar por nome, você precisa cruzar as duas listas — exatamente o que o relatório 7 da Aula 12 fazia com find. É a mesma ideia de chave estrangeira que você verá no Nível 2.

Se você sentir vontade de "só ajustar um campinho" no dados.js para simplificar o filtro, resista: o js/relatorios.js (Aula 12) e o js/palestrantes.js (Aula 13) leem esse mesmo arquivo e quebram junto. Adaptar o consumidor ao dado é barato; adaptar o dado a um consumidor é como se perde a fonte única.

Passo 6 — a interface de consulta em programacao.html

Troque os cartões escritos à mão (Aula 07) por um contêiner vazio e os controles de consulta. Antes, o <head> com os cinco scripts:

programacao.html<head> completo

HTML
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Programação completa da Semana Acadêmica de Sistemas de Informação.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <title>Programação — Semana Acadêmica de Sistemas de Informação</title>
  <link rel="stylesheet" href="css/estilo.css">
  <script src="js/menu.js" defer></script>
  <script src="js/dados.js" defer></script>
  <script src="js/app.js" defer></script>
  <script src="js/relatorios.js" defer></script>
  <script src="js/programacao.js" defer></script>
</head>

O relatorios.js da Aula 12 continua aqui: ele só escreve no Console e não atrapalha nada. E repare que js/app.js vem antes de js/programacao.js: é dele que sai o debounce do Passo 4.

programacao.html — conteúdo do <main>

HTML
<main id="conteudo" tabindex="-1" class="container">
  <h1>Programação</h1>
  <p>Três dias de palestras, minicursos e maratona. Use a busca e os filtros para achar sua atividade.</p>

  <form class="consulta" id="consulta-programacao" role="search">
    <div class="campo">
      <label for="busca">Buscar por título ou palestrante</label>
      <input type="search" id="busca" name="busca" placeholder="Ex.: acessibilidade"
             autocomplete="off">
    </div>

    <div class="campo">
      <label for="area">Área</label>
      <select id="area" name="area">
        <option value="todas">Todas as áreas</option>
        <option value="web">Desenvolvimento Web</option>
        <option value="dados">Ciência de Dados</option>
        <option value="seguranca">Segurança</option>
        <option value="ia">Inteligência Artificial</option>
      </select>
    </div>

    <div class="campo">
      <label for="ordenacao">Ordenar por</label>
      <select id="ordenacao" name="ordenacao">
        <option value="hora">Dia e horário</option>
        <option value="titulo">Título (A–Z)</option>
        <option value="vagas">Vagas restantes</option>
      </select>
    </div>

    <button type="button" class="botao botao--contorno" id="limpar-consulta">Limpar filtros</button>
  </form>

  <p id="contador-programacao" class="contador" role="status" aria-live="polite"></p>

  <ul id="lista-programacao" class="cartoes"></ul>
</main>

O <form role="search"> não envia nada: ele existe para agrupar os controles semanticamente (leitores de tela anunciam "busca") e para o reset() do botão "Limpar filtros" funcionar de graça. Os cinco <script> precisam estar na ordem do <head> acima: dados.js declara palestras e palestrantes, app.js declara debounce, e só então programacao.js usa os três.

Passo 7 — js/programacao.js

Ele consome palestras, palestrantes e nomesDasAreas do js/dados.js — sem redeclarar nada — e debounce do js/app.js. Os nomes das constantes e funções levam o sufixo Programacao pelo mesmo motivo da Aula 13: scripts sem type="module" dividem o escopo global, e um els ou um renderizar genérico colide com o do arquivo vizinho no dia em que as duas páginas carregarem os dois.

js/programacao.js

JavaScript
// js/programacao.js — busca, filtro e ordenação da programação.
// Depende de js/dados.js (palestras, palestrantes, nomesDasAreas) e de
// js/app.js (debounce), ambos carregados antes deste arquivo.

// ===== ESTADO =====
let termoBusca = "";
let areaAtualProgramacao = "todas";
let ordenacaoAtual = "hora";

// ===== ELEMENTOS =====
const elementosProgramacao = {
  form: document.querySelector("#consulta-programacao"),
  busca: document.querySelector("#busca"),
  area: document.querySelector("#area"),
  ordenacao: document.querySelector("#ordenacao"),
  limpar: document.querySelector("#limpar-consulta"),
  lista: document.querySelector("#lista-programacao"),
  contador: document.querySelector("#contador-programacao"),
};

// ===== FUNÇÕES DE DADOS =====
function normalizar(texto) {
  return texto
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .toLowerCase()
    .trim();
}

/**
 * Cruza a palestra com o array de palestrantes pelo palestranteId.
 * É a mesma junção do relatório 7 da Aula 12: `find` + `?.` + `??`.
 * @param {Object} palestra - item de `palestras`
 * @returns {string} o nome de quem apresenta, ou "A definir"
 */
function nomeDoPalestrante(palestra) {
  const pessoa = palestrantes.find((p) => p.id === palestra.palestranteId);
  return pessoa?.nome ?? "A definir";
}

function vagasRestantes(palestra) {
  return palestra.vagas - palestra.inscritos;
}

function obterPalestrasVisiveis() {
  let resultado = [...palestras]; // cópia: sort() altera o array original

  if (termoBusca) {
    const termo = normalizar(termoBusca);
    resultado = resultado.filter(
      (p) =>
        normalizar(p.titulo).includes(termo) ||
        normalizar(nomeDoPalestrante(p)).includes(termo) ||
        normalizar(p.local).includes(termo)
    );
  }

  if (areaAtualProgramacao !== "todas") {
    resultado = resultado.filter((p) => p.area === areaAtualProgramacao);
  }

  const ordenadores = {
    hora: (a, b) => a.dia - b.dia || a.hora.localeCompare(b.hora),
    titulo: (a, b) => a.titulo.localeCompare(b.titulo, "pt-BR"),
    vagas: (a, b) => vagasRestantes(b) - vagasRestantes(a),
  };
  resultado.sort(ordenadores[ordenacaoAtual]);

  return resultado;
}

// ===== RENDERIZAÇÃO =====
function criarCartaoDePalestra(palestra) {
  const item = document.createElement("li");
  item.classList.add("cartao");
  item.dataset.id = palestra.id;

  const restantes = vagasRestantes(palestra);
  if (restantes === 0) {
    const selo = document.createElement("span");
    selo.classList.add("cartao__selo");
    selo.textContent = "Esgotado";
    item.appendChild(selo);
  }

  const titulo = document.createElement("h2");
  titulo.textContent = palestra.titulo;

  const meta = document.createElement("p");
  meta.classList.add("cartao__meta");
  meta.textContent = `Dia ${palestra.dia} · ${palestra.hora} · ${palestra.local}`;

  const quem = document.createElement("p");
  quem.textContent = nomeDoPalestrante(palestra);

  const etiqueta = document.createElement("span");
  etiqueta.classList.add("etiqueta");
  etiqueta.textContent = nomesDasAreas[palestra.area];

  const vagas = document.createElement("p");
  vagas.classList.add("cartao__vagas");
  vagas.textContent =
    restantes === 0 ? "Sem vagas — entre na lista de espera" : `${restantes} vagas restantes`;

  item.append(titulo, meta, quem, etiqueta, vagas);
  return item;
}

function renderizarProgramacao() {
  const visiveis = obterPalestrasVisiveis();
  elementosProgramacao.lista.innerHTML = "";

  if (visiveis.length === 0) {
    const vazio = document.createElement("li");
    vazio.classList.add("vazio");
    vazio.textContent = termoBusca
      ? `Nenhuma atividade encontrada para "${termoBusca}". Tente outro termo ou limpe os filtros.`
      : "Nenhuma atividade nesta área. Escolha outra ou limpe os filtros.";
    elementosProgramacao.lista.appendChild(vazio);
  } else {
    const fragmento = document.createDocumentFragment();
    visiveis.forEach((p) => fragmento.appendChild(criarCartaoDePalestra(p)));
    elementosProgramacao.lista.appendChild(fragmento);
  }

  elementosProgramacao.contador.textContent =
    visiveis.length === 1 ? "1 atividade encontrada" : `${visiveis.length} atividades encontradas`;
}

// ===== EVENTOS =====
function registrarEventosDaProgramacao() {
  elementosProgramacao.busca.addEventListener(
    "input",
    debounce((e) => {
      termoBusca = e.target.value;
      renderizarProgramacao();
    }, 300)
  );

  elementosProgramacao.area.addEventListener("change", (e) => {
    areaAtualProgramacao = e.target.value;
    renderizarProgramacao();
  });

  elementosProgramacao.ordenacao.addEventListener("change", (e) => {
    ordenacaoAtual = e.target.value;
    renderizarProgramacao();
  });

  elementosProgramacao.limpar.addEventListener("click", () => {
    elementosProgramacao.form.reset();
    termoBusca = "";
    areaAtualProgramacao = "todas";
    ordenacaoAtual = "hora";
    renderizarProgramacao();
    elementosProgramacao.busca.focus();
  });
}

// ===== INICIALIZAÇÃO =====
function iniciarProgramacao() {
  if (!elementosProgramacao.lista) return;
  registrarEventosDaProgramacao();
  renderizarProgramacao();
}

iniciarProgramacao();

Passo 8 — o CSS da consulta

css/estilo.css — acrescente ao fim

CSS
/* ===== Consulta da programação ===== */
.consulta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  align-items: end;
  gap: 1rem;
  margin-block: 1.5rem;
  padding: 1rem;
  border-radius: 12px;
  background: var(--cor-fundo);
  border: 1px solid var(--cor-borda);
}

.consulta .campo {
  margin-bottom: 0;
}

.cartao__vagas {
  font-size: 0.875rem;
}

.contador {
  font-weight: 600;
}

Como testar

  1. Abra inscricao.html com o Live Server. Clique no campo "Nome completo", saia sem digitar: aparece "O nome completo é obrigatório." em vermelho, com o símbolo de alerta, e a borda fica vermelha. Digite "Ana": a mensagem muda para "Informe nome e sobrenome." enquanto você digita — porque o campo já foi tocado.
  2. Digite qualquer sequência de 11 dígitos no CPF: a máscara aplica pontos e traço sozinha. Teste 111.111.111-11 (rejeitado por repetição) e 123.456.789-00 (rejeitado pelos dígitos verificadores). Um CPF válido de verdade deixa a borda verde.
  3. No campo de nascimento, digite 31/02/2000: "Essa data não existe no calendário." Digite uma data que resulte em menos de 16 anos: "É necessário ter ao menos 16 anos para se inscrever."
  4. Clique em "Enviar inscrição" com o formulário vazio: todos os erros aparecem de uma vez, o foco vai para o primeiro campo inválido, a página rola até ele e o aviso do topo diz "Confira os campos destacados antes de enviar."
  5. Preencha tudo corretamente e envie: o console mostra o objeto com cpf e telefone sem máscara, atividades como array, e a página exibe a mensagem verde de sucesso.
  6. Preencha só o nome e o e-mail e recarregue a página (F5dronize a entrada do validador (por exemplo, sempre o elemento) para não precisar de exceções.
  7. Compare o resultado com o criarValidador do exercício C1: você acabou de construir metade dele. ): os valores voltam e o aviso diz que o rascunho foi recuperado. Confira no DevTools → Application → Local Storage que a chave inscricao:rascunho não contém CPF nem telefone. Clique em "Limpar rascunho" e recarregue: o formulário volta vazio.
  8. Abra programacao.html: os doze cartões do dados.js aparecem ordenados por dia e horário, e o contador diz "12 atividades encontradas". Cada cartão mostra o nome de quem apresenta — resolvido pelo palestranteId, não digitado.
  9. Digite seguranca (sem cedilha e sem acento) na busca: aparece "Segurança em aplicações web: dez erros comuns". Digite carla: aparecem as duas atividades de Carla Mendes, provando que a busca cruza as duas listas. Digite xyz: a mensagem de estado vazio explica o que fazer.
  10. Mude "Ordenar por" para "Vagas restantes": o minicurso de Git e o de redes neurais (ambos esgotados) vão para o fim e ganham o selo "Esgotado". Mude para "Título (A–Z)" e confira que os acentos ficam na ordem correta. No filtro de área, "Segurança" deixa duas atividades e "Inteligência Artificial", outras duas.
  11. Percorra a página inteira com Tabcê já fez Tags: acessibilidade, formularios, investigacao, projeto

Um formulário pode passar em todos os testes automáticos e ainda ser impossível de preencher sem o mouse. Nesta missão você vai auditar o formulário do seu projeto autoral como um usuário que não enxerga a tela, corrigir o que encontrar e provar o resultado. Isso entra bem no Marco 3 da unidade.

Critérios de pronto

  • Um vídeo de até três minutos preenchendo e enviando o formulário só com o teclado, com o monitor desligado ou os olhos fechados, usando o leitor de tela do sistema (NVDA no Windows, Orca no Linux, VoiceOver no macOS).
  • O formulário tem um resumo de erros no topo, ligado por link a cada campo com problema, que recebe foco ao falhar o envio.
  • Todo campo tem rótulo associado, texto de ajuda ligado por aria-describedby e mensagem por role="alert".
  • A aba Lighthouse do DevTools marca 100 em Acessibilidade na página do formulário, e a captura está no relatório.
  • Um arquivo acessibilidade.md lista os problemas encontrados, a correção aplicada e — o item mais importante — dois problemas que o Lighthouse não apontou e que só a navegação por teclado revelou.

: todo campo tem foco visível, o botão "Limpar filtros" devolve o foco à busca, e o console está sem nenhum erro em vermelho nas cinco páginas — em especial, nenhum ReferenceError: debounce is not defined e nenhum Identifier … has already been declared. 11. Ainda em inscricao.html, confira que a seção de vagas continua no topo, com o número calculado e o aviso "Últimas vagas!" das Aulas 10 e 11 — e que ele é visualmente diferente do aviso verde/vermelho do envio do formulário.

🧪 Laboratório

Nível A — Fixação

A1. Por que event.preventDefault() é obrigatório no ouvinte de submit quando o formulário é processado em JavaScript? O que acontece exatamente sem ele?

A2. Cite as três formas de ler os valores de um formulário e uma vantagem de cada uma.

A3. O que input.value devolve em um campo type="number"? Escreva a conversão correta e diga o que acontece se o campo estiver vazio.

A4. Diferencie os eventos input, change e blur para fins de validação. Qual deles não borbulha, e qual é a alternativa borbulhante?

A5. Para que serve o atributo novalidate no <form>? Por que mantemos required e pattern no HTML mesmo assim?

A6. O que fazem campo.checkValidity(), campo.reportValidity() e campo.setCustomValidity()? Qual armadilha o terceiro traz?

A7. Por que sinalizar erro apenas pela cor da borda é um problema de acessibilidade? Cite duas formas de resolver.

A8. Para que servem role="alert", aria-invalid e aria-describedby na exibição de erros? O que cada um muda para quem usa leitor de tela?

A9. Reescreva como mensagem útil: "Erro no campo 3". Invente o contexto que preferir e justifique a sua escolha em uma linha.

A10. Por que a validação no navegador não substitui a validação no servidor? Descreva, em duas frases, como alguém burlaria a sua validação.

A11. O que faz Object.fromEntries(new FormData(form).entries())? Cite duas limitações do FormData.

A12. Por que a máscara precisa ser removida antes de validar e de enviar o dado?

A13. O que fazem as flags g, i e m de uma expressão regular?

A14. Qual a diferença entre \d, \w e \s? E entre \d e \D?

A15. O que casa cada padrão: /a+/, /a*/, /a?/, /a{2,4}/? Dê um exemplo de string aceita e uma rejeitada para cada.

A16. Diferencie quantificador guloso de preguiçoso, com um exemplo próprio.

A17. Qual a diferença entre /\d{3}/ e /^\d{3}$/? Por que isso é crítico em validação?

A18. O que faz \b? Dê um exemplo em que a presença dele muda o resultado.

A19. Diferencie (abc) de (?:abc). Quando você usaria o segundo?

A20. O que faz o lookahead (?=…)? Escreva uma regex que aceite apenas strings que contenham ao menos um dígito, em qualquer posição.

A21. Escreva a regex para: (a) somente dígitos; (b) somente letras, com acento; (c) alfanumérico de 6 a 12 caracteres; (d) começa com maiúscula; (e) termina com .pdf.

A22. Diferencie test(), match() e matchAll(). Qual deles exige a flag g?

A23. Escreva o replace que converta "07/05/1998" em "1998-05-07", usando grupos capturados.

A24. Por que a validação de e-mail por regex não deve ser rigorosa demais? Qual é a estratégia profissional recomendada?

A25. O que a regex não consegue validar em um CPF? E em uma data?

A26. O que é ReDoS e qual padrão de escrita costuma causá-lo?

A27. Por que [...palestras] aparece antes do sort() em obterPalestrasVisiveis()? Descreva o bug que surge sem essa cópia.

A28. O que localeCompare(b, "pt-BR") resolve que o sort() puro não resolve? Dê um exemplo com nomes acentuados.

A29. Explique, linha a linha, o que a função normalizar() faz com a string "Segurança".

A30. Por que o <p> do contador de resultados precisa existir vazio no HTML em vez de ser criado pelo JavaScript?

Nível B — Aplicação

B1. Implemente um formulário de login com validação de e-mail, senha de no mínimo 8 caracteres, botão "mostrar senha" que alterna o type do campo, bloqueio após três tentativas com contagem regressiva de 30 segundos e mensagens acessíveis por campo.

Resultado esperado: as três tentativas erradas desabilitam o botão de envio e mostram "Tente novamente em 30, 29, 28… segundos"; o botão "mostrar senha" tem aria-pressed sincronizado; nenhum erro é sinalizado só por cor.

Dica

Guarde o número de tentativas em uma variável de estado e use setInterval para a contagem, limpando com clearInterval ao chegar a zero. Para alternar o tipo do campo: campo.type = campo.type === "password" ? "text" : "password".

B2. Crie um medidor de força de senha com cinco critérios (comprimento ≥ 8, maiúscula, minúscula, número e caractere especial), exibindo a lista de critérios que ficam verdes conforme atendidos e uma barra de progresso colorida.

Resultado esperado: cada critério é um <li> com um ícone que muda de estado; a barra usa <meter> ou uma div com largura proporcional; o texto do nível ("Fraca", "Razoável", "Forte") é anunciado por uma região aria-live.

Dica

Uma regex por critério: /[A-Z]/, /[a-z]/, /\d/, /[^A-Za-z0-9]/. Some um ponto por critério atendido e penalize repetições com /(.)\1{2,}/. Nunca guarde a senha em localStorage.

B3. Implemente a validação de data de nascimento como um componente independente: o campo calcula a idade exata em tempo real e a exibe ao lado ("Você tem 19 anos"), rejeita datas futuras, datas que não existem no calendário e idade abaixo de 16 anos.

Resultado esperado: digitar 29/02/2001 mostra "Essa data não existe no calendário" (2001 não é bissexto); digitar 29/02/2000 é aceito; a idade aparece assim que a data fica completa e válida.

Dica

Reaproveite calcularIdade da Mão na massa e teste a existência da data reconstruindo dia, mês e ano do objeto Date, como explica a seção 4.1. Para o ano bissexto você não precisa de regra própria: o Date já sabe.

B4. Construa um formulário de agendamento de sala com: data (não pode ser passada nem fim de semana), horário em um <select> gerado dinamicamente de 30 em 30 minutos entre 8h e 18h, duração em horas e observações, exibindo um resumo textual do agendamento antes da confirmação.

Resultado esperado: as opções do <select> são criadas por um laço, não escritas à mão; escolher um sábado mostra "A sala não é liberada aos fins de semana"; o resumo é atualizado a cada alteração e lido por uma região aria-live.

Dica

data.getDay() devolve 0 para domingo e 6 para sábado. Para gerar os horários, um for de 8 a 17 com dois push por hora (:00 e :30) resolve. Use String(hora).padStart(2, "0") para formatar.

B5. Pegue o formulário de contato de contato.html (Unidade 1), que hoje só tem validação nativa, e adicione validação completa em JavaScript sem alterar o HTML além de acrescentar os <span> de erro. Documente cada regra implementada em um comentário no topo do arquivo.

Resultado esperado: um arquivo js/contato.js com validadores puros e reaproveitáveis; o HTML mantém required e type, e ganha apenas os <span class="erro" role="alert">; o console fica limpo.

Dica

Comece copiando a estrutura de js/inscricao.js e apagando o que não se aplica. As funções mostrarErro, validarEmail e validarNomeCompleto saem de lá sem alteração — esse é o ponto de escrever validadores puros.

B6. Escreva e teste expressões regulares para validar: matrícula acadêmica (quatro dígitos de ano seguidos de cinco dígitos), placa Mercosul, código de rastreio dos Correios (duas letras, nove dígitos e BR), IPv4 e cartão com 16 dígitos com ou sem espaços e hífens.

Resultado esperado: uma página exercicios/aula14/regex.html com um campo por padrão, validação em tempo real e, ao lado de cada campo, três exemplos válidos e três inválidos que você testou.

Dica

Ancore tudo com ^ e $. Para o IPv4, cada octeto vai de 0 a 255: (25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d) repetido quatro vezes, separado por \.. Teste cada expressão no https://regex101.com antes de colar no código.

B7. Escreva gerarSlug(titulo) que converta "Minicurso: Acessibilidade na Prática — Nível 1!" em "minicurso-acessibilidade-na-pratica-nivel-1", tratando acentos, símbolos, espaços múltiplos e hífens nas pontas.

Resultado esperado: uma função pura com cinco casos de teste no console, incluindo uma string só com símbolos (que deve devolver string vazia) e uma com espaços nas pontas.

Dica

O caminho é: normalizar (seção 9.2) para tirar acentos, replace(/[^a-z0-9]+/g, "-") para trocar tudo que não é alfanumérico por hífen e replace(/^-+|-+$/g, "") para limpar as pontas.

B8. Implemente um destacador de busca: dado o termo digitado e o texto de cada cartão da programação, envolva todas as ocorrências em <mark>, ignorando maiúsculas e acentos, sem quebrar quando o termo contiver caracteres especiais de regex.

Resultado esperado: buscar web destaca "web" em todos os títulos; buscar ( não gera erro no console; o destaque some ao limpar a busca.

Dica

Escape o termo antes de montar a regex: termo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"). Como o destaque exige HTML, este é o único lugar da aula em que innerHTML é aceitável — e só porque o texto vem dos seus dados, não do usuário. Sanitize antes.

B9. Acrescente paginação à programação: quatro itens por página, botões "anterior" e "próxima", indicação "Página 2 de 3" e volta automática à página 1 sempre que a busca ou o filtro mudarem.

Resultado esperado: a paginação respeita o resultado filtrado (não o array completo); os botões ficam desabilitados nos extremos; o foco vai para o topo da lista ao trocar de página.

Dica

Guarde paginaAtual no estado e use lista.slice(inicio, inicio + POR_PAGINA). O total de páginas é Math.ceil(lista.length / POR_PAGINA) — e nunca deixe paginaAtual maior que ele depois de um filtro.

Nível C — Desafio

C1. Biblioteca de validação configurável. Escreva um js/validador.js genérico, dirigido por um objeto de regras, que funcione em qualquer formulário do seu projeto:

JavaScript
const validador = criarValidador(form, {
  nome: { obrigatorio: true, min: 5, max: 80, tipo: "nomeCompleto" },
  email: { obrigatorio: true, tipo: "email" },
  cpf: { obrigatorio: true, tipo: "cpf" },
  nascimento: { obrigatorio: true, tipo: "data", idadeMinima: 16 },
});

Deve suportar regras predefinidas por tipo, regras personalizadas por função, mensagens customizadas, validação em blur/input/submit, exibição automática das mensagens e um método validador.estaValido(). Demonstre a mesma biblioteca funcionando em três formulários diferentes do seu projeto.

Dica

criarValidador devolve um objeto com métodos — é o padrão de módulo que você reencontrará no Nível 2. Guarde as regras em uma constante e percorra Object.entries(regras) para registrar os ouvintes. Cada tipo é uma entrada em um objeto VALIDADORES_POR_TIPO, o que evita uma cadeia gigante de if.

C2. Painel de consulta completo. Sobre a listagem principal do seu projeto autoral, implemente ao mesmo tempo: busca com debounce e normalização de acentos, dois filtros combináveis (categoria e faixa de valor ou data), ordenação por três critérios, paginação, contagem de resultados anunciada, estado vazio com sugestão útil e persistência da consulta no localStorage (ao recarregar, os filtros voltam como estavam).

Dica

Mantenha um objeto de estado (const consulta = { termo: "", categoria: "todas", ordem: "nome", pagina: 1 }) e uma função aplicarConsulta() que lê esse objeto, filtra, ordena, pagina e renderiza. Salvar e restaurar vira JSON.stringify(consulta) e Object.assign(consulta, JSON.parse(bruto)).

🏆 Desafios

⭐ O formulário que aceita qualquer coisa

formulariosjavascriptbugacessibilidade

O arquivo abaixo deveria validar uma inscrição, mas aceita nome vazio, e-mail sem arroba e CPF com uma letra no meio — e, quando reclama, ninguém que use leitor de tela fica sabendo. São seis defeitos: quatro de lógica e dois de acessibilidade. Encontre todos usando só o DevTools e o console, sem reescrever o arquivo do zero.

caca-ao-bug.html

HTML
<form id="form" novalidate>
  <label for="nome">Nome</label>
  <input type="text" id="nome" name="nome">
  <span id="erro-nome"></span>

  <label for="email">E-mail</label>
  <input type="text" id="email" name="email">
  <span id="erro-email"></span>

  <button type="button" id="enviar">Enviar</button>
</form>

caca-ao-bug.js

JavaScript
const form = document.querySelector("#form");

function validarNome(valor) {
  if (valor === "") return "Nome obrigatório";
  return "";
}

function validarEmail(valor) {
  if (/\w+@\w+/.test(valor)) return "";
  return "Inválido";
}

document.querySelector("#enviar").addEventListener("click", () => {
  const nome = document.querySelector("#nome").value;
  const email = document.querySelector("#email").value;

  document.querySelector("#erro-nome").style.color = "red";
  document.querySelector("#erro-nome").innerHTML = validarNome(nome);
  document.querySelector("#erro-email").innerHTML = validarEmail(email);

  if (validarNome(nome) == "" && validarEmail(email) == "") {
    form.submit();
  }
});

Critérios de pronto

  • Um espaço em branco (" ") no campo nome passa a ser rejeitado.
  • "ana@teste" e "ana @teste.br" passam a ser rejeitados, e "ana@teste.br" continua sendo aceito.
  • O envio passa pelo evento submit do formulário e funciona com Enter em qualquer campo.
  • Cada mensagem é anunciada por leitor de tela e o campo com erro fica marcado com aria-invalid.
  • As mensagens dizem o que fazer, não apenas "Inválido".
  • Um comentário de uma linha acima de cada correção explica o defeito que ela resolve.
Pistas
  1. Compare a regex de e-mail com a da seção 5.10: o que falta nas pontas dela? E o que \w deixa passar?
  2. valor === "" e valor.trim() === "" não são a mesma pergunta.
  3. Releia a Aula 13 sobre por que ouvir click no botão não é o mesmo que ouvir submit no formulário.
  4. innerHTML para texto puro é desnecessário e arriscado; e cor não é mensagem. Volte à seção 7 e liste os cinco requisitos.
⭐⭐

⭐⭐ Busca que ignora acentos, plurais e a ordem das palavras

javascriptinvestigacaoperformancedom

Digite "minicurso git" na busca da programação: nada aparece, porque nenhum título contém exatamente essa sequência. Digite "seguranças": também nada. Usuários não digitam do jeito que os seus dados foram escritos — e a sua busca não pode depender disso. Torne a busca tolerante: ela deve encontrar o item quando todas as palavras digitadas aparecerem no texto, em qualquer ordem, ignorando acentos, maiúsculas e o "s" final.

Critérios de pronto

  • "minicurso git" encontra "Minicurso: Git e GitHub do zero ao primeiro pull request".
  • "git minicurso" encontra o mesmo item (a ordem das palavras não importa).
  • "seguranças" encontra "segurança em aplicações web".
  • A tolerância está isolada em uma função pura, testada no console com pelo menos seis casos.
  • A busca continua respondendo instantaneamente com 500 itens no array (meça com console.time antes e depois).
Pistas
  1. Divida o termo digitado em palavras com split(/\s+/) e exija que todas passem: palavras.every(...).
  2. String.prototype.normalize("NFD") mais a faixa ̀-ͯ resolvem os acentos; a seção 9.2 tem a receita.
  3. Para o plural ingênuo, remova um s final de cada palavra antes de comparar — e documente em um comentário que essa é uma simplificação, não uma regra do português.
  4. Normalizar o texto de cada item a cada tecla é desperdício. Faça isso uma vez, guardando o texto já normalizado em um campo do próprio objeto ao iniciar.
⭐⭐

⭐⭐ Validação sem uma linha de if

javascriptrefatoracaoformularios

O validarCampo da Mão na massa tem um switch com sete casos. Funciona, mas cada campo novo exige mexer nessa função — e, num formulário de vinte campos, ela vira um monstro. Existe uma alternativa que os frameworks usam: descrever as regras como dados e deixar uma função genérica aplicá-las. Refatore a validação da inscrição para esse modelo, sem perder nenhuma mensagem nem nenhum comportamento.

Critérios de pronto

  • Existe um objeto REGRAS mapeando o id de cada campo para uma lista de validadores.
  • validarCampo(campo) tem no máximo dez linhas e não cita nenhum campo pelo nome.
  • Acrescentar um campo novo ao formulário exige alterar apenas o HTML e o objeto REGRAS.
  • Todos os comportamentos do Como testar continuam funcionando, item por item.
  • Um comentário no topo explica, em três linhas, a vantagem dessa organização.
Pistas
  1. Um validador é uma função (valor) => mensagem. Uma lista deles pode ser percorrida com find para achar a primeira mensagem não vazia.
  2. Regras que precisam de parâmetro (tamanho mínimo, idade mínima) viram funções que devolvem funções: minimo(5) devolve (valor) => ....
  3. O campo termos trabalha com checked, não com value. Padronize a entrada do validador (por exemplo, sempre o elemento) para não precisar de exceções.
  4. Compare o resultado com o criarValidador do exercício C1: você acabou de construir metade dele.
⭐⭐⭐

⭐⭐⭐ O formulário mais acessível que você já fez

acessibilidadeformulariosinvestigacaoprojeto

Um formulário pode passar em todos os testes automáticos e ainda ser impossível de preencher sem o mouse. Nesta missão você vai auditar o formulário do seu projeto autoral como um usuário que não enxerga a tela, corrigir o que encontrar e provar o resultado. Isso entra bem no Marco 3 da unidade.

Critérios de pronto

  • Um vídeo de até três minutos preenchendo e enviando o formulário só com o teclado, com o monitor desligado ou os olhos fechados, usando o leitor de tela do sistema (NVDA no Windows, Orca no Linux, VoiceOver no macOS).
  • O formulário tem um resumo de erros no topo, ligado por link a cada campo com problema, que recebe foco ao falhar o envio.
  • Todo campo tem rótulo associado, texto de ajuda ligado por aria-describedby e mensagem por role="alert".
  • A aba Lighthouse do DevTools marca 100 em Acessibilidade na página do formulário, e a captura está no relatório.
  • Um arquivo acessibilidade.md lista os problemas encontrados, a correção aplicada e — o item mais importante — dois problemas que o Lighthouse não apontou e que só a navegação por teclado revelou.
Pistas
  1. Comece navegando só com Tab, Shift+Tab, Espaço e Enter. Anote todo momento em que você não soube onde estava.
  2. O resumo de erros é uma <div tabindex="-1"> com uma lista de links href="#a14-id-do-campo"; ao falhar o envio, chame .focus() nela.
  3. Ferramentas automáticas não detectam rótulo errado, ordem de foco ilógica nem mensagem que não diz o que fazer. É aí que estão os seus dois achados.
  4. O leitor de tela do seu sistema já está instalado. No Windows, o Narrador abre com Ctrl+Win+Enter; no Linux, o Orca com Super+Alt+S.

🐛 Erros comuns

Sintoma Causa Solução
A página recarrega ao enviar e "os dados somem" Falta e.preventDefault() no ouvinte de submit Chame e.preventDefault() na primeira linha do ouvinte
Uncaught TypeError: Cannot read properties of null (reading 'value') querySelector não achou o campo: id errado, ou o script roda antes do HTML Confira o seletor com $$("#id") no console e use defer na tag <script>
O botão "Enviar" não faz nada, e Enter no campo envia sem validar O ouvinte está no click do botão, não no submit do formulário Ouça submit no <form>; o botão deve ser type="submit"
idade + 1 resulta em "251" input.value é sempre string, mesmo em type="number" Converta com Number(campo.value) antes de qualquer conta
" " passa na validação de campo obrigatório Comparou valor === "" sem trim() Use valor.trim() === ""
O formulário nunca envia, mesmo com tudo preenchido setCustomValidity("mensagem") nunca foi limpo Chame campo.setCustomValidity("") no início de cada validação
pattern="/\d{5}/" nunca casa As barras da forma literal viraram caracteres a serem procurados No atributo pattern não use barras; as âncoras ^ e $ já são implícitas
/\d{3}/.test("abc1234") devolve true e aceita lixo Regex sem âncoras verifica se o padrão existe em algum lugar Ancore o padrão de validação: /^\d{3}$/
Uncaught SyntaxError: Identifier 'els' has already been declared Dois scripts da mesma página declaram a mesma const no escopo global Nomes diferentes por arquivo, ou um único script por página (módulos só no Nível 2)
Uncaught ReferenceError: palestras is not defined js/programacao.js carregou antes de js/dados.js Coloque dados.js antes na ordem das tags <script defer>
A lista some ao clicar em um filtro e volta errada depois sort() reordenou o array original de dados Ordene sempre uma cópia: [...palestras].sort(...)
Buscar "cafe" não encontra "Café" Comparação direta entre strings com e sem acento Normalize os dois lados com normalize("NFD") e a remoção dos combinantes
"Ávila" aparece depois de "Zampieri" na ordenação sort() puro compara códigos de caractere Use a.nome.localeCompare(b.nome, "pt-BR")
A página trava ao digitar na busca de uma lista grande Filtro pesado executado a cada tecla Envolva o ouvinte em debounce(fn, 300)
O layout "pula" toda vez que uma mensagem de erro aparece O elemento da mensagem tem altura zero quando vazio min-height no .erro, reservando o espaço da linha
O leitor de tela não anuncia o resultado da busca A região aria-live foi criada junto com o conteúdo Deixe o elemento vazio no HTML e só altere o textContent depois
localStorage.getItem devolve [object Object] O objeto foi salvo sem JSON.stringify setItem(chave, JSON.stringify(obj)) e JSON.parse na leitura

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (15 min). FLANAGAN, JavaScript: o guia definitivo, capítulo de expressões regulares. MILETTO & BERTAGNOLLI, Desenvolvimento de software II, seção de validação de dados de entrada. Na MDN em português, o artigo "Validação de formulário no lado do cliente" (link em Para aprofundar). Anote duas restrições de validação nativa que não apareceram nesta aula.

Parte 2 — Entrega (40 min). No seu projeto autoral:

  1. O formulário principal com validação completa em JavaScript: pelo menos seis campos, mensagens específicas por campo, ao menos uma validação por expressão regular e uma regra de negócio que a regex não resolve (dígito verificador, idade mínima, data no calendário ou confirmação de senha).
  2. Mensagens acessíveis: role="alert", aria-invalid, aria-describedby, indicação que não dependa só de cor e foco no primeiro campo inválido ao enviar.
  3. A listagem principal do seu domínio com busca (com debounce e normalização de acentos), um filtro e uma ordenação, com contador anunciado e estado vazio tratado.
  4. Os exercícios B2 (medidor de força de senha) e B7 (gerarSlug) em exercicios/aula14/.

Critério de pronto: enviar o formulário vazio destaca todos os campos com erro, leva o foco ao primeiro e não recarrega a página; digitar um termo sem acento encontra o item acentuado; o console fica limpo em todas as páginas.

Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: traga uma expressão regular sua que falhou em um caso que você não tinha previsto, explique por que falhou e mostre a correção.

Guarde no seu repositório: commit + push (ou a pasta do projeto).

✅ Checkpoint do projeto

  • [ ] Formulário principal com novalidate, um <span class="erro" role="alert"> por campo e id no padrão erro-<id-do-campo>.
  • [ ] Validadores escritos como funções puras (recebem valor, devolvem mensagem ou ""), separados da exibição.
  • [ ] Ao menos uma validação por expressão regular ancorada com ^ e $, e uma regra algorítmica que a regex não resolve.
  • [ ] Validação no blur, revalidação no input depois do primeiro erro e validação completa no submit.
  • [ ] Foco levado ao primeiro campo inválido, com rolagem até ele.
  • [ ] aria-invalid, aria-describedby e indicação visual que não dependa só de cor em todos os campos.
  • [ ] Máscara aplicada na digitação e removida antes de usar o valor.
  • [ ] Rascunho no localStorage sem nenhum dado sensível, com JSON.stringify/JSON.parse.
  • [ ] Listagem com busca (debounce + normalização de acentos), filtro e ordenação sobre uma cópia do array.
  • [ ] Contador de resultados em região aria-live e mensagem de estado vazio com sugestão útil.
  • [ ] Nenhum erro no console em nenhuma das cinco páginas.

📚 Para aprofundar

Na próxima aula, o site sai da sua máquina e vai para a internet: você vai preparar o projeto para produção, versioná-lo com Git, publicá-lo no GitHub Pages, auditar o resultado com o Lighthouse e fechar o Marco 3 — o site do evento, no ar, com um endereço que qualquer pessoa pode acessar.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. Capturando os dados do formulário1.1 O ponto de partida: submit e preventDefault1.2 Três formas de ler os valores1.3 Cada tipo de campo devolve uma coisa diferente2. Quando validar3. A Constraint Validation API3.1 O objeto validity3.2 Os três métodos3.3 Traduzindo validity em mensagem útil4. Funções validadoras puras4.1 Datas digitadas: duas armadilhas do Date4.2 CPF: o algoritmo de verdade5. Expressões regulares5.1 O que é e como criar5.2 Flags5.3 Classes de caracteres5.4 Quantificadores5.5 Âncoras e limites5.6 Grupos, alternância e lookahead5.7 Escapando metacaracteres5.8 Os métodos que você vai usar5.9 Padrões brasileiros de referência5.10 O caso do e-mail5.11 O que a regex NÃO faz6. Máscaras e formatação7. Mostrando o erro de forma acessível7.1 Validação no envio e o foco no primeiro erro8. Guardando a inscrição com localStorage9. Consultas dinâmicas: busca, filtro e ordenação9.1 O pipeline9.2 Buscar ignorando acentos e caixa9.3 debounce: não filtrar a cada tecla9.4 Estado vazio e contador💻 Mão na massa — Inscrição validada e programação com buscaPasso 1 — o formulário de inscricao.htmlPasso 2 — o CSS dos camposPasso 3 — js/inscricao.js ampliadoPasso 4 — debounce disponível em todas as páginasPasso 5 — conferir (e não tocar) o js/dados.jsPasso 6 — a interface de consulta em programacao.htmlPasso 7 — js/programacao.jsPasso 8 — o CSS da consultaComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ O formulário que aceita qualquer coisa⭐⭐ Busca que ignora acentos, plurais e a ordem das palavras⭐⭐ Validação sem uma linha de if⭐⭐⭐ O formulário mais acessível que você já fez🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto📚 Para aprofundar
Nível 1Unidade 3 · JavaScript e interatividade3 aulas de 50 min + 1 h EADFecha a unidade · Marco 3

Aula 15 — Publicando seu website na internet

Nível 1 — Introdução ao Desenvolvimento Web · WebLab

🎯 Objetivos de aprendizagem

Ao final desta aula você será capaz de:

  • Explicar o que são hospedagem, domínio e certificado HTTPS, e como os três se combinam para que um endereço digitado por um estranho chegue ao seu index.html.
  • Identificar as diferenças entre o ambiente local e um servidor real — sensibilidade a maiúsculas, caminhos e subdiretório na URL — e corrigir o projeto antes de publicar.
  • Preparar um projeto front-end para produção: limpeza de código, otimização de imagens, metadados de SEO e Open Graph, favicon e página 404.
  • Versionar o projeto com os comandos essenciais do Git e publicá-lo em um repositório público no GitHub, com .gitignore e README.md adequados.
  • Publicar o site no GitHub Pages e reconhecer quando a Netlify ou a Vercel são alternativas melhores.
  • Auditar o site publicado com o validador do W3C e o Lighthouse, interpretar as quatro pontuações e aplicar as correções de maior impacto.
  • Fechar o Marco 3 com o projeto autoral no ar, acessível por uma URL pública que qualquer pessoa consegue abrir.

📋 Pré-requisitos

  • [ ] O site do evento acadêmico completo: cinco páginas responsivas, animações, menu hambúrguer acessível, listagens renderizadas por JavaScript, formulário validado e programação com busca, filtro e ordenação (Aulas 01 a 14).
  • [ ] Console limpo nas cinco páginas, conforme o Checkpoint da Aula 14.
  • [ ] Git instalado e configurado com o seu nome e e-mail (git --version responde no terminal). Se ainda não estiver, o Capítulo 01 — Caixa de ferramentas do dev web tem o passo a passo por sistema operacional.
  • [ ] Uma conta gratuita no GitHub, com o e-mail confirmado.
  • [ ] Navegador Chrome ou Edge atualizado (a aba Lighthouse do DevTools existe neles; no Firefox, use o PageSpeed Insights).
  • [ ] Uma pasta de projeto chamada site-evento/, com index.html na raiz.

Na aula passada você fechou o ciclo do JavaScript no navegador: o formulário de inscrição ganhou validação campo a campo com mensagens acessíveis, e a programação ganhou busca, filtro e ordenação em tempo real. O site está pronto — e existe para exatamente uma pessoa, porque só roda no http://127.0.0.1:5500 da sua máquina. Hoje ele sai daí: você vai prepará-lo para produção, versioná-lo com Git, publicá-lo no GitHub Pages, auditar o resultado com o Lighthouse e terminar a aula com um endereço que qualquer pessoa do mundo consegue abrir.

🗺️ Roteiro

Bloco Tempo Atividade
1 50 min Hospedagem, domínio e HTTPS; o que muda entre "funciona aqui" e "está no ar"; preparação para produção
2 50 min Git essencial, repositório no GitHub, GitHub Pages passo a passo e alternativas
3 50 min Auditoria com validador do W3C e Lighthouse, correções, README e Marco 3 do projeto

1. O caminho de um site até o navegador de outra pessoa

Na Aula 01 você viu o ciclo requisição–resposta: o navegador pede, o servidor responde, o navegador desenha. Naquele momento o servidor era o Live Server, rodando na sua própria máquina. Publicar um site significa trocar esse servidor doméstico por um que fica ligado o tempo todo, com um endereço que existe para o mundo inteiro.

Três peças fazem isso acontecer. Elas são independentes — dá para ter uma sem a outra — e é justamente por isso que confundi-las causa tanta dor de cabeça.

1.1 Hospedagem: onde os arquivos ficam

Hospedagem é o computador (ou o conjunto de computadores) que guarda os seus arquivos e os entrega quando alguém pede. Para um site estático como o seu — só HTML, CSS, JavaScript e imagens, sem banco de dados nem código rodando no servidor —, hospedar é barato ao ponto de ser gratuito: o servidor não precisa pensar, só devolver arquivos.

Existem três modelos que você vai encontrar por aí:

Modelo Como funciona Quando usar
Hospedagem de sites estáticos Você envia os arquivos; a plataforma serve e distribui Sites como o seu, portfólios, documentação
Hospedagem compartilhada Um servidor com PHP e banco, dividido entre muitos clientes Sites em WordPress e similares
Servidor próprio (VPS) Você aluga uma máquina Linux e configura tudo Aplicações com back-end e requisitos próprios

O primeiro modelo é o desta aula, e o serviço que você vai usar — o GitHub Pages — pertence a ele. O terceiro é assunto do Capítulo 06 — Servidor próprio (VPS) com nginx da trilha Deploy, e vale a leitura quando você quiser entender o que as plataformas escondem de você.

🧠 Você sabia? Sites estáticos praticamente não custam nada para servir porque a resposta é sempre a mesma para todo mundo — o arquivo pode ser copiado para servidores espalhados pelo planeta (uma CDN) e entregue a partir do mais próximo de quem pediu. É por isso que o GitHub Pages, a Netlify e a Vercel oferecem o serviço de graça: o custo marginal de mais um site pequeno é ínfimo. Um site que gera cada página na hora, consultando um banco, custa ordens de grandeza mais — e essa diferença é o principal motivo do renascimento dos sites estáticos na última década.

1.2 Domínio: o nome que as pessoas digitam

Servidores são encontrados por endereço IP (185.199.108.153, por exemplo). Ninguém decora isso. O domínio é um apelido legível — wikipedia.org, github.io, weblab.aprendabit.com — e o DNS é a agenda telefônica mundial que traduz o apelido no número.

Quando alguém digita o endereço do seu site, acontece o seguinte, nesta ordem:

  1. O navegador pergunta ao DNS qual é o IP daquele domínio.
  2. O DNS responde (ou diz que não conhece, e você vê DNS_PROBE_FINISHED_NXDOMAIN).
  3. O navegador abre uma conexão com aquele IP e pede o caminho da URL.
  4. O servidor responde com o arquivo — ou com um status de erro, como o 404 que você já viu na aba Network.

Registrar um domínio .com.br custa por volta de quarenta reais por ano no Registro.br. Para esta trilha você não precisa comprar nada: o GitHub oferece um subdomínio gratuito no formato https://<seu-usuario>.github.io/<nome-do-repositorio>/, e ele é um endereço público de verdade — funciona no celular de qualquer pessoa, na casa da sua avó e em qualquer lugar com internet.

Se, depois desta trilha, você quiser um domínio seu, o Capítulo 04 — Domínios, DNS e HTTPS da trilha Deploy cobre registro, registros A e CNAME, propagação e a configuração no lado da plataforma.

1.3 HTTPS: o cadeado

O s de HTTPS significa que a conversa entre o navegador e o servidor é cifrada. Sem ele, qualquer pessoa no mesmo Wi-Fi consegue ler — e alterar — o que trafega. Com ele, o conteúdo é embaralhado por chaves negociadas no início da conexão, e um certificado emitido por uma autoridade confiável garante que o servidor do outro lado é mesmo quem diz ser.

Três consequências práticas para você hoje:

  • O HTTPS é gratuito. A autoridade certificadora Let's Encrypt emite certificados sem custo, e as plataformas de hospedagem cuidam da emissão e da renovação sozinhas. No GitHub Pages, basta marcar uma caixa.
  • Recursos misturados não carregam. Uma página servida por HTTPS que tenta buscar uma imagem por http:// recebe do navegador um bloqueio de conteúdo misto, com a mensagem Mixed Content: The page at 'https://…' was loaded over HTTPS, but requested an insecure element. Solução: nunca escreva http:// nas URLs do seu site.
  • Algumas APIs só existem em HTTPS. Geolocalização, câmera, microfone e notificações são bloqueadas em conexões inseguras. Isso não afeta o site do evento, mas afeta o seu próximo projeto.

⚠️ Atenção localhost é tratado pelo navegador como origem segura mesmo em http://. Ou seja: coisas que funcionam na sua máquina podem falhar assim que o site vai ao ar em outro domínio. Depois de publicar, refaça o teste no endereço público, não no Live Server.

1.4 Onde a trilha Deploy aprofunda

Esta aula é uma travessia rápida e completa: no fim dela o seu site está no ar. Ela não é, porém, o assunto inteiro. O WebLab tem uma trilha transversal só para isso, e dois capítulos dela conversam diretamente com o que você vai fazer hoje:

Leia os dois quando quiser ir além do "está no ar" e chegar ao "está no ar com processo".

2. O que muda entre "funciona aqui" e "está no ar"

Um site que funciona perfeitamente no Live Server pode chegar ao servidor completamente quebrado — sem estilo, sem imagens, com o JavaScript morto. Não é azar: são três diferenças concretas de ambiente.

Diferença Consequência no ar
O servidor distingue maiúsculas de minúsculas Estilo.cssestilo.css: a página aparece "crua", sem CSS
Caminhos absolutos da sua máquina C:/Users/ana/site/img/logo.png não existe em servidor nenhum
O site fica em um subdiretório da URL Em usuario.github.io/site-evento/, o caminho /img/logo.png aponta para a raiz errada

A primeira diferença é a mais cruel. O Windows e o macOS, por padrão, tratam Foto.JPG e foto.jpg como o mesmo arquivo. O Linux — que roda em praticamente todo servidor do mundo — não. O seu site funciona na sua máquina e morre no servidor, com um 404 para cada arquivo cujo nome você escreveu com uma letra diferente da real.

As três regras que evitam quase todos os problemas:

  1. Tudo em minúsculas, sem espaços e sem acentos, em arquivos e em pastas. foto-do-palestrante.webp, nunca Foto do Palestrante.WEBP.
  2. Sempre caminhos relativos: css/estilo.css, img/logo-sasi.svg, ../index.html. Nunca comece um caminho com / em um site que vai para subdiretório, e nunca use caminho de disco.
  3. O arquivo de entrada se chama index.html e fica na raiz do repositório. É o nome que todo servidor procura quando a URL termina em /.

🔎 Por baixo do capô Quando você pede https://usuario.github.io/site-evento/, o servidor recebe o caminho /site-evento/ e precisa decidir o que devolver. A convenção, herdada dos primeiros servidores web dos anos 1990, é procurar um arquivo de índice dentro do diretório: index.html. Se não achar, ele responde 404 ou, em servidores configurados para isso, devolve a listagem do diretório — aquela página cinza feia com os nomes dos arquivos. Renomear index.html para home.html é o jeito mais rápido de ver essa tela.

🔬 Investigue Abra o index.html do site do evento e troque a linha do CSS para <link rel="stylesheet" href="CSS/Estilo.css">. Salve e recarregue no Live Server: no Windows e no macOS, provavelmente nada muda — o site continua estilizado. Agora abra o DevTools na aba Network, marque a caixa Disable cache e recarregue: a linha do CSS aparece com status 200 mesmo com o nome errado. Esse é exatamente o cenário que quebra no servidor Linux, onde o mesmo pedido volta 404. Desfaça a alteração e adote a regra: nomes de arquivo sempre em minúsculas, escritos uma vez e copiados, nunca redigitados.

3. Preparando o projeto para produção

"Produção" é o nome que se dá ao ambiente onde os usuários de verdade usam o sistema. Preparar para produção é fazer, de uma vez, o que você foi adiando durante o semestre.

3.1 A estrutura final de pastas

Antes de qualquer coisa, o projeto precisa ter uma estrutura que outra pessoa entenda em dez segundos:

Texto
site-evento/
├── index.html
├── programacao.html
├── inscricao.html
├── palestrantes.html
├── contato.html
├── 404.html
├── css/
│   └── estilo.css
├── js/
│   ├── menu.js
│   ├── efeitos.js
│   ├── dados.js
│   ├── app.js
│   ├── relatorios.js
│   ├── palestrantes.js
│   ├── inscricao.js
│   └── programacao.js
├── img/
│   ├── favicon.svg
│   ├── logo-sasi.svg
│   ├── preview.jpg
│   ├── banner.jpg
│   └── palestrante-01.webp  (até palestrante-06.webp)
├── .gitignore
├── .nojekyll
└── README.md

Regras dessa estrutura:

  • HTML na raiz. Colocar as páginas dentro de uma pasta paginas/ quebra todos os caminhos relativos e não traz benefício algum em um site de cinco páginas.
  • Uma pasta por tipo de recurso: css/, js/, img/. Se as imagens forem muitas, subpastas por seção (img/palestrantes/) — no site do evento elas são poucas e ficam direto em img/, com os nomes numerados que o js/dados.js referencia desde a Aula 12.
  • Nada de arquivos órfãos. teste.html, estilo-antigo.css, Nova pasta/, index - Cópia.html: apague. Se der medo, é para isso que serve o Git — depois do primeiro commit, nada se perde de verdade.

3.2 A limpeza obrigatória

Percorra esta lista antes de publicar. Ela leva vinte minutos e evita metade dos problemas do Marco 3:

  • Remova os console.log de depuração. Console limpo é critério de qualidade, e um console cheio de mensagens suas denuncia código não revisado. O truque profissional está na próxima subseção.
  • Remova blocos de código comentado. Aquele CSS antigo que você "deixou comentado por via das dúvidas" não serve para nada: a versão anterior está no histórico do Git.
  • Remova arquivos não referenciados. Se nenhum HTML aponta para ele, ele só está ocupando espaço e confundindo quem lê.
  • Confira todos os links. Nenhum href="#" esquecido, nenhum link para uma página que você renomeou.
  • Confira todas as imagens. Cada <img> tem alt adequado, e o arquivo existe com exatamente aquele nome.
  • Confira os textos. Nada de texto de preenchimento sobrando: o conteúdo tem que ser real, ainda que curto.

3.3 Depuração que não suja o console

Apagar todos os console.log na véspera da entrega é péssimo: você perde as ferramentas justo quando mais precisa delas. A solução é uma chave única e uma função que a consulta.

Ela precisa ficar no topo do js/dados.js, não do app.js. Desde a Aula 12, dados.js é o primeiro script do projeto que declara coisas usadas pelos outros — e ele mesmo tem um console.log no fim, que você vai querer trocar por depurar. Se a função morasse no app.js, que é carregado depois, essa troca daria Uncaught ReferenceError: depurar is not defined na primeira linha do site.

site-evento/js/dados.js (acrescente no início do arquivo, antes dos arrays)

JavaScript
// ---------- DEPURAÇÃO ----------
// Chave de depuração: deixe true enquanto desenvolve, false antes de publicar.
const DEPURAR = false;

/**
 * Escreve no console apenas quando a depuração está ligada.
 * Use no lugar de console.log em qualquer mensagem de desenvolvimento.
 * @param {...*} mensagens - o que você quer inspecionar
 */
function depurar(...mensagens) {
  if (DEPURAR) {
    console.log("[site-evento]", ...mensagens);
  }
}

A partir daí, troque todo console.log de desenvolvimento por depurar — a começar pela última linha do próprio dados.js, que passa a ser depurar(\dados.js carregado: ${palestras.length} atividades, ${palestrantes.length} palestrantes.`)`. Ligar e desligar a depuração inteira vira uma edição de uma linha.

Duas ressalvas importantes:

  • console.error fica. Erros de verdade — uma imagem que não carregou, um dado inválido — devem aparecer sempre, inclusive em produção.
  • depurar precisa estar declarado antes de ser usado pelos outros scripts. Com ele no topo do js/dados.js, isso vale para app.js, relatorios.js, palestrantes.js, inscricao.js e programacao.js — todos carregados depois, e defer garante a ordem das tags. O único que vem antes é o js/menu.js, que não escreve nada no console; se um dia precisar, mova a função para um js/util.js carregado em primeiro lugar.

3.4 Imagens: o maior peso do seu site

Em quase todo projeto de estudante, as imagens respondem por 80% dos bytes da página. Uma foto de celular tem cerca de 4000 pixels de largura e pesa 4 MB; exibida em um cartão de 400 pixels, ela entrega cem vezes mais dados do que o necessário — e quem paga a conta é quem abre o seu site em dados móveis.

Ação Ganho típico
Redimensionar para o tamanho real de exibição 50% a 90%
Converter de JPEG para WebP 25% a 35% a mais
Usar SVG para logos e ícones Enorme, e escala sem perder qualidade
loading="lazy" no que está abaixo da dobra Carregamento inicial mais rápido
Declarar width e height Elimina o "pulo" do layout ao carregar

O caminho prático, sem instalar nada: abra squoosh.app, arraste a imagem, escolha WebP com qualidade em torno de 75, ajuste a largura para o dobro do tamanho de exibição (para telas de alta densidade) e baixe. Uma foto de 4 MB costuma sair com 80 KB e nenhuma diferença visível.

A marcação final de uma imagem otimizada:

HTML
<img
  src="img/palestrante-03.webp"
  alt="Carla Mendes, palestrante da área de segurança, sorrindo em frente a um quadro branco"
  width="400"
  height="400"
  loading="lazy"
>

Os atributos width e height não fixam o tamanho na tela — o CSS continua mandando. Eles informam ao navegador a proporção da imagem, para que ele reserve o espaço correto antes do arquivo chegar. Sem eles, o texto abaixo da imagem "pula" quando ela carrega, e o Lighthouse penaliza isso na métrica de estabilidade visual.

⚠️ Atenção Não coloque loading="lazy" na imagem principal do topo da página (o herói, o logo do cabeçalho). Ela é justamente a que precisa aparecer primeiro; adiar o seu carregamento piora a experiência e a pontuação. lazy é para o que está abaixo da dobra — fora da primeira tela.

3.5 Metadados: o que o Google e o WhatsApp leem

O <head> de cada página carrega informações que não aparecem na tela, mas definem como o seu site é visto pelo mundo: o título na aba, o resumo no resultado de busca, o cartão que aparece quando alguém cola o link em um grupo de WhatsApp.

site-evento/index.html (o <head> completo)

HTML
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Semana Acadêmica de Sistemas de Informação</title>
  <meta name="description" content="Três dias de palestras, minicursos e maratona de programação no campus de Sinop. Programação completa, inscrição gratuita e certificado.">
  <meta name="author" content="Semana Acadêmica de Sistemas de Informação">
  <meta name="theme-color" content="#0b3d5c">

  <link rel="icon" href="img/favicon.svg" type="image/svg+xml">
  <link rel="canonical" href="https://usuario.github.io/site-evento/">

  <meta property="og:type" content="website">
  <meta property="og:title" content="Semana Acadêmica de Sistemas de Informação">
  <meta property="og:description" content="Palestras, minicursos e maratona de programação. Inscrição gratuita.">
  <meta property="og:image" content="https://usuario.github.io/site-evento/img/preview.jpg">
  <meta property="og:url" content="https://usuario.github.io/site-evento/">
  <meta property="og:locale" content="pt_BR">

  <link rel="stylesheet" href="css/estilo.css">
  <script src="js/menu.js" defer></script>
  <script src="js/dados.js" defer></script>
  <script src="js/app.js" defer></script>
  <script src="js/efeitos.js" defer></script>
</head>

A ordem dos <script> é a mesma fixada nas Aulas 13 e 14 — menu.js, dados.js, app.js e, por último, o script específico da página (aqui o efeitos.js da Aula 09; em programacao.html seriam relatorios.js e programacao.js, e em inscricao.html o inscricao.js). É por isso que o depurar() da seção 3.3 mora no topo do dados.js: assim ele já existe quando o app.js e os scripts de página rodam.

O que cada bloco faz:

  • <title> — o texto da aba e o link azul do resultado de busca. Escreva no padrão Assunto da página — Nome do site, com no máximo 60 caracteres. Cada página tem o seu, diferente das outras.
  • <meta name="description"> — o parágrafo cinza abaixo do link no Google. Até 160 caracteres, escritos para uma pessoa, não para um robô. Cada página tem a sua.
  • theme-color — colore a barra do navegador no Android. Detalhe pequeno, efeito grande.
  • <link rel="icon"> — o favicon, o ícone da aba. Um SVG resolve todos os tamanhos.
  • canonical — declara qual é o endereço oficial da página, evitando que ela seja tratada como conteúdo duplicado quando acessível por mais de uma URL.
  • og:* — o protocolo Open Graph, criado pelo Facebook e adotado por praticamente todo mundo (WhatsApp, Telegram, LinkedIn, Discord, Slack). É o que gera o cartão com imagem, título e descrição quando alguém compartilha o link. A og:image precisa ser uma URL absoluta — endereço completo, com https:// — porque quem lê essa marcação é um servidor de outra empresa, que não tem como resolver caminho relativo.

A imagem de prévia deve ter 1200 × 630 pixels. Menos que isso e as plataformas mostram um cartão pequeno, sem destaque.

🧠 Você sabia? Quando você cola um link no WhatsApp, não é o seu celular que gera a prévia: um servidor da plataforma abre a sua página, lê apenas o <head> e monta o cartão. Isso tem duas consequências curiosas. A primeira: se o seu site estiver fora do ar naquele instante, a prévia nunca mais é gerada para aquele link, porque o resultado fica em cache por muito tempo. A segunda: conteúdo inserido por JavaScript não entra na prévia — esses leitores não executam scripts. É por isso que as metatags precisam estar escritas no HTML, e não geradas pelo seu app.js.

3.6 A página 404

Cedo ou tarde alguém vai digitar errado o endereço de uma das suas páginas, ou clicar num link antigo. A resposta padrão do GitHub Pages é uma página em inglês, com o mascote do GitHub — nada a ver com o seu site.

Uma página 404.html na raiz do repositório resolve: o Pages a serve automaticamente sempre que um caminho não existir.

site-evento/404.html

HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Página não encontrada — Semana Acadêmica de SI</title>
  <meta name="description" content="A página que você procurou não existe neste site.">
  <meta name="robots" content="noindex">
  <link rel="icon" href="/site-evento/img/favicon.svg" type="image/svg+xml">
  <link rel="stylesheet" href="/site-evento/css/estilo.css">
</head>
<body>
  <a class="pular-para-conteudo" href="#conteudo">Pular para o conteúdo</a>

  <main id="conteudo" class="erro-404">
    <p class="erro-404__codigo">404</p>
    <h1>Esta página não existe</h1>
    <p>
      O endereço que você abriu não corresponde a nenhuma página do site da
      Semana Acadêmica. Pode ser um link antigo, um erro de digitação — ou uma
      página que ainda vamos criar.
    </p>
    <p>Talvez você esteja procurando por:</p>
    <ul class="erro-404__links">
      <li><a href="/site-evento/">Página inicial</a></li>
      <li><a href="/site-evento/programacao.html">Programação completa</a></li>
      <li><a href="/site-evento/inscricao.html">Inscrição</a></li>
      <li><a href="/site-evento/palestrantes.html">Palestrantes</a></li>
      <li><a href="/site-evento/contato.html">Contato</a></li>
    </ul>
  </main>
</body>
</html>

Repare que esta é a única página do projeto com caminhos começando por /site-evento/. O motivo: a 404.html é servida em resposta a qualquer caminho inexistente, inclusive /site-evento/uma/pasta/funda/pagina.html. Um caminho relativo seria resolvido a partir dessa pasta imaginária e apontaria para o nada. Como o prefixo é o nome do repositório, você o ajusta se um dia renomeá-lo.

O CSS correspondente, no fim de css/estilo.css:

site-evento/css/estilo.css

CSS
/* ---------- Página 404 ---------- */
.erro-404 {
  max-width: 60ch;
  margin-inline: auto;
  padding: var(--espaco-xl) var(--espaco-md);
  text-align: center;
}

.erro-404__codigo {
  font-size: clamp(4rem, 18vw, 9rem);
  font-weight: 800;
  line-height: 1;
  margin: 0;
  color: var(--cor-primaria);
  opacity: 0.25;
}

.erro-404__links {
  list-style: none;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--espaco-sm);
  justify-content: center;
}

.erro-404__links a {
  display: inline-block;
  padding: 0.5rem 1rem;
  border: 2px solid var(--cor-primaria);
  border-radius: var(--raio-md);
  text-decoration: none;
}

.erro-404__links a:hover,
.erro-404__links a:focus-visible {
  background-color: var(--cor-primaria);
  color: var(--cor-clara);
}

O <meta name="robots" content="noindex"> pede aos buscadores que não indexem a página de erro — ela não deveria aparecer em resultado de busca nenhum.

4. Auditoria: provar que está bom

Publicar sem auditar é entregar um trabalho sem reler. Três ferramentas dão, em minutos, um diagnóstico honesto do seu site.

4.1 O validador do W3C

O validator.w3.org lê o seu HTML e aponta tudo o que está fora da especificação: tag não fechada, atributo inventado, id repetido, hierarquia de títulos quebrada, texto solto onde só cabe elemento.

Três modos de uso, todos gratuitos:

Modo Quando usar
Address O site já está publicado: cole a URL e valide a página no ar
File Upload Ainda local: envie o arquivo .html
Direct Input Um trecho suspeito: cole o HTML direto na caixa

Valide todas as páginas, não só a inicial. A meta é zero erros. Avisos (warnings) merecem leitura, mas nem todos exigem ação — o aviso sobre <section> sem cabeçalho, por exemplo, costuma ser legítimo em alguns layouts.

📌 Vale gravar Saiba dizer a diferença entre erro e aviso no validador, e por que HTML inválido pode funcionar no navegador mesmo assim: o algoritmo de análise do HTML é deliberadamente tolerante, e "conserta" a árvore do documento por conta própria. O problema é que cada navegador conserta de um jeito, e o resultado deixa de ser previsível — sem falar que leitores de tela e buscadores dependem da estrutura correta.

Duas conferências rápidas, feitas no próprio DevTools:

  1. Aba Console, nas cinco páginas: zero mensagens em vermelho. Cada Uncaught é um recurso do seu site que não funciona.
  2. Aba Network, com o filtro de status: qualquer linha 404 é um arquivo que o HTML pede e o servidor não tem. Nome errado, arquivo esquecido fora do commit ou caminho equivocado.

Para os links internos, o teste é manual e leva cinco minutos: clique em todos, em todas as páginas, incluindo os do rodapé e os do menu. Depois de publicado, um serviço como o W3C Link Checker faz a varredura sozinho.

4.3 Lighthouse: a nota do seu site

O Lighthouse vem embutido no Chrome e no Edge. Ele carrega a sua página simulando um celular modesto em rede 4G, mede dezenas de indicadores e devolve quatro notas de 0 a 100.

Categoria Meta nesta trilha O que costuma derrubar
Desempenho ≥ 80 Imagens grandes, fontes pesadas, layout instável
Acessibilidade ≥ 90 Contraste, alt ausente, label ausente, ordem de títulos
Práticas recomendadas ≥ 90 Erros no console, imagens sem proporção, recursos por HTTP
SEO ≥ 90 Sem description, sem title, link sem texto, sem viewport

Como rodar, do jeito certo:

  1. Abra o site publicado (não o localhost) em uma janela anônima — extensões do navegador poluem o resultado e derrubam a nota de práticas recomendadas.
  2. F12 → aba Lighthouse.
  3. Modo Navigation, dispositivo Mobile, as quatro categorias marcadas.
  4. Analyze page load. Aguarde de vinte segundos a um minuto.

Rode duas ou três vezes: a nota de desempenho oscila alguns pontos entre execuções, porque depende da rede e da carga da sua máquina naquele instante. Considere a mediana.

As correções de maior impacto, em ordem de retorno pelo esforço:

  1. Comprimir e converter as imagens para WebP.
  2. Declarar width e height em todas as imagens.
  3. Aplicar loading="lazy" no que está abaixo da dobra.
  4. Reduzir a dois os pesos de fonte importados (por exemplo, 400 e 700 apenas).
  5. Corrigir os contrastes reprovados.
  6. Zerar os erros do console.
  7. Preencher title e description de cada página.

🔬 Investigue Rode o Lighthouse no site do evento antes de otimizar as imagens e anote as quatro notas. Depois abra a aba Network, marque Disable cache, recarregue e leia a barra de status no rodapé: ela mostra o total transferido. Anote também esse número. Faça a otimização das imagens e repita as duas medições. Na prática, o peso da página inicial costuma cair de 6 MB para menos de 500 KB, e o desempenho salta trinta pontos — com uma tarde de trabalho e nenhuma linha de código. É a melhor relação entre esforço e resultado de toda a trilha.

4.4 O teste que nenhuma ferramenta faz

Automação não cobre tudo. Antes de considerar o site pronto:

  • Teclado apenas. Percorra o site inteiro com Tab, Shift+Tab, Enter e Esc. Todo elemento focado precisa ser visível, e a ordem precisa fazer sentido.
  • Celular real, com dados móveis. O emulador do DevTools mente sobre desempenho e sobre o tamanho do seu dedo.
  • Dois navegadores. Chrome e Firefox, no mínimo.
  • Janela anônima. Elimina cache, cookies e extensões — é o que um visitante novo vê.
  • Zoom em 200%. Requisito de acessibilidade: o conteúdo tem que continuar utilizável, sem rolagem horizontal.

5. Git: o mínimo para publicar

O Git é um sistema de controle de versão: ele guarda fotografias sucessivas do seu projeto, permite voltar a qualquer uma delas e junta o trabalho de várias pessoas. Ele é também o mecanismo pelo qual o seu código chega ao GitHub e, de lá, ao ar.

Esta seção é o suficiente para publicar. A versão completa — branches, merges, conflitos, pull requests, stash, tags — está no Capítulo 02 — Git e GitHub do zero ao pull request.

5.1 Configuração, uma vez na vida

Terminal
git config --global user.name "Seu Nome Completo"
git config --global user.email "seu-email@exemplo.com"
git config --global init.defaultBranch main

O e-mail precisa ser o mesmo cadastrado no GitHub. É por ele que a plataforma associa cada commit à sua conta — e é assim que fica registrado quem escreveu o quê (inclusive para o professor conferir, se você cursa esta trilha em uma turma). Confira o que ficou gravado:

Terminal
git config --global --list

5.2 Os seis comandos do dia a dia

Terminal
git init                    # cria o repositório na pasta atual (uma vez por projeto)
git status                  # o que mudou desde o último commit
git add .                   # prepara todas as mudanças para o próximo commit
git commit -m "mensagem"    # grava a fotografia com uma descrição
git log --oneline           # o histórico, uma linha por commit
git push                    # envia os commits para o GitHub

O ciclo normal de trabalho tem três passos, repetidos indefinidamente: editar → git add .git commit -m "…". Ao fim da sessão, git push.

5.3 Mensagens de commit que servem para alguma coisa

Uma mensagem de commit é um bilhete para o seu eu de daqui a três meses. Escreva no imperativo, dizendo o que o commit faz ao projeto:

Terminal
git commit -m "Adiciona validação de CPF no formulário de inscrição"
git commit -m "Corrige menu que não fechava com Esc no celular"
git commit -m "Otimiza imagens dos palestrantes para WebP"

O que não escrever: alteracoes, att, final, final2, agora vai, .. Um histórico assim é indistinguível de nenhum histórico.

Uma boa regra: um commit por ideia. Terminou o menu? Commit. Corrigiu o contraste dos botões? Outro commit. Commits pequenos e frequentes são fáceis de entender e de desfazer.

5.4 .gitignore: o que nunca entra no repositório

Alguns arquivos não pertencem ao repositório: lixo do sistema operacional, configuração pessoal do editor, dependências que qualquer um consegue reinstalar e — principalmente — segredos.

site-evento/.gitignore

Texto
# Sistema operacional
.DS_Store
Thumbs.db
desktop.ini

# Editor
.vscode/
.idea/

# Dependências e logs
node_modules/
*.log

# Arquivos temporários e originais pesados
*.tmp
img-originais/

A pasta img-originais/ é uma sugestão prática: guarde nela as fotos em tamanho original, fora do Git, e versione apenas as versões otimizadas que o site usa.

⚠️ Atenção O .gitignore só funciona para arquivos que ainda não foram commitados. Se você já enviou um arquivo por engano, adicioná-lo ao .gitignore não o remove do repositório nem do histórico. E, uma vez publicado, considere que o conteúdo vazou: um repositório público é lido por robôs em minutos. Nunca coloque senhas, tokens ou chaves de API em um repositório — no seu site do evento não há nenhum, mas no Nível 2 haverá.

5.5 O .nojekyll

O GitHub Pages nasceu como uma plataforma para o gerador de sites Jekyll, e por herança ele ainda processa os arquivos antes de publicá-los. Um efeito colateral: pastas e arquivos que começam com _ são ignorados. Se um dia você criar _imagens/ ou _parciais/, elas simplesmente não vão ao ar.

A solução é um arquivo vazio na raiz, chamado .nojekyll:

Terminal
touch .nojekyll

No Windows, sem o Git Bash, o mesmo efeito com o PowerShell:

Terminal
New-Item -Path .nojekyll -ItemType File

6. GitHub Pages passo a passo

O GitHub Pages transforma um repositório em um site. A cada git push, ele pega os arquivos da branch escolhida e os serve em um endereço público, com HTTPS incluído. É gratuito para repositórios públicos e não exige cartão de crédito.

6.1 Criar o repositório no GitHub

  1. Entre em github.com e clique no + no canto superior direito → New repository.
  2. Repository name: site-evento (minúsculas, com hífen, sem acento e sem espaço — esse nome vai aparecer na URL).
  3. Description: uma frase sobre o projeto.
  4. Public. Obrigatório aqui: em repositório privado, o GitHub Pages só está disponível nos planos pagos.
  5. Não marque nada em "Initialize this repository": nem README, nem .gitignore, nem licença. Você já tem esses arquivos localmente, e um repositório remoto com commits próprios complica o primeiro push.
  6. Create repository.

A página seguinte mostra os comandos para conectar um repositório local existente. São estes:

Terminal
git remote add origin https://github.com/usuario/site-evento.git
git branch -M main
git push -u origin main

Traduzindo:

  • git remote add origin <url> — apelida a URL do repositório remoto como origin.
  • git branch -M main — garante que a branch local se chama main (o nome que o GitHub espera).
  • git push -u origin main — envia os commits e memoriza o destino; a partir daí, git push sozinho basta.

Na primeira vez, o Git pedirá autenticação. O navegador abre e você autoriza — é o fluxo padrão do Git Credential Manager. Se preferir chave SSH ou o GitHub CLI, o Capítulo 02 cobre os dois.

6.2 Ativar o Pages

  1. No repositório, abra Settings (a engrenagem, na barra superior do repositório — não a do seu perfil).
  2. No menu lateral esquerdo, clique em Pages.
  3. Em Source, escolha Deploy from a branch.
  4. Em Branch, escolha main e a pasta / (root). Clique em Save.
  5. Aguarde de um a três minutos. Recarregue a página: aparece uma faixa verde com Your site is live at e o endereço.

O endereço segue o padrão:

Texto
https://<seu-usuario>.github.io/<nome-do-repositorio>/

Para o repositório site-evento da usuária ana-souza, por exemplo: https://ana-souza.github.io/site-evento/.

Existe um caso especial: se você nomear o repositório exatamente como <seu-usuario>.github.io, o site é publicado na raiz do subdomínio, sem subpasta. É a escolha certa para um portfólio pessoal — e cada conta só pode ter um.

🧠 Você sabia? O github.io é um domínio separado do github.com por um motivo de segurança, não de estética. Se as páginas dos usuários fossem servidas em github.com/paginas/…, qualquer JavaScript publicado por qualquer pessoa rodaria na mesma origem do GitHub — e poderia, pela política de mesma origem, ler os cookies de sessão de quem estivesse logado. Isolando os sites em outro domínio, o navegador impede a leitura. A mesma preocupação explica por que o Google usa googleusercontent.com e a Microsoft usa sharepointonline.com para conteúdo enviado por usuários.

6.3 A armadilha do subdiretório

Este é o erro que mais aparece na primeira publicação. O site funciona no Live Server, sobe para o Pages e chega sem estilo nenhum.

A causa: caminhos que começam com barra.

HTML
<!-- Funciona no Live Server, quebra no GitHub Pages -->
<link rel="stylesheet" href="/css/estilo.css">

No Live Server, a raiz é a pasta do projeto, e /css/estilo.css resolve certo. No Pages, a raiz é https://usuario.github.io/, e a barra manda o navegador buscar https://usuario.github.io/css/estilo.css — um lugar onde o seu CSS não está.

HTML
<!-- Funciona nos dois -->
<link rel="stylesheet" href="css/estilo.css">

Regra prática para o seu site: nenhum href ou src interno começa com /, exceto os da 404.html, pelo motivo já explicado na seção 3.6. Localize os culpados de uma vez com a busca do VS Code (Ctrl+Shift+F) procurando por href="/ e src="/.

6.4 Republicar e o cache

Publicar de novo é só isto:

Terminal
git add .
git commit -m "Corrige caminhos das imagens para publicação"
git push

De trinta segundos a dois minutos depois, o site no ar reflete a mudança. A aba Actions do repositório mostra a publicação em andamento; um visto verde significa concluída.

Se a alteração não aparecer, o culpado quase sempre é o cache do navegador: recarregue com Ctrl+Shift+R (ou Cmd+Shift+R no macOS), ou abra em janela anônima. Antes de concluir que "o Pages está com problema", confirme no próprio GitHub que o commit chegou: se o arquivo mudou lá, o problema está do seu lado da conexão.

7. Netlify e Vercel: quando valem mais

O GitHub Pages resolve o caso desta trilha. Duas alternativas gratuitas resolvem casos que ele não cobre.

Netlify. Em netlify.com, há dois caminhos. O primeiro é literalmente arrastar a pasta do projeto para a área indicada no painel: em vinte segundos o site está no ar, sem Git nenhum. O segundo, recomendado, é Import from Git → autorizar o GitHub → escolher o repositório → deixar o build command vazio e o publish directory como .Deploy. O endereço sai como nome-aleatorio.netlify.app e pode ser renomeado nas configurações do site.

Vercel. Em vercel.com, o fluxo é praticamente idêntico ao da Netlify. Para um site estático, o resultado é o mesmo; a Vercel brilha quando há um framework moderno envolvido.

Recurso GitHub Pages Netlify Vercel
Custo e HTTPS automático Gratuito, sim Gratuito, sim Gratuito, sim
Prévia automática por branch Não Sim Sim
Formulários sem back-end Não Sim Não
Redirecionamentos e cabeçalhos Limitado Sim Sim

A prévia por branch é o recurso mais transformador dos três da tabela: cada branch enviada ganha uma URL própria, o que permite mostrar uma versão em teste sem tocar no site oficial. Os formulários da Netlify recebem envios de um <form> HTML puro e mostram as respostas no painel — resolvendo, sem back-end, o formulário de contato que hoje só existe visualmente no seu site.

Para o Marco 3, qualquer uma das três serve. Publique em uma; se sobrar tempo, publique nas três e compare (é o exercício B4).

8. Domínio próprio, em quatro passos

Você não precisa de domínio próprio para o marco. Mas se quiser um — para o portfólio, por exemplo —, o caminho é curto:

  1. Registre. No Registro.br para .com.br (exige CPF e custa por volta de quarenta reais por ano); em registradores internacionais para .com, .dev, .me.
  2. Aponte o DNS. No painel do registrador, crie os registros que a plataforma de hospedagem indicar: quatro registros A para o GitHub Pages (com os IPs listados na documentação oficial) ou um CNAME apontando para <seu-usuario>.github.io.
  3. Declare o domínio na plataforma. No GitHub: Settings → Pages → Custom domain. Isso cria um arquivo CNAME na raiz do repositório — não o apague.
  4. Ative o HTTPS. Marque Enforce HTTPS. O certificado é emitido automaticamente, em minutos ou algumas horas.

A propagação do DNS leva de minutos a algumas horas; enquanto isso, é normal ver o site ora pelo endereço antigo, ora pelo novo. O Capítulo 04 — Domínios, DNS e HTTPS explica cada tipo de registro, o TTL e como diagnosticar problemas com dig e nslookup.

9. O README.md: a capa do projeto

O README.md é a primeira coisa que o GitHub mostra ao abrir o repositório — e, muitas vezes, a única que um avaliador apressado lê. Ele é escrito em Markdown, a mesma linguagem desta apostila.

site-evento/README.md

Markdown
# Semana Acadêmica de Sistemas de Informação

Site do evento acadêmico do curso de Sistemas de Informação, desenvolvido com o
[WebLab](https://weblab.aprendabit.com), trilha Introdução ao Desenvolvimento Web.

**Site publicado:** https://usuario.github.io/site-evento/

## Sobre

Site de cinco páginas que divulga a programação do evento, apresenta os
palestrantes e recebe inscrições. O objetivo é concentrar em um só lugar as
informações que hoje se perdem em grupos de mensagens: o que acontece, quando,
onde e como participar.

O visitante encontra a programação completa com busca e filtro por trilha, a
lista de palestrantes e um formulário de inscrição validado no navegador.

## Funcionalidades

- Menu de navegação responsivo, acessível por teclado, com indicação da página atual
- Programação renderizada a partir de dados em JavaScript, com busca, filtro por
  trilha e ordenação
- Formulário de inscrição com validação campo a campo, incluindo CPF, telefone e
  e-mail, com mensagens acessíveis
- Rascunho da inscrição salvo no navegador com localStorage
- Layout responsivo em três larguras, com animações que respeitam
  prefers-reduced-motion

## Tecnologias

HTML5 semântico, CSS3 (variáveis, Flexbox, Grid, media queries) e JavaScript
(ES2015+), sem frameworks nem bibliotecas externas.

## Estrutura

- `index.html`, `programacao.html`, `inscricao.html`, `palestrantes.html`,
  `contato.html` — as cinco páginas
- `404.html` — página de erro
- `css/estilo.css` — folha de estilo única, organizada por seções comentadas
- `js/menu.js` — abre e fecha o menu hambúrguer (Aulas 08, 09 e 13)
- `js/efeitos.js` — revelação ao rolar com IntersectionObserver (Aula 09)
- `js/dados.js` — fonte única de dados: palestras, palestrantes e áreas
- `js/app.js` — comportamento comum a todas as páginas (contagem regressiva,
  `aria-current` e `debounce`)
- `js/relatorios.js` — relatórios do evento no console (Aula 12)
- `js/palestrantes.js` — lista de palestrantes com filtro por área (Aula 13)
- `js/inscricao.js` — vagas restantes e validação do formulário
- `js/programacao.js` — busca, filtro e ordenação da programação
- `img/` — imagens otimizadas em WebP e SVG

## Como executar localmente

1. Clone o repositório: `git clone https://github.com/usuario/site-evento.git`
2. Abra a pasta no VS Code
3. Clique com o botão direito em `index.html` e escolha "Open with Live Server"

## Auditoria

Lighthouse (mobile, janela anônima, no site publicado):
desempenho 92, acessibilidade 100, práticas recomendadas 100, SEO 100.
HTML validado sem erros no validator.w3.org em todas as páginas.

## Autoria

Desenvolvido por Nome Sobrenome, estudante de Sistemas de Informação.

Sete seções, nenhuma opcional na prática: o que é, onde está no ar, o que faz, com o que foi feito, como está organizado, como rodar e quem fez. Um README com o link do site no topo economiza o tempo de quem avalia — e isso conta.

💡 Dica No Markdown do GitHub, três coisas melhoram muito o README com pouco esforço: uma captura de tela do site logo abaixo do título (![Página inicial do site](img/captura-inicial.png)), listas de tarefas com - [x] para mostrar o que já está pronto, e blocos de código com a linguagem declarada. Evite badges decorativos: eles enchem o topo e não dizem nada sobre um trabalho acadêmico.

💻 Mão na massa — O site do evento no ar

Ao fim deste roteiro, o site do evento acadêmico estará acessível em https://<seu-usuario>.github.io/site-evento/, auditado e documentado. Reserve os cinquenta minutos do segundo bloco e os primeiros vinte do terceiro.

Passo 1 — Limpeza e conferência local

Com o projeto aberto no VS Code:

  1. Ctrl+Shift+F e procure por console.log. Substitua cada ocorrência de depuração por depurar(, depois de acrescentar o bloco da seção 3.3 ao topo do js/dados.js. Mantenha os console.error.
  2. Procure por href="/ e por src="/. Cada resultado é um caminho absoluto que vai quebrar no Pages: remova a barra inicial.
  3. Procure por href="#". Todo link de menu ou de rodapé precisa apontar para uma página real.
  4. Apague arquivos órfãos: testes, cópias, folhas antigas, pastas de rascunho.
  5. Renomeie para minúsculas, sem espaço e sem acento, qualquer arquivo que ainda esteja fora do padrão — e corrija as referências no HTML e no CSS.
  6. Abra as cinco páginas no Live Server e confirme: console limpo, nenhuma linha vermelha na aba Network.

Passo 2 — O favicon

Crie um favicon vetorial simples com as iniciais do evento. Um SVG resolve todos os tamanhos e pesa menos de 1 KB.

site-evento/img/favicon.svg

HTML
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="SASI">
  <rect width="64" height="64" rx="14" fill="#0b3d5c"></rect>
  <text x="32" y="42" text-anchor="middle"
        font-family="Segoe UI, Roboto, sans-serif"
        font-size="26" font-weight="700" fill="#ffffff">SA</text>
</svg>

Referencie-o no <head> de todas as páginas:

HTML
<link rel="icon" href="img/favicon.svg" type="image/svg+xml">

Passo 3 — Metadados nas cinco páginas

Cada página recebe title e description próprios. Copie o <head> completo da seção 3.5 para o index.html e ajuste as demais:

site-evento/programacao.html

HTML
<title>Programação — Semana Acadêmica de Sistemas de Informação</title>
<meta name="description" content="Palestras, minicursos e mesas-redondas dos três dias do evento, com busca por título e filtro por trilha.">

site-evento/inscricao.html

HTML
<title>Inscrição — Semana Acadêmica de Sistemas de Informação</title>
<meta name="description" content="Inscreva-se gratuitamente na Semana Acadêmica de Sistemas de Informação e garanta o seu certificado de participação.">

site-evento/palestrantes.html

HTML
<title>Palestrantes — Semana Acadêmica de Sistemas de Informação</title>
<meta name="description" content="Conheça quem vai palestrar no evento: professores, profissionais do mercado e egressos do curso.">

site-evento/contato.html

HTML
<title>Contato — Semana Acadêmica de Sistemas de Informação</title>
<meta name="description" content="Fale com a organização da Semana Acadêmica: e-mail, telefone e endereço do campus de Sinop.">

As metatags og: ficam iguais em todas as páginas, exceto og:title e og:url, que acompanham a página. Se ainda não tiver a imagem de prévia, gere uma de 1200 × 630 com o logo e o nome do evento e salve como img/preview.jpg.

Passo 4 — A página 404

Crie 404.html na raiz com o conteúdo da seção 3.6 e acrescente ao fim de css/estilo.css o bloco .erro-404. Teste depois de publicar: abrir https://<seu-usuario>.github.io/site-evento/pagina-que-nao-existe deve mostrar a sua página de erro.

Passo 5 — Otimizar as imagens

  1. Crie a pasta img-originais/ e mova para lá as fotos em tamanho original.
  2. Para cada imagem, abra o squoosh.app, arraste o arquivo, escolha WebP com qualidade 75 e ajuste a largura para o dobro da exibição (uma foto exibida em 400 px sai com 800 px de largura).
  3. Salve o resultado em img/, com nome em minúsculas e hífens.
  4. Atualize os src no HTML e acrescente width, height e loading="lazy" no que estiver abaixo da dobra:

As fotos dos palestrantes não estão mais no HTML desde a Aula 13 — quem as escreve é o js/palestrantes.js, a partir do campo foto do js/dados.js. Então o ajuste acontece em um lugar só: no array de dados, trocando a extensão dos seis caminhos de .jpg para .webp. Nada mais do arquivo muda — os nomes, as áreas, os id e os palestranteId são os mesmos desde a Aula 12, e o relatorios.js e o programacao.js continuam lendo os mesmos campos.

site-evento/js/dados.js (trecho: só o campo foto de cada pessoa)

JavaScript
const palestrantes = [
  { id: 1, nome: "Ana Lúcia Ferreira", instituicao: "Universidade Estadual — Sinop", area: "ia",
    tema: "Redes neurais para prever a safra de soja",
    foto: "img/palestrante-01.webp" },
  { id: 2, nome: "Bruno Takahashi", instituicao: "Startup AgroData", area: "dados",
    tema: "Dashboards que os produtores realmente usam",
    foto: "img/palestrante-02.webp" },
  { id: 3, nome: "Carla Mendes", instituicao: "UFMT", area: "seguranca",
    tema: "O que um ataque de phishing ensina sobre UX",
    foto: "img/palestrante-03.webp" },
  { id: 4, nome: "Diego Nascimento", instituicao: "Prefeitura de Sinop", area: "web",
    tema: "Acessibilidade em portais públicos: erros que vimos",
    foto: "img/palestrante-04.webp" },
  { id: 5, nome: "Eduarda Ribeiro", instituicao: "Universidade Estadual — Sinop", area: "web",
    tema: "Do HTML ao deploy: o caminho do estudante",
    foto: "img/palestrante-05.webp" },
  { id: 6, nome: "Felipe Arruda", instituicao: "Cooperativa Coopercana", area: "ia",
    tema: "Visão computacional no controle de pragas",
    foto: "img/palestrante-06.webp" },
];

Recarregue palestrantes.html: os seis cartões continuam lá, agora com as fotos em WebP. Se algum ficar com o ícone de imagem quebrada, o nome do arquivo em img/ não bate com o do array — o Linux do GitHub Pages diferencia maiúsculas de minúsculas, e o seu Windows não.

  1. Compare o peso da página na aba Network antes e depois. Anote os dois números: eles entram na atividade assíncrona.

Passo 6 — .gitignore, .nojekyll e README.md

Crie os três arquivos na raiz do projeto: o .gitignore da seção 5.4, o .nojekyll vazio e o README.md da seção 9, com os seus dados. Deixe o campo do Lighthouse em branco por enquanto — você o preenche no Passo 10.

Passo 7 — O repositório local

No terminal integrado do VS Code (Ctrl+'), dentro da pasta site-evento/:

Terminal
git init
git status

O git status lista todos os arquivos como não rastreados. Confirme que img-originais/ não aparece — se aparecer, o .gitignore está com o nome errado ou fora da raiz.

Agora o primeiro commit:

Terminal
git add .
git commit -m "Site do evento academico completo, pronto para publicacao"

E confira:

Terminal
git log --oneline

Uma linha com um código de sete caracteres e a sua mensagem. Esse código é o identificador do commit.

Passo 8 — O repositório no GitHub

  1. Crie no GitHub o repositório público site-evento, sem inicializar com nenhum arquivo (seção 6.1).
  2. Conecte e envie:
Terminal
git remote add origin https://github.com/SEU-USUARIO/site-evento.git
git branch -M main
git push -u origin main

Troque SEU-USUARIO pelo seu nome de usuário do GitHub. Autentique no navegador quando pedido.

  1. Recarregue a página do repositório: os arquivos estão lá, e o README.md aparece renderizado abaixo da lista.

Passo 9 — Ativar o GitHub Pages

  1. Settings → Pages.
  2. Source: Deploy from a branch. Branch: main, pasta / (root). Save.
  3. Espere de um a três minutos e recarregue. Copie o endereço da faixa verde.
  4. Abra o endereço em uma janela anônima.

Passo 10 — Corrigir o que quebrou e auditar

É normal que algo quebre agora. Com o site aberto no endereço público:

  1. F12Console: nenhum erro. Se houver Failed to load resource, veja qual arquivo faltou.
  2. F12Network, com Disable cache marcado: nenhuma linha com status 404. Cada 404 é caminho errado ou arquivo que ficou de fora do commit.
  3. Clique em todos os links, nas cinco páginas.
  4. Abra https://<seu-usuario>.github.io/site-evento/pagina-que-nao-existe e confirme que a sua 404.html aparece.
  5. Valide as cinco páginas em validator.w3.org, no modo Address, colando a URL de cada uma. Corrija os erros.
  6. Rode o Lighthouse (Mobile, janela anônima) na página inicial e anote as quatro notas.
  7. Aplique as correções da lista da seção 4.3, da primeira para a última, até bater as metas.
  8. Preencha a seção Auditoria do README.md com as notas finais.

A cada rodada de correções, republique:

Terminal
git add .
git commit -m "Corrige caminhos e otimiza imagens apontadas pelo Lighthouse"
git push

Passo 11 — A prova dos nove

Pegue o celular, desligue o Wi-Fi e abra o endereço público usando dados móveis. Navegue pelas cinco páginas, abra o menu, use a busca da programação e preencha o formulário de inscrição.

Se tudo funcionar aí, o seu site está no ar de verdade. Mande o link para alguém que não viu o projeto antes e peça para abrir.

Como testar

  1. O endereço https://<seu-usuario>.github.io/site-evento/ abre a página inicial em janela anônima, com todo o estilo aplicado.
  2. As cinco páginas carregam, com CSS e JavaScript, e o menu funciona em todas.
  3. A aba da página mostra o seu favicon.
  4. O console está limpo e a aba Network não tem nenhum 404 nas cinco páginas.
  5. https://<seu-usuario>.github.io/site-evento/qualquer-coisa mostra a sua página 404 personalizada, com os links funcionando.
  6. Colar o link em uma conversa de WhatsApp gera um cartão com título, descrição e imagem.
  7. O validador do W3C não aponta erros em nenhuma das cinco páginas.
  8. O Lighthouse (Mobile, anônima) devolve desempenho ≥ 80 e acessibilidade, práticas recomendadas e SEO ≥ 90.
  9. git log --oneline mostra ao menos cinco commits com mensagens descritivas.
  10. O README.md aparece renderizado no GitHub, com o link do site publicado logo no início.
  11. O site funciona no celular, com dados móveis, sem depender do Wi-Fi do campus.

🧪 Laboratório

Nível A — Fixação

A1. Cite as três diferenças entre o ambiente local e o servidor que mais quebram sites recém-publicados, e a regra que evita cada uma.

A2. Por que o arquivo de entrada precisa se chamar index.html e ficar na raiz do repositório? O que o servidor faz quando não encontra esse arquivo?

A3. Explique, para alguém que nunca ouviu falar do assunto, a diferença entre hospedagem, domínio e certificado HTTPS. Dê um exemplo de cada um no endereço https://ana.github.io/site-evento/.

A4. Qual a diferença entre href="/css/estilo.css" e href="css/estilo.css" em um site publicado em usuario.github.io/site-evento/? Qual dos dois funciona, e por quê?

A5. Para que serve o .gitignore? Cite três entradas típicas e explique o que aconteceria sem elas.

A6. Escreva, na ordem correta, a sequência completa de comandos para transformar uma pasta com o site em um repositório publicado no GitHub — do git init ao git push -u origin main.

A7. Quais são as quatro categorias avaliadas pelo Lighthouse e qual é a meta de cada uma nesta trilha? Cite duas causas comuns de nota baixa em cada uma.

A8. Por que a auditoria do Lighthouse deve ser feita no site publicado, em janela anônima e no modo Mobile? O que cada uma dessas três escolhas evita?

A9. O que fazem os atributos width, height e loading="lazy" em uma tag <img>? Em qual imagem da sua página inicial você não deve usar loading="lazy", e por quê?

A10. Para que servem as metatags og:? Por que a og:image precisa de URL absoluta, enquanto o src de uma <img> pode ser relativo?

Nível B — Aplicação

B1. Publique o site do evento no GitHub Pages e produza um registro do processo: uma captura de tela de cada etapa (repositório criado, primeiro push, tela do Pages, site no ar) e um parágrafo descrevendo um problema que você encontrou e como o resolveu.

Resultado esperado: um arquivo publicacao.md no repositório, com as quatro capturas e o relato; o site acessível pela URL pública em janela anônima.

Dica

No GitHub, arraste a imagem para dentro do editor de arquivos: ele faz o upload e insere o Markdown sozinho. Se preferir versionar as capturas, crie img/capturas/ e referencie com caminho relativo.

B2. Otimize todas as imagens do projeto: redimensione, converta para WebP, aplique loading="lazy" onde couber e declare width e height em todas. Registre o peso total da página inicial antes e depois, com as capturas da aba Network.

Resultado esperado: redução de pelo menos 60% no peso total transferido da página inicial, sem perda visível de qualidade; nenhuma imagem sem alt, width e height.

Dica

Na aba Network, o rodapé mostra transferred (o que veio pela rede) e resources (o tamanho descompactado). Compare o primeiro. Marque Disable cache antes de medir, senão a segunda medição vem do cache e não significa nada.

B3. Rode o Lighthouse no site publicado, registre as quatro pontuações iniciais, aplique todas as correções sugeridas pelo relatório e registre as pontuações finais. Escreva um parágrafo sobre qual correção teve o maior impacto e por quê.

Resultado esperado: uma tabela antes/depois com as quatro notas no README.md e as metas da trilha atingidas.

Dica

Abra cada item reprovado no relatório: o Lighthouse mostra exatamente quais elementos causaram o problema e estima quantos milissegundos você ganha ao corrigir. Comece pelos de maior estimativa.

B4. Publique o mesmo projeto nas três plataformas (GitHub Pages, Netlify e Vercel) e compare: passos necessários, tempo até ficar no ar, o que cada painel oferece e o que faltou em cada um.

Resultado esperado: três URLs funcionais e uma tabela comparativa de no máximo quatro colunas, com sua conclusão sobre qual usar em cada situação.

Dica

Na Netlify e na Vercel, importe o mesmo repositório do GitHub — não recrie o projeto. Como o site é estático, o campo de comando de build fica vazio e o diretório de publicação é a raiz.

B5. Escreva o README.md completo do projeto seguindo o modelo da seção 9, acrescentando uma captura de tela da página inicial logo abaixo do título e uma lista de tarefas com - [x] mostrando o que já está pronto e o que ficou para depois.

Resultado esperado: o README renderizado no GitHub, sem link quebrado e sem imagem faltando, legível por alguém que nunca viu o projeto.

Dica

Teste o resultado renderizado, não o texto-fonte: uma imagem referenciada com caminho errado aparece como um ícone quebrado. Caminhos no README são relativos à raiz do repositório.

Nível C — Desafio

C1. Faça uma auditoria cruzada: troque URLs com outra pessoa que esteja estudando esta trilha (um colega de turma, alguém de um grupo de estudos, ou uma troca combinada em algum fórum de desenvolvimento) e cada um audita o site do outro. Sem ninguém para trocar? Audite o site de um projeto público que você admire, com os mesmos critérios. Produza um relatório de uma página com as quatro notas do Lighthouse (mobile e desktop), o resultado da validação W3C de todas as páginas, a navegação completa por teclado (o que quebrou), o teste com zoom em 200% e uma lista priorizada de cinco problemas com a correção sugerida para cada um.

Dica

Auditar site alheio é mais fácil que auditar o próprio: você não tem os atalhos mentais de quem construiu. Comece pelo teclado — desconecte o mouse de verdade — e depois rode as ferramentas. Escreva os problemas de forma acionável: "o botão de filtro não recebe foco visível" vale mais do que "acessibilidade ruim".

C2. Configure o repositório com um fluxo de trabalho por branch: crie uma branch com uma melhoria (por exemplo, um modo escuro no CSS), publique-a, abra um pull request descrevendo a mudança, peça a revisão de outra pessoa (ou revise você mesmo com calma, no dia seguinte) e só então faça o merge na main. Documente o processo com as URLs do PR e das revisões.

Dica

Os comandos são git switch -c melhoria/modo-escuro, os commits normais e git push -u origin melhoria/modo-escuro. O GitHub oferece o botão "Compare & pull request" sozinho quando detecta a branch nova. O Capítulo 02 da trilha Deploy tem o fluxo completo com revisão.

🏆 Desafios

⭐ O site que quebrou ao subir

deploybugdevtoolsinvestigacao

O repositório abaixo quase funciona no Live Server do Windows e chega ao GitHub Pages sem estilo, sem imagens e com o JavaScript morto. São cinco linhas com defeito, e nenhum defeito é erro de sintaxe: todos são consequência das diferenças entre a sua máquina e um servidor Linux servindo o site em um subdiretório. Encontre as cinco lendo apenas as abas Console e Network do DevTools, sem baixar o repositório.

index.html (trecho do <head> e do corpo)

HTML
<head>
  <meta charset="UTF-8">
  <title>Meu site</title>
  <link rel="stylesheet" href="/CSS/Estilo.css">
  <script src="js/App.js" defer></script>
</head>
<body>
  <img src="C:/Users/ana/projeto/img/logo.png" alt="Logotipo">
  <a href="/programacao.html">Programação</a>
  <img src="img/Foto Palestrante.JPG" alt="Palestrante">
</body>

Critérios de pronto

  • Os cinco defeitos estão listados, cada um com o sintoma exato que aparece no DevTools (mensagem literal do console ou status da aba Network).
  • Cada defeito tem a correção escrita, com o antes e o depois da linha.
  • Um parágrafo explica por que quatro dos cinco passam despercebidos no Live Server do Windows — e identifica qual é o quinto, que falha em qualquer ambiente.
  • A regra geral que evitaria os cinco de uma vez está enunciada em uma frase.
Pistas
  1. Duas abas bastam: no Console aparecem os erros de script; na Network, o status de cada arquivo pedido.
  2. Compare, letra a letra, o nome do arquivo no HTML e o nome real no repositório. Sistemas de arquivos do Linux não perdoam.
  3. Qual é a raiz de usuario.github.io/site-evento/? Onde uma barra inicial faz o navegador procurar?
  4. Um dos cinco defeitos nem chega a virar requisição HTTP: o navegador se recusa a buscar aquele endereço a partir de uma página web.
⭐⭐

⭐⭐ Cem pontos em acessibilidade

acessibilidadeperformancedevtoolsdeploy

O Lighthouse dá nota 100 em acessibilidade para páginas que um usuário de leitor de tela não consegue usar — ele testa o que é automatizável, e isso é menos da metade do problema. Neste desafio você faz as duas coisas: tira 100 na ferramenta e prova que o site funciona sem mouse e sem enxergar a tela.

Critérios de pronto

  • As cinco páginas do site publicado marcam 100 em acessibilidade no Lighthouse, em Mobile e em Desktop.
  • Um vídeo de até três minutos mostra a navegação completa de uma tarefa (abrir a programação, buscar uma palestra, ir à inscrição e preencher o formulário) usando apenas o teclado, com o foco visível em cada parada.
  • O mesmo percurso é feito com um leitor de tela (NVDA no Windows, VoiceOver no macOS ou Orca no Linux) e relatado em texto: o que foi anunciado, o que ficou mudo, o que confundiu.
  • Um relatório lista pelo menos três problemas que o Lighthouse não detectou e as correções aplicadas.
  • Todas as correções estão publicadas no site no ar, não apenas no repositório local.
Pistas
  1. Comece pelo relatório do Lighthouse: a seção "Additional items to manually check" lista justamente o que a ferramenta não consegue testar sozinha.
  2. Ordem de tabulação, foco preso dentro de um menu aberto, mensagens que aparecem sem serem anunciadas e imagens decorativas com alt descritivo são os quatro problemas invisíveis mais comuns.
  3. No NVDA, Insert+F7 abre a lista de elementos da página: se os seus títulos e links não fizerem sentido fora do contexto visual, o problema aparece ali imediatamente.
  4. Uma região aria-live só anuncia mudanças se já existir no HTML quando a página carrega. Criar o elemento junto com a mensagem faz o leitor de tela ignorá-la.
⭐⭐⭐

⭐⭐⭐ Meio megabyte, no máximo

performancedeploydevtoolsrefatoracao

A página inicial de um site de evento não deveria pesar mais que um aplicativo de celular. Neste desafio, a meta é objetiva e medida por ferramenta: a página inicial do seu site, carregada pela primeira vez em um navegador sem cache, transfere no máximo 500 KB no total — HTML, CSS, JavaScript, fontes e imagens somados — mantendo o mesmo visual e as mesmas funcionalidades.

Critérios de pronto

  • A aba Network, com Disable cache marcado e o perfil de rede em Fast 4G, mostra total transferido ≤ 500 KB na primeira carga da página inicial publicada.
  • Uma tabela de no máximo quatro colunas mostra o peso por tipo de recurso (documento, estilo, script, imagem, fonte), antes e depois.
  • O desempenho no Lighthouse mobile é ≥ 90, e a métrica de estabilidade visual (CLS) fica abaixo de 0,1.
  • Nenhuma funcionalidade foi removida para atingir a meta, e nenhuma imagem ficou visivelmente pior. As decisões de compressão estão justificadas em um parágrafo.
  • Um parágrafo final explica qual foi a otimização de maior impacto e quantos KB ela economizou.
Pistas
  1. Ordene a aba Network pela coluna de tamanho, em ordem decrescente. Os três primeiros itens costumam responder por 80% do peso; comece por eles.
  2. Fontes do Google Fonts entram com vários pesos por padrão. Cada peso é um arquivo. Dois pesos bastam para quase todo projeto — e uma fonte do sistema custa zero byte.
  3. <img> aceita várias fontes com srcset e sizes: o navegador escolhe o arquivo certo para o tamanho da tela, e o celular deixa de baixar a imagem do desktop.
  4. Ícones em SVG inline no HTML economizam uma requisição inteira cada. Se você usa uma biblioteca de ícones inteira por causa de cinco ícones, esse é o corte mais fácil da lista.
🔥

🔥 Boss — Do zero ao ar, em três horas

projetodeploygithubacessibilidade

Este é o desafio que fecha a Unidade 3 e esta trilha. A regra é simples e desconfortável: um tema novo, do zero, publicado e auditado em três horas. Sem reaproveitar o código do site do evento nem o do seu projeto autoral — só o que está na sua cabeça e a documentação aberta.

Escolha um tema que você nunca usou (catálogo de plantas do Pantanal, agenda de quadras do bairro, mural de estágios, brechó, controle de pescarias, cardápio de restaurante) e construa um site de três páginas que exercite tudo o que esta trilha cobriu: HTML semântico, CSS com sistema de design e responsividade, JavaScript com listagem renderizada a partir de dados, busca e formulário validado. Marque o tempo do início ao fim.

Esta é a simulação mais honesta do que se pede em um teste técnico de estágio — e a prova, para você mesmo, de que o conteúdo entrou de verdade.

Critérios de pronto

  • Três páginas interligadas, HTML5 semântico, zero erros no validador do W3C nas três.
  • CSS externo com variáveis, layout em Grid ou Flexbox, responsivo em pelo menos duas larguras e estados :hover, :focus-visible e :active nos elementos interativos.
  • Uma listagem renderizada por JavaScript a partir de um array de objetos, com busca funcionando e estado vazio tratado.
  • Um formulário com validação em JavaScript, incluindo pelo menos uma regra por expressão regular e mensagens de erro acessíveis por campo.
  • Publicado no GitHub Pages, em repositório público próprio, com README.md, .gitignore e página 404.html.
  • Lighthouse no site publicado: acessibilidade ≥ 90 e SEO ≥ 90.
  • Um RELATO.md no repositório com o tempo total gasto, o que você conseguiu de memória, o que precisou consultar e o que faria diferente com mais uma hora.
Pistas
  1. Planeje quinze minutos antes de escrever a primeira linha: as três páginas, os campos do formulário e os cinco atributos dos objetos da listagem. Quem começa pelo CSS não termina.
  2. Comece pelo HTML das três páginas inteiro, depois o CSS, depois o JavaScript. Trocar de camada o tempo todo custa mais do que parece.
  3. Publique antes de terminar — no primeiro commit que renderiza alguma coisa. Um site incompleto no ar vale mais que um site completo que quebrou na publicação e não deu tempo de investigar.
  4. Guarde os últimos trinta minutos para auditar: validador, Lighthouse, teclado e o teste no celular. É onde os pontos aparecem.

Para ir além: publique o repositório com um tópico (topic) weblab no GitHub e compare o seu resultado com o de outras pessoas que fizeram o mesmo desafio.

🐛 Erros comuns

Sintoma Causa Solução
O site publicado aparece "cru", sem nenhum estilo O href do CSS começa com / e aponta para a raiz do github.io, não do repositório Use caminho relativo: href="css/estilo.css"
Failed to load resource: the server responded with a status of 404 () para o CSS Nome do arquivo com maiúscula diferente da referência no HTML Padronize tudo em minúsculas e corrija a referência
net::ERR_FILE_NOT_FOUND em uma imagem src com caminho de disco (C:/Users/...) em vez de relativo Mova a imagem para img/ e use src="img/arquivo.webp"
A URL do Pages devolve a página do mascote do GitHub Não existe index.html na raiz do repositório, ou o Pages foi apontado para a pasta errada Renomeie o arquivo de entrada para index.html e escolha / (root) em Settings → Pages
Your site is live não aparece e o repositório não tem a aba Pages ativa Repositório privado no plano gratuito Settings → General → Change visibility → Public
A alteração não aparece no site depois do git push Cache do navegador ou publicação ainda em andamento Recarregue com Ctrl+Shift+R e confira a aba Actions do repositório
fatal: not a git repository (or any of the parent directories): .git O terminal está numa pasta acima ou abaixo da pasta do projeto cd até a pasta que contém o index.html e rode git init ali
error: remote origin already exists. O git remote add origin foi executado duas vezes git remote set-url origin <url-correta>
Updates were rejected because the remote contains work that you do not have locally O repositório do GitHub foi criado com README, gerando um commit que você não tem git pull --rebase origin main e depois git push
Support for password authentication was removed ao dar push O GitHub não aceita mais senha de conta pela linha de comando Autentique pelo navegador quando o Git pedir, ou configure chave SSH
Os seus commits aparecem no GitHub sem foto e sem link para o seu perfil O user.email do Git é diferente do e-mail cadastrado na conta do GitHub git config --global user.email com o e-mail da conta; os commits novos já saem vinculados
Mixed Content: The page at 'https://…' was loaded over HTTPS, but requested an insecure element Alguma URL do site está escrita com http:// Troque por https:// ou por caminho relativo
Lighthouse com nota de desempenho abaixo de 40 Imagens em tamanho original, de vários megabytes cada Redimensione, converta para WebP e declare width/height
A nota de práticas recomendadas cai sem motivo aparente Auditoria rodada com extensões do navegador ativas Rode em janela anônima, com as extensões desabilitadas
A prévia do link no WhatsApp mostra só a URL, sem cartão Falta og:title, og:description ou og:image, ou a og:image está com caminho relativo Preencha as três com URL absoluta na imagem e teste em uma conversa consigo mesmo
Uma pasta com nome começando por _ não aparece no site O Jekyll do GitHub Pages ignora arquivos e pastas iniciados por sublinhado Crie um arquivo vazio .nojekyll na raiz do repositório

🏠 Para praticar depois da aula (1 h)

Parte 1 — Leitura (15 min). MILETTO & BERTAGNOLLI, Desenvolvimento de software II, capítulo sobre implantação. TERUEL, HTML 5 — Guia Prático, capítulo de publicação. Na documentação oficial do GitHub Pages, a página "Configuring a publishing source for your GitHub Pages site". Anote uma diferença entre o processo descrito no livro e o que você fez hoje.

Parte 2 — Produção (40 min). No seu projeto autoral:

  1. Publique o projeto autoral no GitHub Pages, com repositório público e README.md completo conforme o modelo da seção 9.
  2. Otimize todas as imagens e registre no README o peso total da página inicial antes e depois, com as duas capturas da aba Network.
  3. Rode o Lighthouse no site publicado (Mobile, janela anônima), registre as quatro notas no README e aplique as correções necessárias até atingir as metas deste material (veja o Marco 3, logo abaixo).
  4. Valide as páginas no validador do W3C até zero erros e crie a página 404.html personalizada.

Critério de pronto: o link público abre o site em qualquer máquina, em janela anônima; o console está limpo; o validador não aponta erros; o README traz o link do site, as quatro notas do Lighthouse e a comparação de peso.

Parte 3 — Discussão (5 min). Em texto próprio — ou no fórum da turma, se você está cursando esta trilha em grupo —: poste o link do seu projeto autoral publicado, com uma frase sobre o problema mais difícil da publicação. Se puder, peça a outra pessoa para abrir o link, rodar o Lighthouse e devolver um elogio específico e uma sugestão acionável.

Guarde no seu repositório: o link do site publicado, registrado no README.

✅ Checkpoint do projeto

  • [ ] Repositório público no GitHub, com .gitignore, .nojekyll e README.md completo na raiz.
  • [ ] Histórico com pelo menos cinco commits de mensagens descritivas, no imperativo.
  • [ ] Site publicado e acessível por URL pública, com HTTPS ativo, aberto em janela anônima.
  • [ ] index.html na raiz; nomes de arquivos e pastas em minúsculas, sem espaços e sem acentos.
  • [ ] Nenhum caminho interno absoluto, exceto os da 404.html.
  • [ ] Página 404.html personalizada, servida em qualquer caminho inexistente.
  • [ ] title e description próprios em cada página; metatags og: com og:image em URL absoluta; favicon aparecendo na aba.
  • [ ] Todas as imagens otimizadas (WebP ou SVG), com alt, width, height e loading="lazy" onde couber.
  • [ ] Zero erros no validador do W3C nas cinco páginas.
  • [ ] Console limpo e nenhum 404 na aba Network, nas cinco páginas do site publicado.
  • [ ] Lighthouse no site publicado: desempenho ≥ 80; acessibilidade, práticas recomendadas e SEO ≥ 90.
  • [ ] Site testado em celular real com dados móveis e navegável inteiramente por teclado.

🎓 Marco do projeto — Unidade 3

Escopo. Ao fim da Unidade 3, o seu projeto autoral — o mesmo dos Marcos 1 e 2, com as correções apontadas já aplicadas — precisa estar dinâmico e interativo com JavaScript (eventos, validação de formulários e consultas dinâmicas) e publicado na internet.

Requisitos.

# Requisito Onde foi estudado
1 JavaScript externo, carregado com defer, organizado em funções nomeadas Aula 10
2 Código estruturado nos blocos ESTADO → ELEMENTOS → DADOS → RENDERIZAÇÃO → EVENTOS → INICIALIZAÇÃO Aula 13
3 Dados do domínio em um array de objetos, em arquivo próprio Aula 12
4 Listagem renderizada a partir desse array, com textContent, e estado vazio tratado Aulas 12 e 13
5 Menu responsivo funcional em JavaScript, acessível por teclado, com aria-expanded Aula 13
6 Delegação de eventos em pelo menos uma lista dinâmica Aula 13
7 Busca com normalização de acentos e debounce, mais um filtro e uma ordenação Aula 14
8 Formulário validado em JavaScript, com pelo menos uma regra por expressão regular Aula 14
9 Mensagens de erro específicas por campo, com role="alert", aria-invalid e aria-describedby Aula 14
10 Uso de condicionais, laços e operadores com valores calculados na página Aulas 11 e 12
11 Zero erros no console durante o uso normal, nas páginas todas Aulas 13 a 15
12 Repositório público no GitHub, com .gitignore, README.md e ao menos cinco commits descritivos Aula 15
13 Site publicado, acessível por URL pública com HTTPS, e 404.html personalizada Aula 15
14 Imagens otimizadas, metatags de SEO e Open Graph em todas as páginas Aula 15
15 Zero erros no validador do W3C e Lighthouse com acessibilidade ≥ 90 no site publicado Aula 15

Frameworks e bibliotecas de JavaScript (jQuery, React, Vue e similares) não entram aqui — o objetivo deste marco é demonstrar domínio da linguagem pura. O site publicado é o que fecha o marco: um projeto que só existe na sua máquina ainda não chegou lá.

Checklist de qualidade.

  • Interatividade construída com eventos, delegação e manipulação do DOM, não com gambiarras.
  • Formulário validado com regras claras, regex corretas e mensagens acessíveis (não só cor).
  • Consultas dinâmicas (busca, filtro, ordenação) com estados vazios tratados com uma mensagem de verdade, não uma tela em branco.
  • Código JavaScript organizado e legível — você deveria conseguir reabri-lo em seis meses e entender na hora.
  • Publicação completa: repositório com histórico de commits que conta uma história, README que orienta um estranho e site no ar com HTTPS.
  • Qualidade auditada: validador W3C e Lighthouse sem alertas ignorados.
  • Coerência do projeto como um todo — o visitante não percebe onde uma aula terminou e a outra começou.

Como saber que está pronto.

  • Abra o site publicado em uma janela anônima, em outro computador se possível: se abrir e funcionar sem nada da sua máquina, está no ar de verdade.
  • Rode o Lighthouse (Mobile) no site publicado: desempenho ≥ 80; acessibilidade, boas práticas e SEO ≥ 90.
  • No Console, use o site inteiro (busca, filtro, formulário) e confirme zero linhas vermelhas.
  • Peça para alguém preencher o formulário tentando errar de propósito: as mensagens de erro precisam fazer sentido para quem não escreveu o código.
  • Use IA para tirar dúvida ou revisar uma abordagem — não para gerar a lógica inteira. Se você não souber explicar por que os blocos do seu app.js estão naquela ordem, ainda não é seu.

📚 Para aprofundar


🎓 Fim da trilha — para onde ir agora

Quinze aulas atrás você abriu um arquivo vazio e escreveu <!DOCTYPE html> sem saber direito por quê. Hoje você tem, no ar, um site de cinco páginas com estrutura semântica, um sistema de design em CSS, layout responsivo, animações que respeitam quem prefere menos movimento, listagens geradas por JavaScript, busca, filtro, ordenação e um formulário que valida CPF de verdade. E, o mais importante: você entende cada linha, porque escreveu cada uma.

Guarde o repositório. Ele já é peça de portfólio — o tipo de link que se coloca em um currículo de estágio e que sobrevive a qualquer pergunta de entrevista, porque você sabe explicar o que fez.

O Nível 2, a continuação direta

O Nível 2 — Desenvolvimento Web tem esta trilha como pré-requisito e continua exatamente de onde você parou. Lá, o CSS escrito à mão ganha a companhia de frameworks (Bootstrap e Tailwind) e de SVG; o js/dados.js com o array fixo vira um fetch que busca dados de uma API real, com async/await, estados de carregamento e tratamento de erro; as cinco páginas HTML viram uma SPA, uma aplicação de página única com navegação sem recarregamento. E, na terceira unidade, o JavaScript sai do navegador: com Node.js e Express você escreve o servidor que responde às requisições, adiciona login com conta Google e constrói um CRUD completo, com dados que sobrevivem ao fechar do navegador.

O projeto fio-condutor de lá é o Café Cerrado, uma cafeteria fictícia que percorre o mesmo caminho: site estático, depois dinâmico, depois full-stack.

A trilha Deploy, para quando publicar virar rotina

Hoje você publicou clicando em botões. A trilha Deploy & Ferramentas é transversal e transforma isso em processo: o Capítulo 02 leva o seu Git de seis comandos a branches, conflitos e pull requests revisados; o Capítulo 03 aprofunda a publicação de sites estáticos com prévia por branch, cache e redirecionamentos; o Capítulo 09 automatiza tudo com GitHub Actions, para que cada git push valide, audite e publique sozinho.

Por conta própria, até lá

  • Refaça o site do evento com outro tema, do zero. É o Boss desta aula, e é o exercício que mais consolida.
  • Contribua com um projeto de código aberto. Comece por uma correção de documentação em português: é uma contribuição real, e o fluxo de pull request é o mesmo dos projetos grandes.
  • Leia o código dos sites que você usa. Ctrl+U mostra o HTML de qualquer página. Você já entende boa parte dele — e a parte que não entende é a sua próxima lista de estudo.
  • Publique tudo o que fizer. Um projeto no ar vale mais que dez pastas na sua máquina. O hábito de terminar e publicar é, sozinho, uma vantagem competitiva.

Na próxima aula da sua trajetória — a Aula 01 do Nível 2 — Desenvolvimento Web — o Café Cerrado começa com um index.html vazio, exatamente como este começou. A diferença é que, desta vez, você já sabe o que fazer com ele. Bons deploys a todos.

🎯 Objetivos de aprendizagem📋 Pré-requisitos🗺️ Roteiro1. O caminho de um site até o navegador de outra pessoa1.1 Hospedagem: onde os arquivos ficam1.2 Domínio: o nome que as pessoas digitam1.3 HTTPS: o cadeado1.4 Onde a trilha Deploy aprofunda2. O que muda entre "funciona aqui" e "está no ar"3. Preparando o projeto para produção3.1 A estrutura final de pastas3.2 A limpeza obrigatória3.3 Depuração que não suja o console3.4 Imagens: o maior peso do seu site3.5 Metadados: o que o Google e o WhatsApp leem3.6 A página 4044. Auditoria: provar que está bom4.1 O validador do W3C4.2 Links quebrados e o console4.3 Lighthouse: a nota do seu site4.4 O teste que nenhuma ferramenta faz5. Git: o mínimo para publicar5.1 Configuração, uma vez na vida5.2 Os seis comandos do dia a dia5.3 Mensagens de commit que servem para alguma coisa5.4 .gitignore: o que nunca entra no repositório5.5 O .nojekyll6. GitHub Pages passo a passo6.1 Criar o repositório no GitHub6.2 Ativar o Pages6.3 A armadilha do subdiretório6.4 Republicar e o cache7. Netlify e Vercel: quando valem mais8. Domínio próprio, em quatro passos9. O README.md: a capa do projeto💻 Mão na massa — O site do evento no arPasso 1 — Limpeza e conferência localPasso 2 — O faviconPasso 3 — Metadados nas cinco páginasPasso 4 — A página 404Passo 5 — Otimizar as imagensPasso 6 — .gitignore, .nojekyll e README.mdPasso 7 — O repositório localPasso 8 — O repositório no GitHubPasso 9 — Ativar o GitHub PagesPasso 10 — Corrigir o que quebrou e auditarPasso 11 — A prova dos noveComo testar🧪 LaboratórioNível A — FixaçãoNível B — AplicaçãoNível C — Desafio🏆 Desafios⭐ O site que quebrou ao subir⭐⭐ Cem pontos em acessibilidade⭐⭐⭐ Meio megabyte, no máximo🔥 Boss — Do zero ao ar, em três horas🐛 Erros comuns🏠 Para praticar depois da aula (1 h)✅ Checkpoint do projeto🎓 Marco do projeto — Unidade 3📚 Para aprofundar🎓 Fim da trilha — para onde ir agoraO Nível 2, a continuação diretaA trilha Deploy, para quando publicar virar rotinaPor conta própria, até lá
WebLab — Laboratório de Desenvolvimento Web
Conteúdo sob CC BY 4.0; o gerador do site, sob licença MIT. Reuso livre com atribuição.
Início · Autoria e créditos · Banco de Desafios · Links úteis · Fontes no GitHub