<!-- ==========================================================================
AGENDAMENTO DE REUNIÃO — dia, horário e confirmação numa peça só
Prefixo: agd-
Requer: core (tokens, base, motion.css, motion.js) + estilo.css + script.js
Usa as primitivas .glass, .visually-hidden, .btn e .pill do core.
>>> ATENÇÃO: O ENVIO É SÓ FRONT-END. NADA SAI DESTA PÁGINA. <<<
O botão de confirmar valida os campos, trava o formulário e mostra a
mensagem de sucesso — e NÃO faz requisição nenhuma. Publicar assim
significa marcar reuniões que ninguém vê. O ponto exato de ligar o
endpoint está marcado com ENVIO no script.js. Mesmo aviso na ficha.
A IDEIA. O passo em que a pessoa escolhe QUANDO, e o formulário como
último passo — não o primeiro. Quem já escolheu o horário preenche o
nome; quem tem de sair da página para uma ferramenta de terceiro some.
O irmão conversao/cta-formulario-lead resolve o outro caso: a captura
genérica, em que o campo é a porta de entrada e a data se combina depois,
por e-mail. Os dois não se substituem — ver a ficha.
A DISPONIBILIDADE VIVE AQUI, NO HTML, e não no script. Cada dia é um
<input> com value (data ISO), data-agd-rotulo (o nome por extenso, para
o resumo e para o leitor de tela) e data-agd-vagas; dia sem vaga leva
`disabled` e continua na fila, porque a ausência informa. Cada horário é
um <input> cujo value já carrega a data inteira. O script só reage. É
isso que permite trocar este bloco pelo que um backend imprimir, sem
tocar em uma linha de JavaScript.
DATAS ILUSTRATIVAS: março de 2026, dez dias úteis (2 a 13), dois deles
sem vaga. São exemplo editorial. O script sabe recalcular a partir de
hoje, mas SÓ quando a seção declara data-agd-recalcular — fora isso o
que está escrito aqui é o que aparece na tela, e este arquivo continua
legível sozinho. Ver a ficha, seção Compliance.
SEM JAVASCRIPT tudo o que está aqui fica de pé: os dez dias, as oito
grades de horário (cada uma com o dia escrito em cima) e os quatro
campos, empilhados como um formulário comum de radios. Quem esconde o
que sobra é o SCRIPT — nunca o CSS. Por isso os passos 2 e 3 não nascem
com [hidden]: o que nasce assim é só o que não faz sentido sem script —
a trilha de passos, o botão Continuar e o resumo.
========================================================================== -->
<sectionclass="agd sec-dark sec-pad"id="agenda"data-agd>
<divclass="wrap">
<headerclass="agd-head">
<pclass="eyebrow"data-rv>Agenda</p>
<h2class="sec-title ttl agd-convite">Escolha quando será a primeira <em>conversa</em>.</h2>
<pclass="sec-lead agd-apoio"data-rvdata-d="2">Trinta minutos com um assessor, por vídeo ou telefone. Serve para entender o seu caso e explicar como a assessoria trabalha e como é remunerada. Não é recomendação de investimento, e não há custo nem compromisso.</p>
</header>
<divclass="glass agd-painel"data-rvdata-d="2">
<!-- Trilha decorativa: aria-hidden porque a informação de verdade vai na
região aria-live logo abaixo, que anuncia "Passo 2 de 3" junto do que
já foi escolhido. Nasce [hidden] — sem script não há passo nenhum. -->
<divclass="agd-barra"hidden>
<olclass="agd-trilha"aria-hidden="true">
<liclass="agd-etapa is-atual"><spanclass="agd-etapa-n">1</span><spanclass="agd-etapa-t">Dia</span></li>
<liclass="agd-etapa"><spanclass="agd-etapa-n">2</span><spanclass="agd-etapa-t">Horário</span></li>
<liclass="agd-etapa"><spanclass="agd-etapa-n">3</span><spanclass="agd-etapa-t">Confirmação</span></li>
</ol>
<buttonclass="agd-voltar"type="button"hidden>Voltar</button>
</div>
<pclass="agd-anuncio visually-hidden"role="status"aria-live="polite"></p>
<divclass="agd-palco">
<formclass="agd-form"novalidate>
<!-- ============================================ PASSO 1 — o dia -->
<fieldsetclass="agd-passo">
<legendclass="agd-legenda agd-foco"tabindex="-1">Escolha o dia</legend>
<pclass="agd-dica">Dez dias úteis. O dia sem vaga continua na fila, apagado — é informação, não erro.</p>
<!-- CONTEÚDO ILUSTRATIVO: as datas e o número de vagas são exemplo.
Trocar pela disponibilidade real da agenda do cliente. -->
<divclass="agd-dias">
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-02"data-agd-rotulo="Segunda, 2 de março"data-agd-vagas="4"aria-label="Segunda, 2 de março, 4 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Seg</span>
<spanclass="agd-dia-num"aria-hidden="true">02</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">4 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-03"data-agd-rotulo="Terça, 3 de março"data-agd-vagas="5"aria-label="Terça, 3 de março, 5 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Ter</span>
<spanclass="agd-dia-num"aria-hidden="true">03</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">5 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<!-- Dia sem vaga: `disabled` no input tira o dia da ordem de
tabulação E da navegação por seta, que é o comportamento
nativo do grupo de radio. Ele não some da fila. -->
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-04"data-agd-rotulo="Quarta, 4 de março"data-agd-vagas="0"disabledaria-label="Quarta, 4 de março, sem horário livre">
<spanclass="agd-dia-sem"aria-hidden="true">Qua</span>
<spanclass="agd-dia-num"aria-hidden="true">04</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">Sem vaga</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-05"data-agd-rotulo="Quinta, 5 de março"data-agd-vagas="3"aria-label="Quinta, 5 de março, 3 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Qui</span>
<spanclass="agd-dia-num"aria-hidden="true">05</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">3 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-06"data-agd-rotulo="Sexta, 6 de março"data-agd-vagas="5"aria-label="Sexta, 6 de março, 5 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Sex</span>
<spanclass="agd-dia-num"aria-hidden="true">06</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">5 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-09"data-agd-rotulo="Segunda, 9 de março"data-agd-vagas="6"aria-label="Segunda, 9 de março, 6 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Seg</span>
<spanclass="agd-dia-num"aria-hidden="true">09</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">6 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-10"data-agd-rotulo="Terça, 10 de março"data-agd-vagas="4"aria-label="Terça, 10 de março, 4 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Ter</span>
<spanclass="agd-dia-num"aria-hidden="true">10</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">4 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-11"data-agd-rotulo="Quarta, 11 de março"data-agd-vagas="0"disabledaria-label="Quarta, 11 de março, sem horário livre">
<spanclass="agd-dia-sem"aria-hidden="true">Qua</span>
<spanclass="agd-dia-num"aria-hidden="true">11</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">Sem vaga</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-12"data-agd-rotulo="Quinta, 12 de março"data-agd-vagas="5"aria-label="Quinta, 12 de março, 5 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Qui</span>
<spanclass="agd-dia-num"aria-hidden="true">12</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">5 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
<labelclass="agd-dia">
<inputclass="agd-dia-in"type="radio"name="agd-dia"value="2026-03-13"data-agd-rotulo="Sexta, 13 de março"data-agd-vagas="3"aria-label="Sexta, 13 de março, 3 horários livres">
<spanclass="agd-dia-sem"aria-hidden="true">Sex</span>
<spanclass="agd-dia-num"aria-hidden="true">13</span>
<spanclass="agd-dia-mes"aria-hidden="true">mar</span>
<spanclass="agd-dia-vagas"aria-hidden="true">3 horários</span>
<spanclass="agd-dia-marca"aria-hidden="true"></span>
</label>
</div>
</fieldset>
<!-- ======================================== PASSO 2 — o horário -->
<fieldsetclass="agd-passo">
<legendclass="agd-legenda agd-foco"tabindex="-1">Escolha o horário</legend>
<!-- O FUSO FICA DECLARADO NA PEÇA, e não numa nota de rodapé:
agendamento sem fuso é armadilha — a pessoa em Lisboa marca
15h30 e aparece três horas depois. Trocar pelo fuso real de
quem atende. -->
<pclass="agd-fuso"><spanclass="pill pill-accent">GMT-3</span> Horários no fuso de Brasília. Cada conversa dura 30 minutos.</p>
<!-- Uma grade por dia. O par é feito por data: data-agd-dia aqui
bate com o value do <input> do dia. O script mostra a grade do
dia escolhido e esconde as outras; sem script, todas ficam de
pé, cada uma com o dia escrito em cima — e o value de cada
horário já carrega a data inteira, então o par dia+hora nunca
fica ambíguo, nem num envio de formulário comum.
CONTEÚDO ILUSTRATIVO. -->
<divclass="agd-grade"data-agd-dia="2026-03-02"role="group"aria-label="Horários de segunda, 2 de março">
<pclass="agd-grade-dia">Segunda, 2 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-02T09:00"data-agd-hora="09h00"><spanclass="agd-hora-txt">09h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-02T11:00"data-agd-hora="11h00"><spanclass="agd-hora-txt">11h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-02T14:00"data-agd-hora="14h00"><spanclass="agd-hora-txt">14h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-02T16:30"data-agd-hora="16h30"><spanclass="agd-hora-txt">16h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
<divclass="agd-grade"data-agd-dia="2026-03-03"role="group"aria-label="Horários de terça, 3 de março">
<pclass="agd-grade-dia">Terça, 3 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-03T09:00"data-agd-hora="09h00"><spanclass="agd-hora-txt">09h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-03T10:00"data-agd-hora="10h00"><spanclass="agd-hora-txt">10h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-03T14:00"data-agd-hora="14h00"><spanclass="agd-hora-txt">14h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-03T15:30"data-agd-hora="15h30"><spanclass="agd-hora-txt">15h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-03T17:30"data-agd-hora="17h30"><spanclass="agd-hora-txt">17h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
<divclass="agd-grade"data-agd-dia="2026-03-05"role="group"aria-label="Horários de quinta, 5 de março">
<pclass="agd-grade-dia">Quinta, 5 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-05T10:00"data-agd-hora="10h00"><spanclass="agd-hora-txt">10h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-05T15:30"data-agd-hora="15h30"><spanclass="agd-hora-txt">15h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-05T17:30"data-agd-hora="17h30"><spanclass="agd-hora-txt">17h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
<divclass="agd-grade"data-agd-dia="2026-03-06"role="group"aria-label="Horários de sexta, 6 de março">
<pclass="agd-grade-dia">Sexta, 6 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-06T09:00"data-agd-hora="09h00"><spanclass="agd-hora-txt">09h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-06T11:00"data-agd-hora="11h00"><spanclass="agd-hora-txt">11h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-06T14:00"data-agd-hora="14h00"><spanclass="agd-hora-txt">14h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-06T16:30"data-agd-hora="16h30"><spanclass="agd-hora-txt">16h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-06T17:30"data-agd-hora="17h30"><spanclass="agd-hora-txt">17h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
<divclass="agd-grade"data-agd-dia="2026-03-09"role="group"aria-label="Horários de segunda, 9 de março">
<pclass="agd-grade-dia">Segunda, 9 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-09T09:00"data-agd-hora="09h00"><spanclass="agd-hora-txt">09h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-09T10:00"data-agd-hora="10h00"><spanclass="agd-hora-txt">10h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-09T11:00"data-agd-hora="11h00"><spanclass="agd-hora-txt">11h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-09T14:00"data-agd-hora="14h00"><spanclass="agd-hora-txt">14h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-09T15:30"data-agd-hora="15h30"><spanclass="agd-hora-txt">15h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-09T16:30"data-agd-hora="16h30"><spanclass="agd-hora-txt">16h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
<divclass="agd-grade"data-agd-dia="2026-03-10"role="group"aria-label="Horários de terça, 10 de março">
<pclass="agd-grade-dia">Terça, 10 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-10T09:00"data-agd-hora="09h00"><spanclass="agd-hora-txt">09h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-10T14:00"data-agd-hora="14h00"><spanclass="agd-hora-txt">14h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-10T15:30"data-agd-hora="15h30"><spanclass="agd-hora-txt">15h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-10T17:30"data-agd-hora="17h30"><spanclass="agd-hora-txt">17h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
<divclass="agd-grade"data-agd-dia="2026-03-12"role="group"aria-label="Horários de quinta, 12 de março">
<pclass="agd-grade-dia">Quinta, 12 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-12T10:00"data-agd-hora="10h00"><spanclass="agd-hora-txt">10h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-12T11:00"data-agd-hora="11h00"><spanclass="agd-hora-txt">11h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-12T14:00"data-agd-hora="14h00"><spanclass="agd-hora-txt">14h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-12T15:30"data-agd-hora="15h30"><spanclass="agd-hora-txt">15h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-12T16:30"data-agd-hora="16h30"><spanclass="agd-hora-txt">16h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
<divclass="agd-grade"data-agd-dia="2026-03-13"role="group"aria-label="Horários de sexta, 13 de março">
<pclass="agd-grade-dia">Sexta, 13 de março</p>
<divclass="agd-horas">
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-13T09:00"data-agd-hora="09h00"><spanclass="agd-hora-txt">09h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-13T11:00"data-agd-hora="11h00"><spanclass="agd-hora-txt">11h00</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
<labelclass="agd-hora"><inputclass="agd-hora-in"type="radio"name="agd-hora"value="2026-03-13T16:30"data-agd-hora="16h30"><spanclass="agd-hora-txt">16h30</span><spanclass="agd-hora-marca"aria-hidden="true"></span></label>
</div>
</div>
</fieldset>
<!-- ==================================== PASSO 3 — a confirmação -->
<fieldsetclass="agd-passo">
<legendclass="agd-legenda agd-foco"tabindex="-1">Confirme os seus dados</legend>
<!-- O resumo nasce [hidden] porque sem script não há o que
resumir: um quadro vazio com "—" seria pior que a ausência.
Os dois botões voltam ao passo que fez a escolha, e é isso que
torna o resumo editável sem tirar a pessoa da peça. -->
<divclass="agd-resumo"hidden>
<divclass="agd-resumo-item">
<pclass="agd-resumo-rot">Dia</p>
<pclass="agd-resumo-val"data-agd-val-dia>—</p>
<buttonclass="agd-editar"type="button"data-agd-ir="0">Editar<spanclass="visually-hidden"> o dia escolhido</span></button>
</div>
<divclass="agd-resumo-item">
<pclass="agd-resumo-rot">Horário</p>
<pclass="agd-resumo-val"data-agd-val-hora>—</p>
<buttonclass="agd-editar"type="button"data-agd-ir="1">Editar<spanclass="visually-hidden"> o horário escolhido</span></button>
</div>
<pclass="agd-resumo-fuso">Fuso de Brasília (GMT-3) · 30 minutos</p>
</div>
<divclass="field agd-campos">
<labelclass="field agd-campo is-full">
<spanclass="agd-rot">Nome completo</span>
<inputclass="agd-input"type="text"name="nome"requiredautocomplete="name"placeholder="Como devemos chamar você">
</label>
<labelclass="field agd-campo">
<spanclass="agd-rot">E-mail</span>
<inputclass="agd-input"type="email"name="email"requiredautocomplete="email"placeholder="voce@email.com">
</label>
<labelclass="field agd-campo">
<spanclass="agd-rot">Telefone</span>
<inputclass="agd-input"type="tel"name="telefone"requiredautocomplete="tel"placeholder="(00) 00000-0000">
</label>
<labelclass="field agd-campo is-full">
<spanclass="agd-rot">Assunto <spanclass="agd-rot-op">opcional</span></span>
<inputclass="agd-input"type="text"name="assunto"placeholder="Em uma linha, o que você quer tratar">
</label>
</div>
<divclass="agd-envio">
<buttonclass="btn btn-primary agd-confirmar"type="submit">
Confirmar reunião
<svgwidth="13"height="13"viewBox="0 0 14 14"fill="none"stroke="currentColor"stroke-width="1.5"stroke-linecap="round"stroke-linejoin="round"aria-hidden="true"><pathd="M2 7H12M8 3L12 7L8 11"/></svg>
</button>
<!-- A ressalva fica ONDE A PESSOA ENTREGA O DADO, não em letra
miúda no rodapé da página. Trocar o link pela política de
privacidade real do cliente antes de publicar. -->
<pclass="nota agd-nota">Usamos os seus dados só para confirmar esta conversa. Nada fica gravado neste navegador. A reunião é uma conversa — não é recomendação de investimento nem análise da sua carteira, que dependem de contrato.</p>
</div>
<!-- >>> ISTO NÃO ENVIA NADA. <<< A mensagem abaixo aparece quando o
script trava o formulário, e é só isso que acontece: não há
requisição. Ligue o endpoint no ponto marcado ENVIO no
script.js antes de publicar. -->
<pclass="agd-ok"role="status"tabindex="-1"hidden></p>
</fieldset>
</form>
</div>
<divclass="agd-acoes"hidden>
<buttonclass="btn btn-primary agd-continuar"type="button"disabled>Continuar</button>
</div>
<!-- Estado honesto sem JavaScript: os dez dias, as oito grades e os
quatro campos continuam de pé como um formulário comum, com um
botão que leva à conversa por outro caminho. O script esconde este
bloco na inicialização. -->
<divclass="agd-sem-js">
<pclass="nota agd-nota">Sem JavaScript o agendamento vira um formulário comum: escolha o dia, o horário e preencha os campos acima. Se preferir, fale direto com a equipe.</p>
<aclass="btn btn-ghost agd-alternativa"href="#contato">Falar com um assessor</a>
</div>
</div>
</div>
</section>
estilo.css · 577 linhas · 23,6 KB
/* ==========================================================================
AGENDAMENTO DE REUNIÃO — prefixo agd-
Painel de vidro com três passos: fila de dias, grade de horários e um
formulário curto de confirmação. Sem imagem — o que ocupa a dobra é a
agenda.
Tokens usados:
--accent dia e horário marcados, filete aceso, passo corrente
--display face dos números do dia e do horário
--sans face do texto e dos campos
--ease --dur curva e tempo do projeto
--fg --fg-2 tinta dos números e do texto
--fg-muted rótulos, vagas, ressalva
--rule --rule-2 contorno em repouso e régua dos campos
--radius-media canto do painel, dos dias e dos horários (0 em flat)
--radius-pill só via .btn e .pill do core
--sec-gap respiro derivado, nunca clamp solto
Primitivas do core consumidas, e por isso NÃO recopiadas: .glass (o
painel), .visually-hidden (a região aria-live e os complementos de rótulo
dos botões Editar), .btn/.btn-primary/.btn-ghost, .pill/.pill-accent.
A primitiva .field é a exceção: o campo daqui tem a mesma medida dela, mas
não a veste. O motivo, medido, está no bloco CAMPOS.
A REGRA QUE ORGANIZA O ARQUIVO: nada aqui esconde passo, grade ou campo.
Quem esconde é o script, com [hidden]. Sem JavaScript o CSS entrega os dez
dias, as oito grades e os quatro campos empilhados — que é o que se quer.
========================================================================== */.agd{
/* Largura mínima do dia na fila, e é ela sozinha que decide se a fila rola.
Um valor só para todas as larguras, e de propósito: com minmax(92px,1fr)
as dez colunas crescem e dividem o painel enquanto couberem (acima de
~1150px de janela), e viram trilho horizontal assim que não couberem —
sem trocar o display, sem segunda árvore de markup, sem breakpoint.
92px não é gosto: é o que "4 horários" pede em 10,5px com tracking, mais
os 6px de respiro de cada lado. Abaixo disso a linha de vagas vaza da
coluna e encosta na do dia seguinte — o texto continua sendo o piso, e
encurtá-lo é o que permite baixar este número. */--agd-dia-min:92px;
/* Largura mínima do bloco de horário. Em px porque o alvo é o dedo: abaixo
de ~100px o bloco fica menor que a área de toque confortável. */--agd-hora-min:104px;
/* Piso do palco. Cobre os passos 1 e 2, que são os de troca frequente e de
altura parecida (fila de dias ~104px; grade de até duas linhas). O passo
3 é um formulário e passa disso de propósito: um piso que coubesse nele
abriria 200px de vão morto embaixo da fila de dias.
Diferente do irmão conversao/diagnostico-perfil, aqui o piso NÃO é medido
pelo script. Lá as cinco telas são do mesmo tipo e só variam com a quebra
de linha do enunciado — medir é a única saída. Aqui os três passos são de
tipos diferentes, e igualá-los seria esconder essa diferença. */--agd-palco-min:clamp(292px,32vh,360px);
/* Saída do passo que sai. O script lê o MESMO número (SAIDA em script.js):
mudar aqui pede mudar lá, senão o passo some antes ou depois da hora. */--agd-saida:.26s;
/* Atraso entre blocos na entrada de um passo. 34ms x 10 dias = .34s, que é
quanto dura a leitura do primeiro. Acima disso a fila parece lenta. */--agd-bloco-passo:34ms;
}
/* [hidden] vem da folha do navegador com especificidade (0,1,0), e QUALQUER
regra de autor que declare display vence — e este componente declara grid e
flex em oito lugares. Sem esta linha o script põe [hidden] nos passos 2 e 3,
o atributo aparece no inspetor, e eles continuam na tela. Uma regra só, no
topo, cobre o componente inteiro. Mesma pegadinha documentada no
.hgal-nav[hidden] da midia/galeria-scroll-horizontal. */.agd [hidden]{display:none}
/* ==========================================================================
CABEÇALHO
========================================================================== */.agd-convite{margin-top:16px}
.agd-apoio{max-width:56ch}
/* ==========================================================================
PAINEL
O bloco de reveal do core. O respiro que o separa do cabeçalho sai de
--sec-gap, como o resto do ritmo vertical (§4) — nunca um clamp solto.
========================================================================== */.agd-painel{
position:relative;
margin-top:calc(var(--sec-gap) * .34);
padding:clamp(22px,3.2vw,38px);
border-radius:var(--radius-media);
--glass-blur:26px;
}
/* ==========================================================================
TRILHA DE PASSOS
Decorativa: [aria-hidden] no markup, e a informação de verdade na região
aria-live. Nasce [hidden] e é o script que a mostra.
========================================================================== */.agd-barra{
display:flex;align-items:center;justify-content:space-between;gap:16px;
padding-bottom:16px;
border-bottom:1px solid var(--rule);
}
/* A trilha é uma ordem de verdade, então o elemento é <ol>. O reset do core
zera `ul` e `ol` juntos — aqui não sobra nada a desfazer. */.agd-trilha{
display:flex;align-items:center;flex-wrap:wrap;gap:10px;
}
.agd-etapa{
display:inline-flex;align-items:center;gap:8px;
font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;
color:var(--fg-muted);
transition:color .3svar(--ease);
}
/* O traço entre um passo e o seguinte. Vive no ::before do próximo, e não
numa caixa própria, para a fila poder quebrar em duas linhas no celular
sem deixar um traço órfão no fim da primeira. */.agd-etapa + .agd-etapa::before{
content:"";width:22px;height:1px;background:var(--rule-2);
}
.agd-etapa-n{
display:grid;place-items:center;
width:22px;height:22px;
border:1px solid var(--rule-2);border-radius:var(--radius-pill);
font-family:var(--display);font-weight:700;font-size:11px;letter-spacing:0;
transition:color .3svar(--ease),border-color .3svar(--ease);
}
/* Passo corrente acende; passo já cumprido volta ao texto de apoio. Progresso
em texto, nunca barra que enche: gamificação é o registro errado para uma
assessoria. */.agd-etapa.is-atual{color:var(--accent)}
.agd-etapa.is-atual .agd-etapa-n{color:var(--accent);border-color:var(--accent)}
.agd-etapa.is-feita{color:var(--fg-2)}
.agd-etapa.is-feita .agd-etapa-n{border-color:var(--rule-2)}
/* "Voltar" nunca é disabled: no primeiro passo ele simplesmente não existe.
Botão apagado que não faz nada é pior que botão ausente. */.agd-voltar{
font-family:var(--sans);font-size:11.5px;letter-spacing:.18em;
text-transform:uppercase;color:var(--fg-muted);
background:none;border:0;padding:0;
transition:color .3svar(--ease);
}
.agd-voltar:hover{color:var(--accent)}
/* A região aria-live (.agd-anuncio) não tem regra própria: leva a primitiva
.visually-hidden do core, que existe exatamente para isto — texto que o
leitor de tela recebe e o olho não vê, com clip-path em vez de
display:none, porque display:none tira o elemento da árvore de
acessibilidade e o anúncio nunca sairia. *//* ==========================================================================
PALCO E PASSOS
========================================================================== */.agd-palco{
position:relative;
min-height:var(--agd-palco-min);
padding-top:clamp(22px,3vh,34px);
}
/* fieldset traz border, padding e margin da folha do navegador, e um
min-inline-size:min-content que estoura a coluna em tela estreita quando
um texto longo não quebra. Os quatro zeros são obrigatórios. */.agd-passo{border:0;margin:0;padding:0;min-inline-size:0}
/* legend recebe o foco na troca de passo (tabindex="-1" no markup). Sem
display:block ela é tratada como caixa especial da borda do fieldset e se
recusa a virar bloco. width:fit-content não é capricho: é o que faz o anel
de foco do core abraçar o TÍTULO — em largura cheia o anel sairia com a
coluna inteira em volta de três palavras. */.agd-legenda{
display:block;float:none;width:fit-content;max-width:100%;padding:0;margin:0;
font-family:var(--display);font-weight:700;letter-spacing:-.02em;
font-size:clamp(21px,2.2vw,28px);line-height:1.2;color:var(--fg);
}
.agd-dica,
.agd-fuso{
margin-top:10px;max-width:64ch;
font-size:13.5px;line-height:1.6;color:var(--fg-muted);
}
/* O fuso é uma declaração, não uma legenda: a pílula à esquerda existe para
ele ser lido antes da grade, e não depois de a pessoa já ter escolhido. */.agd-fuso{display:flex;align-items:center;flex-wrap:wrap;gap:10px}
/* ==========================================================================
PASSO 1 — A FILA DE DIAS
========================================================================== */.agd-dias{
display:grid;grid-auto-flow:column;
grid-auto-columns:minmax(var(--agd-dia-min),1fr);
gap:8px;
margin-top:clamp(20px,3vh,30px);
/* O trilho existe SEMPRE, e é a --agd-dia-min que decide se ele rola. Em
janela larga as dez colunas cabem e o overflow nunca dispara; em janela
estreita o piso estoura a largura e a fila vira trilho. Um só bloco de
markup para os dois casos. overflow-x:auto no CONTÊINER é também o que
impede a página inteira de estourar na horizontal — que é o erro que o
§4 do manual descreve com margin negativa e 100vw. */overflow-x:auto;
overscroll-behavior-x:contain;
scroll-snap-type:x proximity;
/* O anel de foco do core sai 3px para fora do dia. Sem este respiro ele
seria cortado pela própria caixa de rolagem, justamente em quem depende
dele para saber onde está. */padding:6px;
}
.agd-dia{
position:relative;
display:grid;justify-items:center;align-content:center;gap:3px;
padding:14px6px12px;
scroll-snap-align:start;
}
/* O RADIO É DE VERDADE, e cobre o dia inteiro em vez de ficar escondido num
canto de 1px. Duas coisas dependem disso: o :focus-visible do core cai
exatamente em volta do dia, e o clique em qualquer ponto chega ao input sem
depender de for/id — que colidiria se o componente aparecesse duas vezes na
mesma página. appearance:none, e não opacity:0: com opacity:0 o anel de
foco some junto com o widget. */.agd-dia-in,
.agd-hora-in{
-webkit-appearance:none;appearance:none;
position:absolute;inset:0;width:100%;height:100%;
margin:0;padding:0;border:0;background:none;
cursor:pointer;
}
.agd-dia-in:disabled{cursor:default}
.agd-dia-sem{
font-family:var(--sans);font-size:10.5px;letter-spacing:.16em;
text-transform:uppercase;color:var(--fg-muted);
transition:color .3svar(--ease);
}
.agd-dia-num{
font-family:var(--display);font-weight:700;
font-size:clamp(22px,2.2vw,28px);line-height:1;letter-spacing:-.02em;
color:var(--fg);
transition:color .3svar(--ease);
}
.agd-dia-mes{
font-family:var(--sans);font-size:11px;letter-spacing:.1em;
color:var(--fg-2);
transition:color .3svar(--ease);
}
/* O número de vagas é o que faz a fila informar em vez de só oferecer: dez
dias iguais escondem que dois estão cheios. */.agd-dia-vagas{
margin-top:4px;
font-family:var(--sans);font-size:10.5px;letter-spacing:.04em;
color:var(--fg-muted);white-space:nowrap;
transition:color .3svar(--ease);
}
/* O contorno fica numa caixa própria, esticada sobre o dia, e não no <label>:
é ela que permite reagir ao :disabled e ao :checked do input com um seletor
de irmão, sem :has e sem classe posta por script. pointer-events:none para
não roubar o clique do input que está por baixo. */.agd-dia-marca,
.agd-hora-marca{
position:absolute;inset:0;pointer-events:none;
border:1px solid var(--rule);
border-radius:var(--radius-media);
transition:border-color .32svar(--ease),background .32svar(--ease);
}
.agd-dia-in:hover ~ .agd-dia-marca{border-color:var(--rule-2)}
.agd-dia-in:hover ~ .agd-dia-num{color:var(--accent)}
.agd-dia-in:checked ~ .agd-dia-marca{
border-color:var(--accent);
background:color-mix(in srgb,var(--accent) 9%,transparent);
}
.agd-dia-in:checked ~ .agd-dia-num,
.agd-dia-in:checked ~ .agd-dia-sem,
.agd-dia-in:checked ~ .agd-dia-mes{color:var(--accent)}
/* Dia sem vaga: apagado e de contorno tracejado, mas PRESENTE. Some da ordem
de tabulação e da navegação por seta pelo `disabled` do próprio input, que
é comportamento nativo do grupo de radio — nada disso é feito por script. */.agd-dia-in:disabled ~ .agd-dia-marca{border-style:dashed;border-color:var(--rule)}
.agd-dia-in:disabled ~ .agd-dia-num{color:var(--fg-muted);opacity:.6}
.agd-dia-in:disabled ~ .agd-dia-sem,
.agd-dia-in:disabled ~ .agd-dia-mes,
.agd-dia-in:disabled ~ .agd-dia-vagas{opacity:.6}
.agd-dia-in:disabled:hover ~ .agd-dia-marca{border-color:var(--rule)}
.agd-dia-in:disabled:hover ~ .agd-dia-num{color:var(--fg-muted)}
/* ==========================================================================
PASSO 2 — A GRADE DE HORÁRIOS
========================================================================== */.agd-grade{margin-top:clamp(20px,3vh,30px)}
/* O dia escrito em cima da grade não é redundância com o passo 1: com script,
é a confirmação do que foi escolhido; sem script, é o que separa oito
grades soltas de oito grades identificadas. */.agd-grade-dia{
font-family:var(--sans);font-size:11.5px;letter-spacing:.18em;
text-transform:uppercase;color:var(--accent);
}
.agd-horas{
display:grid;
grid-template-columns:repeat(auto-fill,minmax(var(--agd-hora-min),1fr));
gap:8px;
margin-top:12px;
}
.agd-hora{
position:relative;
display:grid;place-items:center;
padding:15px8px;
}
.agd-hora-txt{
font-family:var(--display);font-weight:700;
font-size:15px;letter-spacing:.02em;color:var(--fg-2);
transition:color .3svar(--ease);
}
.agd-hora-in:hover ~ .agd-hora-marca{border-color:var(--rule-2)}
.agd-hora-in:hover ~ .agd-hora-txt{color:var(--fg)}
.agd-hora-in:checked ~ .agd-hora-marca{
border-color:var(--accent);
background:color-mix(in srgb,var(--accent) 9%,transparent);
}
.agd-hora-in:checked ~ .agd-hora-txt{color:var(--accent)}
/* ==========================================================================
PASSO 3 — RESUMO E CAMPOS
========================================================================== */.agd-resumo{
display:grid;grid-template-columns:1fr1fr;gap:024px;
margin-top:clamp(20px,3vh,30px);
padding:18px20px;
border:1px solid var(--rule-2);
border-radius:var(--radius-media);
}
.agd-resumo-item{display:flex;align-items:baseline;flex-wrap:wrap;gap:012px}
.agd-resumo-rot{
flex:00100%;
font-family:var(--sans);font-size:10.5px;letter-spacing:.16em;
text-transform:uppercase;color:var(--fg-muted);
}
.agd-resumo-val{
font-family:var(--display);font-weight:700;
font-size:clamp(16px,1.5vw,19px);letter-spacing:-.01em;color:var(--fg);
margin-top:6px;
}
/* "Editar" é texto, e não um segundo botão de pílula: dois .btn-primary na
mesma caixa disputariam com o Confirmar, que é a ação da tela. */.agd-editar{
font-family:var(--sans);font-size:11px;letter-spacing:.14em;
text-transform:uppercase;color:var(--fg-muted);
background:none;border:0;padding:0;
border-bottom:1px solid var(--rule-2);
transition:color .3svar(--ease),border-color .3svar(--ease);
}
.agd-editar:hover{color:var(--accent);border-color:var(--accent)}
.agd-resumo-fuso{
grid-column:1/-1;margin-top:14px;padding-top:12px;
border-top:1px solid var(--rule);
font-size:12px;color:var(--fg-muted);
}
/* gap 0 na vertical: o respiro entre linhas vem do padding do próprio campo,
senão a régua de baixo de um campo fica longe demais do seguinte. */.agd-campos{
display:grid;grid-template-columns:1fr1fr;gap:026px;
margin-top:clamp(18px,2.6vh,26px);
}
/* O campo veste a primitiva `.field` do core: régua embaixo em vez de caixa,
que acende no foco. Aqui sobra só o que é deste componente.
O rótulo continua local. O <label> EMBRULHA o campo em vez de usar for/id
— pelo mesmo motivo dos radios, id não sobrevive a dois componentes na
mesma página —, então o rótulo é um <span class="agd-rot">, e o seletor do
core (`.field label`, descendente) não casaria com nada.
O anel de foco do input vem do `::focus-visible` do core e NÃO é zerado por
ninguém: a régua de 1px sozinha é indicador fraco demais para quem navega
por teclado, e as duas coisas convivem. Houve um dia, entre a criação da
primitiva e este comentário, em que `.field input` zerava o `outline` e
vestir a primitiva mataria o anel — está no histórico da ficha. */.agd-campo.is-full{grid-column:1/-1}
.agd-rot{
display:block;margin-bottom:8px;
font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
color:var(--fg-muted);
}
.agd-rot-op{letter-spacing:.06em;text-transform:none;opacity:.7}
.agd-input{
width:100%;background:none;border:none;padding:0;
font-family:var(--sans);font-weight:300;font-size:15px;line-height:1.4;
color:var(--fg);
}
.agd-input::placeholder{color:var(--fg-muted);opacity:.7}
/* Campo vazio marcado pela validação do script. Some no primeiro toque —
ver script.js. */.agd-campo.is-erro{border-color:var(--accent-2)}
.agd-envio{
display:flex;align-items:center;flex-wrap:wrap;gap:18px;
margin-top:clamp(22px,3vh,32px);
}
/* A ressalva de compliance fica onde a pessoa ENTREGA O DADO, não em letra
miúda no rodapé da página. *//* A tipografia vem da primitiva .nota do core; daqui sai só a posição — e os
dois parafusos. 11,5px e 62ch porque a ressalva DIVIDE a barra de ações com
o botão de enviar: no corpo padrão ela empurraria o botão para a linha de
baixo. A entrelinha aberta compensa o corpo menor. */.agd-nota{
flex:11320px;
--nota-size:11.5px;
--nota-max:62ch;
line-height:1.7;
}
.agd-ok{
margin-top:20px;padding:16px18px;
border:1px solid var(--accent);border-radius:var(--radius-media);
background:color-mix(in srgb,var(--accent) 10%,transparent);
font-size:14px;line-height:1.6;color:var(--accent);
}
/* ==========================================================================
AÇÕES
O botão nasce disabled e só libera quando o passo está respondido. Ele
existe SEMPRE, e não só em movimento reduzido: o avanço automático é
cortesia para quem clica, nunca a única saída. Ver o gotcha da ficha.
========================================================================== */.agd-acoes{margin-top:clamp(24px,3.4vh,36px)}
.agd .btn:disabled{
opacity:.34;cursor:default;
background:none;color:var(--fg-muted);border-color:var(--rule);
}
.agd .btn:disabled:hover{
background:none;color:var(--fg-muted);border-color:var(--rule);
backdrop-filter:none;-webkit-backdrop-filter:none;
}
/* ==========================================================================
SEM JAVASCRIPT
Bloco visível por padrão; o script o esconde na inicialização. Nada aqui
depende de @media nem de :has — o estado honesto é o estado de partida.
========================================================================== */.agd-sem-js{
margin-top:clamp(24px,3.4vh,36px);padding-top:20px;
border-top:1px solid var(--rule);
display:flex;flex-direction:column;align-items:flex-start;gap:18px;
}
/* ==========================================================================
A TROCA DE PASSO — o movimento da peça
Só existe com script: as regras estão todas sob .agd.is-live, classe que o
script põe na seção ao iniciar. Sem ela o CSS não esconde nem move nada.
Não dá para usar data-rv aqui, e a razão é estrutural: o data-rv do core é
observer de ENTRADA NA TELA — dispara uma vez, desobserva e nunca volta.
Uma peça que troca de conteúdo no mesmo lugar precisa de transição por
classe. O que o core continua fazendo é o que ele sabe: revelar o painel
quando a dobra chega.
========================================================================== *//* Sai com blur e subida. */.agd.is-live .agd-passo.is-out{
opacity:0;filter:blur(12px);transform:translateY(-18px);
transition:opacity var(--agd-saida) var(--ease),
filter var(--agd-saida) var(--ease),
transform var(--agd-saida) var(--ease);
}
/* Entra em bloco: título, dica e resumo juntos. */.agd.is-live .agd-legenda,
.agd.is-live .agd-dica,
.agd.is-live .agd-fuso,
.agd.is-live .agd-resumo,
.agd.is-live .agd-campos,
.agd.is-live .agd-envio,
.agd.is-live .agd-grade-dia{
opacity:0;filter:blur(10px);transform:translateY(14px);
transition:opacity .62svar(--ease),
filter .62svar(--ease),
transform .62svar(--ease);
}
.agd.is-live .agd-passo.is-in .agd-legenda,
.agd.is-live .agd-passo.is-in .agd-dica,
.agd.is-live .agd-passo.is-in .agd-fuso,
.agd.is-live .agd-passo.is-in .agd-resumo,
.agd.is-live .agd-passo.is-in .agd-campos,
.agd.is-live .agd-passo.is-in .agd-envio,
.agd.is-live .agd-passo.is-in .agd-grade-dia{opacity:1;filter:blur(0);transform:none}
/* E os blocos escalonam. O índice mora no style de cada dia e de cada horário
(--agd-i, escrito pelo script), e não numa cadeia de :nth-child: a agenda
muda a cada carga, e cravar o atraso na posição faria a entrada se
desalinhar sozinha na primeira grade de tamanho diferente. Mesmo recurso do
--perf-i do conversao/diagnostico-perfil. */.agd.is-live .agd-dia,
.agd.is-live .agd-hora{
opacity:0;filter:blur(8px);transform:translateY(12px);
transition:opacity .5svar(--ease),
filter .5svar(--ease),
transform .5svar(--ease);
transition-delay:calc(.1s + var(--agd-i,0) * var(--agd-bloco-passo));
}
.agd.is-live .agd-passo.is-in .agd-dia,
.agd.is-live .agd-passo.is-in .agd-hora{opacity:1;filter:blur(0);transform:none}
/* Movimento reduzido: o passo troca, mas não anda. O script já pula a saída
animada; o que sobra é o estado inicial dos blocos, que sem esta regra
deixaria o passo invisível para sempre. A peça continua inteira — o que
morre é o escalonamento, não a funcionalidade. Colada à regra que desliga,
como manda o §6.5 do contrato. */@media(prefers-reduced-motion:reduce){
.agd.is-live .agd-legenda,
.agd.is-live .agd-dica,
.agd.is-live .agd-fuso,
.agd.is-live .agd-resumo,
.agd.is-live .agd-campos,
.agd.is-live .agd-envio,
.agd.is-live .agd-grade-dia,
.agd.is-live .agd-dia,
.agd.is-live .agd-hora{
opacity:1;filter:none;transform:none;transition:none;
}
.agd-dia-marca,
.agd-hora-marca,
}
/* ==========================================================================
MOBILE
========================================================================== */@media(max-width:900px){
.agd-resumo{grid-template-columns:1fr;gap:0}
.agd-resumo-item + .agd-resumo-item{margin-top:16px}
.agd-campos{grid-template-columns:1fr}
.agd-campo.is-full{grid-column:auto}
}
@media(max-width:560px){
.agd{
/* Três horários por linha em 390px, em vez de dois. A fila de dias não
precisa de ajuste aqui: ela já virou trilho sozinha, pelo piso de
--agd-dia-min, umas seiscentas larguras acima desta. */--agd-hora-min:88px;
/* Sem piso no celular, e de propósito: ali o passo já ocupa boa parte da
tela, e o piso só abriria um vão morto embaixo da grade mais curta. */--agd-palco-min:auto;
}
/* "1 DIA — 2 HORÁRIO — 3 CONFIRMAÇÃO" em caixa alta com tracking largo pede
~350px, e quebraria em duas linhas deixando um traço órfão no começo da
segunda. Somem os rótulos, ficam os três discos numerados: o progresso
continua visível, e o nome do passo já está no título dele, dois dedos
abaixo. A informação para o leitor de tela nunca esteve aqui — está na
região aria-live. */.agd-etapa-t{display:none}
.agd-envio .btn,
.agd-acoes .btn,
.agd-sem-js .btn{width:100%}
}
script.js · 496 linhas · 20,8 KB
/* ==========================================================================
AGENDAMENTO DE REUNIÃO — prefixo agd-
Três passos numa peça só: dia, horário e confirmação.
>>> ATENÇÃO: ISTO NÃO ENVIA NADA. <<<
O handler de submit valida os campos, trava o formulário e mostra a
mensagem de sucesso — e NÃO faz requisição alguma. Nenhuma reunião é
marcada em lugar nenhum. Antes de publicar, substitua o bloco marcado
ENVIO por uma chamada real (agenda do assessor, CRM, endpoint próprio).
Publicar assim significa marcar reuniões que ninguém vê.
A DISPONIBILIDADE NÃO MORA AQUI. Ela mora no HTML: cada dia é um <input>
com value (data ISO), data-agd-rotulo e data-agd-vagas, e dia sem vaga leva
`disabled`; cada grade de horários é uma <div data-agd-dia> que casa com o
value do dia. Este script só reage. Para plugar um backend, imprima esse
mesmo bloco de markup com a agenda de verdade — nada aqui precisa mudar.
O QUE ESTE SCRIPT FAZ, EM ORDEM:
1. põe .is-live na seção — é essa classe que liga as regras de troca no
CSS. Sem script, nenhuma delas existe e a peça inteira fica de pé como
um formulário comum de radios;
2. esconde os passos 2 e 3 com [hidden] (nunca com opacity: campo
invisível e tabulável é armadilha) e o bloco "sem JavaScript";
3. numera dias e horários em --agd-i, para a entrada escalonada;
4. troca de passo: saída com blur, entrada escalonada, foco no título
novo, anúncio em aria-live;
5. mantém o par dia/horário coerente: trocar o dia limpa o horário e
troca a grade visível.
NADA É GRAVADO. Sem localStorage, sem cookie, sem sessionStorage: são
nome, e-mail e telefone de alguém com patrimônio. As escolhas vivem nos
próprios <input> e morrem no recarregamento.
MOVIMENTO REDUZIDO: a troca acontece sem saída animada e sem escalonamento
(o CSS zera o estado inicial), e o avanço automático não existe — só o
botão Continuar. A peça continua inteira: o que morre é o movimento.
AVANÇO AUTOMÁTICO, E POR QUE ELE É SÓ PARA QUEM CLICA: seta em grupo de
radio MOVE E MARCA ao mesmo tempo. Quem navega por teclado marcaria o
primeiro dia só de passar por ele, e seria levado para a grade de horários
antes de chegar no dia que queria. Por isso o avanço só dispara quando a
última interação veio de ponteiro. Mesma decisão do
conversao/diagnostico-perfil, e pela mesma razão.
========================================================================== */
(function () {
var raizes = document.querySelectorAll('[data-agd]');
if (!raizes.length) return; // guard silencioso/* Bate com --agd-saida no estilo.css. Mudar um pede mudar o outro. */var SAIDA = 260;
/* Espera antes do avanço automático. Curta o bastante para não parecer
travamento, longa o bastante para o dia marcado acender e ser visto. */var AVANCO = 400;
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
/* Nomes escritos à mão, e não por toLocaleDateString('pt-BR'): o resultado
de toLocale depende do ICU que o navegador embarca, varia entre versões
("seg." com ponto, "segunda-feira" por extenso) e mudaria o desenho da
fila sem aviso. Aqui o texto é o mesmo em toda máquina. */var SEM_CURTA = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
var SEM_LONGA = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'];
var MES_CURTO = ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'];
var MES_LONGO = ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho',
'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'];
function reduzido() {
if (window.LIB && typeof window.LIB.reduce === 'boolean') return window.LIB.reduce;
return matchMedia('(prefers-reduced-motion: reduce)').matches &&
location.search.indexOf('motion=on') < 0;
}
function dois(n) { return (n < 10 ? '0' : '') + n; }
function minuscula(t) { return t ? t.charAt(0).toLowerCase() + t.slice(1) : ''; }
/* ------------------------------------------------------------------------
RECALIBRAGEM DE DATAS — OPCIONAL, e desligada por padrão.
Só roda quando a seção declara data-agd-recalcular. Fora isso o markup é
a verdade: as datas de exemplo estão escritas lá, o arquivo é legível
sozinho e ninguém precisa executar JavaScript para saber o que a peça
mostra.
Para que serve: um preview ou uma página estática que fica meses no ar
envelhece — "segunda, 2 de março" em julho lê como agenda abandonada.
Com o atributo ligado, os dez dias viram os dez próximos dias ÚTEIS a
partir de amanhã, mantendo posição por posição a disponibilidade que o
markup declarou (inclusive quais estão sem vaga).
O que ela NÃO faz, e não deve fazer: inventar disponibilidade. Quem sabe
quais horários existem é a agenda do assessor, e ela chega pelo HTML.
------------------------------------------------------------------------ */function recalibrar(raiz) {
var dias = raiz.querySelectorAll('.agd-dia-in');
var pares = [], k, base;
/* Dois passos: primeiro casa cada dia com a grade dele pelo valor ANTIGO,
depois reescreve. Fazer as duas coisas no mesmo laço quebraria assim que
uma data nova coincidisse com a data antiga de outro dia. */for (k = 0; k < dias.length; k++) {
pares.push({
dia: dias[k],
grade: raiz.querySelector('.agd-grade[data-agd-dia="' + dias[k].value + '"]')
});
}
base = new Date();
base.setHours(12, 0, 0, 0); // meio-dia: imune a horário de verãofor (k = 0; k < pares.length; k++) {
do {
base.setDate(base.getDate() + 1);
} while (base.getDay() === 0 || base.getDay() === 6);
escrever(pares[k], new Date(base.getTime()));
}
}
function escrever(par, d) {
var iso = d.getFullYear() + '-' + dois(d.getMonth() + 1) + '-' + dois(d.getDate());
var rotulo = SEM_LONGA[d.getDay()] + ', ' + d.getDate() + ' de ' + MES_LONGO[d.getMonth()];
var vagas = parseFloat(par.dia.getAttribute('data-agd-vagas')) || 0;
var horas, k, alvo, txt;
par.dia.value = iso;
par.dia.setAttribute('data-agd-rotulo', rotulo);
par.dia.setAttribute('aria-label', rotulo + ', ' +
(vagas > 0 ? vagas + (vagas > 1 ? ' horários livres' : ' horário livre') : 'sem horário livre'));
alvo = par.dia.parentNode;
txt = alvo.querySelector('.agd-dia-sem'); if (txt) txt.textContent = SEM_CURTA[d.getDay()];
txt = alvo.querySelector('.agd-dia-num'); if (txt) txt.textContent = dois(d.getDate());
txt = alvo.querySelector('.agd-dia-mes'); if (txt) txt.textContent = MES_CURTO[d.getMonth()];
if (!par.grade) return;
par.grade.setAttribute('data-agd-dia', iso);
par.grade.setAttribute('aria-label', 'Horários de ' + minuscula(rotulo));
txt = par.grade.querySelector('.agd-grade-dia'); if (txt) txt.textContent = rotulo;
/* O value do horário carrega a data inteira (2026-03-12T15:30), e é isso
que mantém o par dia+hora sem ambiguidade mesmo num envio de formulário
comum, sem script. Só a parte da data é reescrita. */
horas = par.grade.querySelectorAll('.agd-hora-in');
for (k = 0; k < horas.length; k++) horas[k].value = iso + horas[k].value.slice(10);
}
/* ---------------------------------------------------------------------- */function iniciar(raiz) {
var passos = raiz.querySelectorAll('.agd-passo');
if (passos.length < 3) return;
var barra = raiz.querySelector('.agd-barra');
var etapas = raiz.querySelectorAll('.agd-etapa');
var voltar = raiz.querySelector('.agd-voltar');
var acoes = raiz.querySelector('.agd-acoes');
var seguir = raiz.querySelector('.agd-continuar');
var aviso = raiz.querySelector('.agd-anuncio');
var semJs = raiz.querySelector('.agd-sem-js');
var form = raiz.querySelector('.agd-form');
var trilho = raiz.querySelector('.agd-dias');
var dias = raiz.querySelectorAll('.agd-dia-in');
var grades = raiz.querySelectorAll('.agd-grade');
var resumo = raiz.querySelector('.agd-resumo');
var valDia = raiz.querySelector('[data-agd-val-dia]');
var valHora = raiz.querySelector('[data-agd-val-hora]');
var ok = raiz.querySelector('.agd-ok');
var i = 0; // passo corrente, 0 a 2var animando = false;
var timer = 0;
var viaPonteiro = false;
var off = null, aceso = false;
var k;
if (raiz.hasAttribute('data-agd-recalcular')) recalibrar(raiz);
/* ---------------------------------------------------- inicialização */
raiz.classList.add('is-live');
for (k = 1; k < passos.length; k++) passos[k].hidden = true;
if (semJs) semJs.hidden = true;
if (barra) barra.hidden = false;
if (acoes) acoes.hidden = false;
numerar();
trocarGrade();
atualizarBarra();
/* O índice de cada bloco mora no style dele, e não numa cadeia de
:nth-child: a agenda muda a cada carga (uma grade de três horários hoje,
de seis amanhã), e cravar o atraso na posição desalinharia a entrada
sozinha. Nos horários o índice reinicia por grade — o que escalona é a
grade que a pessoa vê, não a soma de todas. */function numerar() {
var n, j, blocos;
for (n = 0; n < dias.length; n++) dias[n].parentNode.style.setProperty('--agd-i', n);
for (n = 0; n < grades.length; n++) {
blocos = grades[n].querySelectorAll('.agd-hora');
for (j = 0; j < blocos.length; j++) blocos[j].style.setProperty('--agd-i', j);
}
}
/* A peça acende quando a dobra chega, e não na carga: assinar o laço único
do core é o caminho do contrato (§7.4) — o componente não registra
listener nem observer próprio. */function acender() {
if (aceso) return;
if (raiz.getBoundingClientRect().top > innerHeight * .92) return;
aceso = true;
passos[0].classList.add('is-in');
if (off) { off(); off = null; }
}
if (window.LIB && window.LIB.onScroll) {
off = window.LIB.onScroll(acender);
if (aceso && off) { off(); off = null; } // já estava na tela
} else {
passos[0].classList.add('is-in');
}
/* ---------------------------------------------------------- utilidades */function marcado(sel) { return raiz.querySelector(sel + ':checked'); }
function rotuloDia() {
var m = marcado('.agd-dia-in');
return m ? m.getAttribute('data-agd-rotulo') : '';
}
function rotuloHora() {
var m = marcado('.agd-hora-in');
return m ? m.getAttribute('data-agd-hora') : '';
}
function respondido(n) {
if (n === 0) return !!marcado('.agd-dia-in');
if (n === 1) return !!marcado('.agd-hora-in');
returntrue;
}
function anunciar(texto) { if (aviso) aviso.textContent = texto; }
function cancelar() { if (timer) { clearTimeout(timer); timer = 0; } }
/* Mostra a grade do dia marcado e esconde as outras. Sem dia marcado (a
carga, ou depois de um "Editar"), a primeira grade fica de pé para o
passo 2 nunca ser uma tela vazia. */function trocarGrade() {
var m = marcado('.agd-dia-in');
var alvo = m ? m.value : null, n, casou = false;
for (n = 0; n < grades.length; n++) {
var bate = alvo ? grades[n].getAttribute('data-agd-dia') === alvo : (n === 0);
grades[n].hidden = !bate;
if (bate) casou = true;
}
/* Dia sem grade correspondente no HTML: nada é escondido às escondidas,
o passo 2 mostra a primeira grade e o console fica limpo. */if (!casou && grades.length) grades[0].hidden = false;
}
function limparHoras() {
var horas = raiz.querySelectorAll('.agd-hora-in'), n;
for (n = 0; n < horas.length; n++) horas[n].checked = false;
}
function atualizarResumo() {
if (valDia) valDia.textContent = rotuloDia() || '—';
if (valHora) valHora.textContent = rotuloHora() || '—';
if (resumo) resumo.hidden = !(rotuloDia() && rotuloHora());
}
function atualizarBarra() {
var n;
for (n = 0; n < etapas.length; n++) {
etapas[n].classList.remove('is-atual');
etapas[n].classList.remove('is-feita');
if (n === i) etapas[n].classList.add('is-atual');
elseif (n < i) etapas[n].classList.add('is-feita');
}
if (voltar) voltar.hidden = (i === 0);
/* No último passo quem avança é o próprio Confirmar, dentro do
formulário. Dois botões primários na mesma tela disputariam. */if (acoes) acoes.hidden = (i >= passos.length - 1);
if (seguir) seguir.disabled = !respondido(i);
}
/* No celular a fila de dias é um trilho: voltar ao passo 1 com o dia 9
escolhido não pode deixar a pessoa olhando para o dia 1. Só mexe quando
há rolagem de verdade — no desktop os dez dias cabem e um scrollLeft
forçado seria movimento sem motivo. */function trazerDiaParaVista() {
var m = marcado('.agd-dia-in'), caixa, alvo;
if (!m || !trilho || trilho.scrollWidth <= trilho.clientWidth) return;
alvo = m.parentNode.getBoundingClientRect();
caixa = trilho.getBoundingClientRect();
trilho.scrollLeft += (alvo.left - caixa.left) - (caixa.width - alvo.width) / 2;
}
function anunciarPasso() {
if (i === 0) { anunciar('Passo 1 de 3. Escolha o dia.'); return; }
if (i === 1) {
anunciar('Passo 2 de 3. Horários de ' + minuscula(rotuloDia()) +
', no fuso de Brasília.');
return;
}
anunciar('Passo 3 de 3. ' + rotuloDia() + ', ' + rotuloHora() +
', horário de Brasília. Confirme os seus dados.');
}
/* ---------------------------------------------------------------- troca */function ir(destino) {
if (animando || destino === i) return;
if (destino < 0 || destino > passos.length - 1) return;
cancelar();
var sai = passos[i], entra = passos[destino];
animando = true;
function fim() {
sai.classList.remove('is-out');
sai.hidden = true;
i = destino;
if (i === 1) trocarGrade();
if (i === 2) atualizarResumo();
entra.hidden = false;
/* Tira e repõe o .is-in para o escalonamento rodar TODA vez que o
passo entra, e não só na primeira. Sem isto, voltar ao horário
depois de trocar o dia mostraria a grade nova já montada, sem
entrada nenhuma. O reflow entre os dois é obrigatório: sem ele o
navegador junta as duas mudanças no mesmo recálculo e a transição
não roda. */
entra.classList.remove('is-in');
void entra.offsetWidth;
entra.classList.add('is-in');
atualizarBarra();
anunciarPasso();
if (i === 0) trazerDiaParaVista();
var alvo = entra.querySelector('.agd-foco');
if (alvo) alvo.focus({ preventScroll: true });
animando = false;
}
if (reduzido()) {
fim();
} else {
sai.classList.add('is-out');
setTimeout(fim, SAIDA);
}
}
function avancar() { if (respondido(i)) ir(i + 1); }
function agendarAvanco() {
if (reduzido() || !viaPonteiro) return;
cancelar();
timer = setTimeout(function () { timer = 0; avancar(); }, AVANCO);
}
/* -------------------------------------------------------------- eventos */
raiz.addEventListener('pointerdown', function () { viaPonteiro = true; }, { passive: true });
raiz.addEventListener('keydown', function (e) {
viaPonteiro = false;
if (e.key !== 'Enter') return;
var alvo = e.target;
if (!alvo || !alvo.classList) return;
if (!alvo.classList.contains('agd-dia-in') && !alvo.classList.contains('agd-hora-in')) return;
e.preventDefault();
cancelar();
avancar();
});
raiz.addEventListener('change', function (e) {
var alvo = e.target, vagas;
if (!alvo || !alvo.classList) return;
if (alvo.classList.contains('agd-dia-in')) {
/* Trocar o dia LIMPA o horário. Manter "15h30" marcado de outro dia
produziria uma reunião que o resumo mostra certa e a agenda não
tem: a grade seguinte é outra, e o horário pode nem existir nela. */
limparHoras();
trocarGrade();
atualizarResumo();
vagas = parseFloat(alvo.getAttribute('data-agd-vagas')) || 0;
anunciar(alvo.getAttribute('data-agd-rotulo') + ' selecionado. ' +
(vagas === 1 ? '1 horário livre.' : vagas + ' horários livres.'));
atualizarBarra();
agendarAvanco();
return;
}
if (alvo.classList.contains('agd-hora-in')) {
atualizarResumo();
anunciar(rotuloDia() + ', ' + rotuloHora() + ' selecionado.');
atualizarBarra();
agendarAvanco();
}
});
if (seguir) seguir.addEventListener('click', function () { cancelar(); avancar(); });
if (voltar) voltar.addEventListener('click', function () { cancelar(); ir(i - 1); });
/* "Editar" no resumo: volta ao passo que fez a escolha, com o que já foi
escolhido intacto — os radios continuam marcados, ninguém remonta nada. */
raiz.addEventListener('click', function (e) {
var alvo = e.target.closest ? e.target.closest('[data-agd-ir]') : null;
if (!alvo) return;
cancelar();
ir(parseFloat(alvo.getAttribute('data-agd-ir')));
});
/* O aviso de campo vazio some no primeiro toque: erro que fica na tela
depois de corrigido treina a pessoa a ignorar o aviso. */
raiz.addEventListener('input', function (e) {
var campo = e.target.closest ? e.target.closest('.agd-campo') : null;
if (campo) campo.classList.remove('is-erro');
});
/* ---------------------------------------------------------------- envio */if (form) form.addEventListener('submit', function (e) {
e.preventDefault();
if (!respondido(0)) { ir(0); return; }
if (!respondido(1)) { ir(1); return; }
/* Validação à mão porque o markup declara novalidate: a bolha nativa do
navegador não é estilizável, aparece fora do painel e some sozinha —
num formulário dentro de um passo que troca, ela vira fantasma. */var campos = raiz.querySelectorAll('.agd-input');
var falhou = null, n, campo;
for (n = 0; n < campos.length; n++) {
campo = campos[n];
if (campo.required && !campo.value.trim()) { falhou = campo; break; }
if (campo.type === 'email' && campo.value.trim() && !EMAIL.test(campo.value.trim())) {
falhou = campo; break;
}
}
if (falhou) {
var caixa = falhou.closest('.agd-campo');
if (caixa) caixa.classList.add('is-erro');
falhou.focus();
anunciar('Falta preencher ' + (caixa ? caixa.querySelector('.agd-rot').textContent : 'um campo') + '.');
return;
}
/* ---------------- ENVIO ----------------
>>> NÃO HÁ ENVIO. Troque este bloco pela chamada real. Exemplo:
fetch('/api/agenda', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
quando: raiz.querySelector('.agd-hora-in:checked').value, // 2026-03-12T15:30
fuso: 'America/Sao_Paulo',
nome: form.nome.value,
email: form.email.value,
telefone: form.telefone.value,
assunto: form.assunto.value
})
}).then(sucesso).catch(erro);
E trate o erro: o horário pode ter sido tomado entre a carga da página
e o clique. O caminho honesto é voltar ao passo 2 com a grade nova. */
sucesso();
function sucesso() {
var trava = raiz.querySelectorAll('input, button, textarea'), n;
if (ok) {
ok.textContent = 'Reunião pedida para ' + minuscula(rotuloDia()) + ', às ' +
rotuloHora() + ' (Brasília). Um assessor confirma por e-mail ' +
'em até um dia útil.';
ok.hidden = false;
}
for (n = 0; n < trava.length; n++) trava[n].disabled = true;
if (barra) barra.hidden = true;
if (acoes) acoes.hidden = true;
/* O foco tem de ir para a confirmação: quem apertou Confirmar pelo
teclado estava no botão, e travar o botão joga o foco no <body> —
a pessoa fica sem lugar nenhum, na exata tela em que precisa ler o
que aconteceu. Por isso o .agd-ok leva tabindex="-1". */if (ok) ok.focus({ preventScroll: true });
/* Nada é gravado: o estado da peça morre com a página, de propósito.
Ver o cabeçalho deste arquivo. */
}
});
}
for (var n = 0; n < raizes.length; n++) iniciar(raizes[n]);
})();
Um painel de vidro na largura da coluna, sem imagem, com três telas que se trocam no mesmo lugar. Na primeira, uma fila de dez dias úteis: dia da semana em caixa alta, número grande em display, mês, e embaixo quantos horários aquele dia tem — o dia lotado continua na fila, apagado e de contorno tracejado. Na segunda, a grade de horários daquele dia, em blocos, com o fuso declarado ali mesmo, numa pílula. Na terceira, o resumo do que foi escolhido — editável em um clique, sem sair da peça — e quatro campos: nome, e-mail, telefone e uma linha de assunto.
O problema editorial é o do atrito da primeira conversa. Uma assessoria não vende no site: o site marca a reunião. E o ponto em que se perde a pessoa é justamente o link que a leva embora — para o Calendly, para o WhatsApp, para o e-mail que ela vai mandar depois. Quem já escolheu quinta às 15h30 preenche o nome; quem tem de sair da página some. Por isso a ordem aqui é invertida em relação a todo formulário de captura: primeiro quando, depois quem. O formulário é a última tela, e chega quando a pessoa já investiu duas decisões na peça.
Quando usar
Quando o próximo passo do funil é uma reunião com hora marcada, e existe agenda real por trás para sustentá-la.
No fim da página, depois de a proposta e a prova social já terem sido apresentadas — ou como dobra de destino de um botão "agendar" que vem do topo.
Quando a agenda é enxuta e curada (dez dias, meia dúzia de horários por dia). Uma agenda de três meses não cabe nesta forma: ali o certo é um calendário de mês, que é outro componente e ainda não existe na biblioteca.
Uma vez por página. Duas peças de agendamento na mesma rolagem viram dúvida sobre qual é a de verdade.
Quando não usar
Quando a captura é genérica e a data se combina depois, por e-mail ou telefone: aí o certo é o conversao/cta-formulario-lead. A regra de bolso: se a empresa não tem horário para oferecer agora, pedir que a pessoa escolha um é promessa que não se cumpre — e um horário confirmado que ninguém honra custa mais caro que um lead que demora um dia. Os dois não se substituem; ver o gotcha "qual dos dois usar".
Quando o que falta é a pessoa se reconhecer antes de falar com alguém: conversao/diagnostico-perfil é a peça do meio da página, e os dois se completam — o diagnóstico termina num botão que pode apontar para cá.
Como abertura de página. A dobra de topo precisa dizer o que a empresa faz antes de pedir uma quinta-feira.
Quando a agenda muda de minuto em minuto e a página é estática e cacheada: a peça mostraria vagas que já não existem. Ou a página passa a ser renderizada com a agenda do momento, ou o campo de horário vira janela ("manhã", "tarde") — ver a variação sem o passo de horário.
Padrão editorial
Slot
Regra
Olho
Uma palavra. "Agenda", "Conversa", "Primeiro passo".
Título
h2.sec-title de uma linha, com o <em> na palavra que nomeia o encontro ("conversa", "reunião"). Diz quando, não "preencha".
Apoio
Três informações, nesta ordem: quanto dura, por onde (vídeo, telefone, escritório) e o que é — uma conversa, não uma consultoria. É onde o compliance mora, e por isso ele não é letra miúda.
Dia
Dez dias úteis, do mais próximo ao mais distante. Nunca reordenar por "mais disponível": a fila é um calendário, e calendário fora de ordem é armadilha.
Vagas
Número, não barra nem "poucas vagas". "4 horários" informa; "últimas vagas" é escassez fabricada, e numa assessoria isso queima a confiança que a peça inteira existe para construir.
Dia lotado
Fica na fila, apagado, com "Sem vaga". A ausência informa: some da fila, e a pessoa acha que a quarta-feira não existe.
Fuso
Declarado na peça, junto da grade e de novo no resumo. Agendamento sem fuso é o erro mais caro desta forma.
Horário
Blocos de mesma duração, em ordem crescente, manhã e tarde na mesma grade. De 3 a 8 por dia; acima disso a grade vira parede e a escolha trava.
Campos
Quatro, no máximo. Nome, e-mail, telefone e uma linha livre. Todo campo a mais aqui é uma reunião a menos — a qualificação é assunto da conversa, não do formulário.
Confirmação
Diz o que vai acontecer e quando ("um assessor confirma por e-mail em até um dia útil"), nunca só "obrigado".
Especificação dos assets
Nenhum. É uma peça de tipografia e contorno: o que ocupa a dobra é a agenda. Uma foto ao lado empurraria a fila de dias para meia coluna, e dez dias em 500px não cabem sem virar trilho já no desktop. O que a seção precisa é de largura — a fila de dez dias mede 1018px a 1440, e é o que a mantém sem rolagem — e de uma âncora de destino para o href="#contato" do botão alternativo.
Compliance
A peça marca uma conversa. Ela não faz diagnóstico, não analisa carteira e não recomenda nada — e o texto tem de dizer isso onde a pessoa lê, não no rodapé.
O que o consumidor tem de trocar antes de publicar:
O envio. >>> O FORMULÁRIO NÃO ENVIA NADA. <<< O handler valida, trava e mostra a confirmação, e é só isso: nenhuma requisição sai da página. O ponto de ligar o endpoint está marcado ENVIO no script.js, com o exemplo de fetch e o lembrete de tratar o erro de horário já tomado. Publicar assim significa marcar reuniões que ninguém vê.
A disponibilidade. Datas, vagas e horários do markup são exemplo, marcados como CONTEÚDO ILUSTRATIVO. Ou o bloco passa a ser impresso pela agenda de verdade, ou a página vira uma promessa de horário que não existe.
O fuso. Está escrito "GMT-3, fuso de Brasília" em dois lugares (a pílula do passo 2 e o rodapé do resumo). Trocar pelo fuso de quem atende. Se o público é internacional, some a obrigação de dizer o fuso do cliente também — e aí o campo vira um select de fuso, que esta peça não traz.
A política de privacidade. O .agd-nota do passo 3 diz para que os dados servem. Onde a área jurídica do cliente exigir, ele recebe o link para a política e a menção à LGPD; o lugar é ali, ao lado do botão, e não em letra miúda no fim da página.
A voz. Nada de "consultoria gratuita de investimentos": consultoria é atividade regulada, e prometer análise de carteira numa conversa de 30 minutos promete o que só existe com contrato. Nada de retorno, nem insinuado; nada de superlativo de mercado; nenhum nome de instituição real. O texto que está no markup diz o essencial: uma conversa para entender o caso e explicar como a assessoria trabalha e como é remunerada — taxa previamente acordada.
A confirmação. O texto de sucesso promete um retorno ("um assessor confirma por e-mail em até um dia útil"). É uma promessa operacional: só deixe no ar se o processo do cliente a sustenta.
Dado pessoal. A peça coleta nome, e-mail e telefone, e não grava nada: sem localStorage, sem sessionStorage, sem cookie. As escolhas vivem nos próprios <input> e morrem no recarregamento. Medido depois do fluxo inteiro, com o formulário já enviado: localStorage 0 chaves, sessionStorage 0 chaves, document.cookie vazio. São dados de contato de alguém com patrimônio — gravar "para não perder o preenchimento" deixaria isso no navegador de um computador que pode ser compartilhado.
O script precisa do core/motion.js carregado antes (lê LIB.reduce e LIB.onScroll), e a página precisa de uma âncora #contato para o botão alternativo do bloco sem JavaScript.
Ganchos
Gancho
Papel
data-agd
A seção. Raiz de toda busca do script; sem ela, saída silenciosa.
data-agd-recalcular
Opcional, ausente por padrão. Reescreve os dez dias como os dez próximos dias úteis a partir de amanhã, mantendo a disponibilidade posicional. Sem ele, o markup é a verdade.
value do dia
Data ISO. É o que casa o dia com a grade dele.
data-agd-rotulo
O dia por extenso, para o resumo, o aria-live e o aria-label.
data-agd-vagas
Quantos horários o dia tem. Dia sem vaga leva também disabled — e é o disabled que o tira da tabulação e da seta.
data-agd-dia
Na grade; casa com o value do dia.
value do horário
Data inteira mais hora (2026-03-12T15:30). Ver gotcha.
data-agd-hora
A hora legível (15h30), para resumo e anúncio.
data-agd-ir
No botão Editar: índice do passo para onde voltar.
--agd-dia-min
Largura mínima do dia, 92px. Decide sozinha se a fila cabe ou vira trilho.
--agd-hora-min
Largura mínima do bloco de horário.
--agd-palco-min
Piso do palco. Cobre os passos 1 e 2; auto no celular.
--agd-saida
Saída do passo, .26s. Bate com SAIDA no script.
--agd-bloco-passo / --agd-i
Atraso entre blocos e o índice de cada um, escrito pelo script.
.is-live
Script vivo. Sem ela nada é escondido: é o estado sem JavaScript.
.is-in / .is-out
O passo que entra e o que sai.
.is-atual / .is-feita
Estado da trilha decorativa.
.agd-foco
Quem recebe o foco na troca: o legend do passo.
.agd-anuncio
role="status" aria-live="polite". Leva a primitiva .visually-hidden do core, sem regra própria.
.agd-nota
A ressalva de compliance.
Variações
cinco dias em vez de dez — apague blocos .agd-dia e as grades correspondentes; nada no script conta dez.
sem o passo de horário — dois blocos por dia ("manhã", "tarde") no lugar da grade. É a saída para agenda sem encaixe fixo.
datas recalculadas a partir de hoje — data-agd-recalcular na seção. Para página estática e para o showroom; nunca para produção com agenda real.
em seção clara — .sec-paper no lugar de .sec-dark; confira o .glass, que foi desenhado para fundo escuro.
sem reveal de entrada — apague o data-rv do .agd-painel; a peça passa a valer com o JavaScript da página inteiro desligado.
Gotchas
Qual dos dois usar, e a pergunta que decide. Este e o conversao/cta-formulario-lead parecem o mesmo componente e não são: lá o formulário é a porta (foto de fundo, lista de benefícios, card de captura, e a data se combina depois); aqui o formulário é o último passo, e o que abre a peça é a agenda. A pergunta que decide não é de design, é de operação: a empresa tem horário para oferecer agora? Se tem, esta peça tira o atrito de duas semanas de e-mail. Se não tem — agenda cheia, equipe pequena, atendimento por triagem —, ela vira uma promessa que o time não honra, e aí o cta-formulario-lead é o certo. Os dois cabem na mesma página em dobras diferentes: capturar quem só quer material, agendar quem já quer conversar.
A disponibilidade vive no HTML, e essa é a decisão que sustenta a peça. Cada dia é um <input> com value, data-agd-rotulo e data-agd-vagas; cada grade é uma <div data-agd-dia> que casa com o value do dia. O script só reage — ele não sabe o que é um dia útil, não constrói horário e não tem tabela nenhuma. Plugar um backend é imprimir esse mesmo bloco com a agenda de verdade, e nenhuma linha de JavaScript muda. O contrário — a agenda descrita num objeto no script — obrigaria todo consumidor a editar o script.js, que é justamente o arquivo que a biblioteca pede para colar sem tocar.
O value do horário carrega a data inteira, e não só a hora.2026-03-12T15:30, não 15h30. Parece redundante, já que o dia tem o próprio radio — e não é: sem JavaScript as oito grades ficam todas visíveis, e nada impede alguém de marcar "quinta, 12" na fila e "10h00" na grade de segunda. Com a data no value, o par chega inteiro do lado de lá mesmo num envio de formulário comum, e o dia marcado vira conferência, não fonte única. É também o que o fetch do exemplo manda: um campo só, sem remontar data no servidor.
Trocar o dia limpa o horário — sempre, e sem perguntar. Manter "15h30" marcado depois de trocar de quinta para sexta produziria uma reunião que o resumo mostra certa e a agenda não tem: a grade seguinte é outra, e aquele horário pode nem existir nela. Medido: escolhido 12/03 às 15h30, ao escolher 06/03 o .agd-hora-in:checked volta a null e a grade visível passa a 2026-03-06. O que não se perde é o caminho de volta: "Editar" no resumo devolve ao passo 1 com dia e horário intactos — medido, dia 2026-03-12 e hora 2026-03-12T15:30 ainda marcados depois do retorno. Limpar é consequência de trocar, não de voltar.
O avanço automático é só para quem clica, e essa é a decisão mais importante do arquivo. Seta em grupo de radio move e marca ao mesmo tempo: quem navega por teclado marcaria o primeiro dia só de passar por ele, e seria levado para a grade de horários antes de chegar no dia que queria. O script guarda a modalidade da última interação (pointerdown liga, keydown desliga) e só agenda o avanço quando veio de ponteiro. Medido com mouse de verdade (Chrome headless, mousedown/mouseup no dia 12): 400ms de espera para o dia marcado acender, mais 260ms de saída, e o passo 2 no lugar com a grade certa e o foco no título novo. Medido por seta: ArrowRight no primeiro dia move a marcação para 03/03 e troca a grade, e 900ms depois a peça continua no passo 1. Mesma decisão do conversao/diagnostico-perfil, e pela mesma razão.
O botão "Continuar" existe sempre, e não só em movimento reduzido. Nasce disabled e libera quando o passo é respondido — medido nos dois passos. Em movimento reduzido ele é o único caminho: medido com prefers-reduced-motion, um clique de ponteiro no dia deixa a peça no passo 1 mesmo 900ms depois, e o Continuar leva adiante normalmente. Escondê-lo quando o avanço automático está ligado deixaria quem chega de teclado sem controle visível de avanço, já que o avanço dele nunca dispara.
O dia sem vaga é disabled no <input>, e nada mais. Sem classe de estado, sem pointer-events:none, sem tratamento no script: o disabled já tira o dia da ordem de tabulação e da navegação por seta do grupo de radio, que é comportamento nativo. Medido: focus() no dia lotado não move o activeElement, click() não marca, e o dia que estava escolhido continua escolhido. O CSS só reage — .agd-dia-in:disabled ~ .agd-dia-marca desenha o contorno tracejado e apaga os números. Escrever isso à mão seria reimplementar, pior, o que o navegador já faz.
A fila de dias é um grid só, e não há breakpoint que a transforme em trilho.grid-auto-columns:minmax(92px,1fr) com overflow-x:auto: enquanto as dez colunas couberem elas crescem e dividem o painel; assim que não couberem, a caixa rola. Medido a 1440: dez dias de 93px num trilho de 1018px que não rola (scrollWidth === clientWidth). Medido a 390: dias de 92px, scrollWidth 1004 contra clientWidth 304, rolagem horizontal dentro da caixa — e a página não acompanha, nem depois de rolar o trilho até o fim (document.scrollWidth === clientWidth, window.scrollX 0). Os 92px não são gosto: são o que "4 horários" pede em 10,5px com tracking, mais o respiro. Encurtar esse texto é o que permite baixar o número.
O trilho tem padding:6px, e é por causa do anel de foco. O :focus-visible do core sai 3px para fora do dia com 2px de traço; sem esse respiro, a própria caixa de rolagem cortaria o anel — justamente em quem depende dele para saber onde está. Medido no primeiro dia: 2px solid rgb(198,206,170) com offset 3px, numa caixa de 93×118 — o dia inteiro, não um quadradinho de radio perdido no canto.
No celular, voltar ao passo 1 traz o dia escolhido de volta à vista. Um trilho de 1004px rolado até o dia 9 e depois devolvido ao começo faria a pessoa procurar a própria escolha. O script recentra o dia marcado — mas só quando há rolagem de verdade (scrollWidth > clientWidth), porque no desktop os dez dias cabem e um scrollLeft forçado seria movimento sem motivo. Medido a 390: depois de "Editar", scrollLeft 700 e o dia escolhido inteiro dentro da caixa.
O piso do palco cobre os passos 1 e 2, e deixa o 3 crescer. Sem piso, cada troca daria um solavanco e a página inteira dançaria sob quem está lendo. Com um piso que coubesse o passo 3 — um formulário de quatro campos —, sobrariam mais de 200px de vão morto embaixo da fila de dias. clamp(292px,32vh,360px) é o meio-termo: medido a 1440, os passos 1 e 2 ficam ambos em 292px, e o 3 passa disso por conta própria. É a diferença para o conversao/diagnostico-perfil, onde o piso é medido pelo script: lá as cinco telas são do mesmo tipo e só variam com a quebra de linha do enunciado, e nenhum clamp acerta; aqui os três passos são de tipos diferentes, e igualá-los seria esconder essa diferença.
O .is-in é tirado e reposto a cada entrada de passo. Se ficasse, voltar ao horário depois de trocar o dia mostraria a grade nova já montada, sem entrada nenhuma — a classe já estava lá, e a transição não roda de novo. Entre tirar e repor há um void entra.offsetWidth: sem esse reflow o navegador junta o "deixou de ser display:none" e o "ganhou .is-in" no mesmo recálculo, e a animação simplesmente não acontece. Medido no meio da entrada: blocos de horário em opacity 0.815, escalonando.
A troca de passo não usa data-rv, e não pode usar. O data-rv do core é observer de entrada na tela: dispara uma vez, desobserva e nunca mais volta. Uma peça que troca de conteúdo três vezes no mesmo lugar precisa de transição por classe. O que o core continua fazendo aqui é o que ele sabe: revelar o painel quando a dobra chega. Por isso o painel fica em opacity:0 se o core/motion.js estiver fora do ar — medido a 390 antes de rolar: painelOpacidade 0; depois de rolar: 1. Vale para todo componente da biblioteca, não é desta peça; quem precisa sobreviver a bloqueador de script usa a variação sem reveal de entrada.
.agd [hidden]{display:none} no topo do arquivo não é redundância. O [hidden] da folha do navegador tem especificidade (0,1,0) e perde para qualquer classe que declare display — e o componente declara grid e flex em oito lugares. Sem essa regra, os passos escondidos reapareceriam empilhados, com o atributo lá no inspetor. Mesma pegadinha que o .hgal-nav[hidden] da midia/galeria-scroll-horizontal documenta.
O estado sem JavaScript é o estado de PARTIDA do markup, e não um <noscript>. Tudo nasce visível; quem esconde os passos 2 e 3, a trilha, o Continuar e o resumo é o script. Medido com o script.js bloqueado (o core vivo): 3 passos visíveis, 10 dias (8 habilitados), 8 grades, 35 blocos de horário, 4 campos e o botão de confirmar de pé; trilha, ações, resumo e mensagem de sucesso escondidos; a nota do .agd-sem-js visível com o botão para #contato; seção de 2720px a 1440 e 3255px a 390, sem estouro horizontal em nenhum dos dois. O inverso — CSS escondendo e script mostrando — dá uma casca vazia quando o script falha, que é o pior estado possível numa peça que existe para marcar reunião.
A recalibragem de datas é opcional, e tem de continuar sendo. Com data-agd-recalcular na seção, o script reescreve os dez dias como os dez próximos dias úteis a partir de amanhã, mantendo posição por posição a disponibilidade que o markup declarou. Medido em 06/09/2026 (um domingo): o primeiro dia vira 2026-09-07 / "Segunda, 7 de setembro" / "Seg 07 set", o terceiro continua "Sem vaga" (2026-09-09), o último vira 2026-09-18, os value dos 35 horários acompanham a data nova e nenhuma grade fica órfã. Sem o atributo, nada disso roda e o markup é a verdade — que é o padrão, porque um arquivo cujo conteúdo só existe depois de executar JavaScript deixa de ser legível para quem revisa. E a recalibragem não inventa disponibilidade: quem sabe quais horários existem é a agenda do assessor.
Os nomes de dia e mês são arrays no script, não toLocaleDateString('pt-BR'). O resultado do toLocale depende do ICU que o navegador embarca: "seg." com ponto numa versão, "segunda-feira" por extenso em outra. A fila tem 92px de largura por dia — uma abreviação a mais quebra o desenho, e quebraria sem aviso, meses depois, numa atualização de navegador.
O <input> está esticado sobre o dia inteiro com appearance:none, e não escondido com opacity:0. Com opacity:0 o anel de foco some junto com o widget e quem navega por teclado perde o indicador. Do jeito que está, o :focus-visible do core cai em volta do dia inteiro, e o clique em qualquer ponto chega ao input sem depender de for/id — que colidiria se o componente aparecesse duas vezes na mesma página. O rótulo para o leitor de tela vem de aria-label no próprio input ("Segunda, 2 de março, 4 horários livres"), porque o conteúdo visual é uma sigla de três letras e um número solto.
A validação é à mão porque o <form> declara novalidate. A bolha nativa do navegador não é estilizável, aparece fora do painel e some sozinha — num formulário que vive dentro de um passo que troca, ela vira fantasma. O script para no primeiro campo vazio (ou e-mail malformado), marca a caixa com .is-erro, leva o foco e anuncia: medido, .agd-campo.is-erro em 1 campo, foco em nome, aria-live com "Falta preencher Nome completo." — e a mensagem de sucesso continua escondida, porque nada foi enviado.
Histórico
Projeto
Onde
Data
Diferenças
autoral
construído do zero na biblioteca
2026-09-06
—
Origem. Não há referência única e não há uma linha copiada. O padrão — fila horizontal de dias com número grande, grade de horários em blocos e formulário curto no fim — aparece em três famílias: as ferramentas de agendamento embutidas (Calendly, Cal.com e parentes), a marcação de consulta de clínica e de laboratório, e o passo de horário de entrega no checkout de e-commerce. O que a biblioteca tirou de todas foi a ordem das telas, não o código. Nunca rodou em produção nossa; o primeiro site que usar deixa a marca aqui. O registro consolidado vai em projetos/autorais.md.
Decisões da tradução para o contrato:
A disponibilidade mora em atributos do HTML e o script só reage — é o que permite plugar um backend sem reescrever a peça.
Dias e horários são <input type="radio"> reais dentro de <label>, sem for/id, com o input esticado sobre o bloco em appearance:none. Setas, Home/End e disabled são o comportamento nativo do grupo de radio; nada disso é reimplementado.
O painel consome a primitiva .glass do core, a região viva consome .visually-hidden, o fuso consome .pill/.pill-accent, os botões consomem .btn, a ressalva consome .nota — nenhuma das cinco é recopiada (§6 do contrato).
A quinta primitiva, .field, veste o wrapper de cada campo — a régua de 1px em --rule-2, o respiro de 16px/12px e o acender no :focus-within vêm do core, e o desligamento em movimento reduzido também. Fica local só o que é daqui: o rótulo, porque o wrapper é o <label> (sem for/id, pelo mesmo motivo dos radios) e por isso o rótulo é um <span class="agd-rot">, que o seletor descendente .field label não alcança. Histórico que vale registrar: entre a criação da primitiva e esta linha, no mesmo dia, .field input trazia outline:none — que com (0,1,1) vencia o ::focus-visible (0,1,0) do próprio core e mataria o anel de teclado deste campo. Este componente foi o único dos três que originaram a receita a manter o anel, e foi por isso que o erro apareceu antes de virar problema de alguém. O core parou de zerar o outline, e a adoção deixou de custar nada.
Zero cor crua: contorno, texto, vagas, erro e ressalva leem --rule, --rule-2, --accent, --accent-2, --fg, --fg-2 e --fg-muted do escopo da seção. O realce do bloco marcado sai de color-mix(in srgb,var(--accent) 9%,transparent), que é token e não cor.
O respiro entre cabeçalho e painel sai de calc(var(--sec-gap) * .34) — a fonte única do ritmo vertical (§4), não um clamp solto.
Movimento: entrada do painel pelo data-rv do core; troca de passo por transição própria com filter:blur() indo a zero, como o §5 exige; escalonamento pelo índice no style de cada bloco, nunca por :nth-child. Em prefers-reduced-motion o escalonamento morre (medido: transition-duration: 0s nos blocos) e o avanço automático não existe — a peça continua inteira e navegável.
O componente não registra listener de scroll próprio: a primeira tela acende assinando o LIB.onScroll do core (§7.4), com a assinatura se desfazendo assim que acende.
Acessibilidade: fieldset + legend por passo, foco levado ao título novo com preventScroll, role="group" com aria-label em cada grade, aria-label por dia, região role="status" aria-live="polite" anunciando passo, dia e horário, trilha decorativa em aria-hidden, foco levado à confirmação depois do envio (o botão que a pessoa apertou é travado, e sem isso o foco cairia no <body>), e passos fora de cena com hidden — nunca só opacity.