← Biblioteca componentes/conversao/agendamento-reuniao/ficha.md CódigoTela cheia
conversao/agendamento-reuniao

Agendamento de reunião em três passos

🟢 estavel complexidade alta prefixo agd- agendamento calendario reuniao multi-etapas formulario radio fuso-horario acessibilidade aria-live vidro sem-lib escuro

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/conversao/agendamento-reuniao/estilo.css">
<script src="componentes/conversao/agendamento-reuniao/script.js"></script>

markup.html · 375 linhas · 27,5 KB

<!-- ==========================================================================
     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.
     ========================================================================== -->

<section class="agd sec-dark sec-pad" id="agenda" data-agd>
  <div class="wrap">

    <header class="agd-head">
      <p class="eyebrow" data-rv>Agenda</p>
      <h2 class="sec-title ttl agd-convite">Escolha quando será a primeira <em>conversa</em>.</h2>
      <p class="sec-lead agd-apoio" data-rv data-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>

    <div class="glass agd-painel" data-rv data-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. -->
      <div class="agd-barra" hidden>
        <ol class="agd-trilha" aria-hidden="true">
          <li class="agd-etapa is-atual"><span class="agd-etapa-n">1</span><span class="agd-etapa-t">Dia</span></li>
          <li class="agd-etapa"><span class="agd-etapa-n">2</span><span class="agd-etapa-t">Horário</span></li>
          <li class="agd-etapa"><span class="agd-etapa-n">3</span><span class="agd-etapa-t">Confirmação</span></li>
        </ol>
        <button class="agd-voltar" type="button" hidden>Voltar</button>
      </div>

      <p class="agd-anuncio visually-hidden" role="status" aria-live="polite"></p>

      <div class="agd-palco">
        <form class="agd-form" novalidate>

          <!-- ============================================ PASSO 1 — o dia -->
          <fieldset class="agd-passo">
            <legend class="agd-legenda agd-foco" tabindex="-1">Escolha o dia</legend>
            <p class="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. -->
            <div class="agd-dias">

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Seg</span>
                <span class="agd-dia-num" aria-hidden="true">02</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">4 horários</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Ter</span>
                <span class="agd-dia-num" aria-hidden="true">03</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">5 horários</span>
                <span class="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. -->
              <label class="agd-dia">
                <input class="agd-dia-in" type="radio" name="agd-dia" value="2026-03-04"
                       data-agd-rotulo="Quarta, 4 de março" data-agd-vagas="0" disabled
                       aria-label="Quarta, 4 de março, sem horário livre">
                <span class="agd-dia-sem" aria-hidden="true">Qua</span>
                <span class="agd-dia-num" aria-hidden="true">04</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">Sem vaga</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Qui</span>
                <span class="agd-dia-num" aria-hidden="true">05</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">3 horários</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Sex</span>
                <span class="agd-dia-num" aria-hidden="true">06</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">5 horários</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Seg</span>
                <span class="agd-dia-num" aria-hidden="true">09</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">6 horários</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Ter</span>
                <span class="agd-dia-num" aria-hidden="true">10</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">4 horários</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="agd-dia-in" type="radio" name="agd-dia" value="2026-03-11"
                       data-agd-rotulo="Quarta, 11 de março" data-agd-vagas="0" disabled
                       aria-label="Quarta, 11 de março, sem horário livre">
                <span class="agd-dia-sem" aria-hidden="true">Qua</span>
                <span class="agd-dia-num" aria-hidden="true">11</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">Sem vaga</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Qui</span>
                <span class="agd-dia-num" aria-hidden="true">12</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">5 horários</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

              <label class="agd-dia">
                <input class="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">
                <span class="agd-dia-sem" aria-hidden="true">Sex</span>
                <span class="agd-dia-num" aria-hidden="true">13</span>
                <span class="agd-dia-mes" aria-hidden="true">mar</span>
                <span class="agd-dia-vagas" aria-hidden="true">3 horários</span>
                <span class="agd-dia-marca" aria-hidden="true"></span>
              </label>

            </div>
          </fieldset>

          <!-- ======================================== PASSO 2 — o horário -->
          <fieldset class="agd-passo">
            <legend class="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. -->
            <p class="agd-fuso"><span class="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. -->

            <div class="agd-grade" data-agd-dia="2026-03-02" role="group" aria-label="Horários de segunda, 2 de março">
              <p class="agd-grade-dia">Segunda, 2 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-02T09:00" data-agd-hora="09h00"><span class="agd-hora-txt">09h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-02T11:00" data-agd-hora="11h00"><span class="agd-hora-txt">11h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-02T14:00" data-agd-hora="14h00"><span class="agd-hora-txt">14h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-02T16:30" data-agd-hora="16h30"><span class="agd-hora-txt">16h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

            <div class="agd-grade" data-agd-dia="2026-03-03" role="group" aria-label="Horários de terça, 3 de março">
              <p class="agd-grade-dia">Terça, 3 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-03T09:00" data-agd-hora="09h00"><span class="agd-hora-txt">09h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-03T10:00" data-agd-hora="10h00"><span class="agd-hora-txt">10h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-03T14:00" data-agd-hora="14h00"><span class="agd-hora-txt">14h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-03T15:30" data-agd-hora="15h30"><span class="agd-hora-txt">15h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-03T17:30" data-agd-hora="17h30"><span class="agd-hora-txt">17h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

            <div class="agd-grade" data-agd-dia="2026-03-05" role="group" aria-label="Horários de quinta, 5 de março">
              <p class="agd-grade-dia">Quinta, 5 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-05T10:00" data-agd-hora="10h00"><span class="agd-hora-txt">10h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-05T15:30" data-agd-hora="15h30"><span class="agd-hora-txt">15h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-05T17:30" data-agd-hora="17h30"><span class="agd-hora-txt">17h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

            <div class="agd-grade" data-agd-dia="2026-03-06" role="group" aria-label="Horários de sexta, 6 de março">
              <p class="agd-grade-dia">Sexta, 6 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-06T09:00" data-agd-hora="09h00"><span class="agd-hora-txt">09h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-06T11:00" data-agd-hora="11h00"><span class="agd-hora-txt">11h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-06T14:00" data-agd-hora="14h00"><span class="agd-hora-txt">14h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-06T16:30" data-agd-hora="16h30"><span class="agd-hora-txt">16h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-06T17:30" data-agd-hora="17h30"><span class="agd-hora-txt">17h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

            <div class="agd-grade" data-agd-dia="2026-03-09" role="group" aria-label="Horários de segunda, 9 de março">
              <p class="agd-grade-dia">Segunda, 9 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-09T09:00" data-agd-hora="09h00"><span class="agd-hora-txt">09h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-09T10:00" data-agd-hora="10h00"><span class="agd-hora-txt">10h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-09T11:00" data-agd-hora="11h00"><span class="agd-hora-txt">11h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-09T14:00" data-agd-hora="14h00"><span class="agd-hora-txt">14h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-09T15:30" data-agd-hora="15h30"><span class="agd-hora-txt">15h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-09T16:30" data-agd-hora="16h30"><span class="agd-hora-txt">16h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

            <div class="agd-grade" data-agd-dia="2026-03-10" role="group" aria-label="Horários de terça, 10 de março">
              <p class="agd-grade-dia">Terça, 10 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-10T09:00" data-agd-hora="09h00"><span class="agd-hora-txt">09h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-10T14:00" data-agd-hora="14h00"><span class="agd-hora-txt">14h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-10T15:30" data-agd-hora="15h30"><span class="agd-hora-txt">15h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-10T17:30" data-agd-hora="17h30"><span class="agd-hora-txt">17h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

            <div class="agd-grade" data-agd-dia="2026-03-12" role="group" aria-label="Horários de quinta, 12 de março">
              <p class="agd-grade-dia">Quinta, 12 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-12T10:00" data-agd-hora="10h00"><span class="agd-hora-txt">10h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-12T11:00" data-agd-hora="11h00"><span class="agd-hora-txt">11h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-12T14:00" data-agd-hora="14h00"><span class="agd-hora-txt">14h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-12T15:30" data-agd-hora="15h30"><span class="agd-hora-txt">15h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-12T16:30" data-agd-hora="16h30"><span class="agd-hora-txt">16h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

            <div class="agd-grade" data-agd-dia="2026-03-13" role="group" aria-label="Horários de sexta, 13 de março">
              <p class="agd-grade-dia">Sexta, 13 de março</p>
              <div class="agd-horas">
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-13T09:00" data-agd-hora="09h00"><span class="agd-hora-txt">09h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-13T11:00" data-agd-hora="11h00"><span class="agd-hora-txt">11h00</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
                <label class="agd-hora"><input class="agd-hora-in" type="radio" name="agd-hora" value="2026-03-13T16:30" data-agd-hora="16h30"><span class="agd-hora-txt">16h30</span><span class="agd-hora-marca" aria-hidden="true"></span></label>
              </div>
            </div>

          </fieldset>

          <!-- ==================================== PASSO 3 — a confirmação -->
          <fieldset class="agd-passo">
            <legend class="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. -->
            <div class="agd-resumo" hidden>
              <div class="agd-resumo-item">
                <p class="agd-resumo-rot">Dia</p>
                <p class="agd-resumo-val" data-agd-val-dia>—</p>
                <button class="agd-editar" type="button" data-agd-ir="0">Editar<span class="visually-hidden"> o dia escolhido</span></button>
              </div>
              <div class="agd-resumo-item">
                <p class="agd-resumo-rot">Horário</p>
                <p class="agd-resumo-val" data-agd-val-hora>—</p>
                <button class="agd-editar" type="button" data-agd-ir="1">Editar<span class="visually-hidden"> o horário escolhido</span></button>
              </div>
              <p class="agd-resumo-fuso">Fuso de Brasília (GMT-3) · 30 minutos</p>
            </div>

            <div class="field agd-campos">
              <label class="field agd-campo is-full">
                <span class="agd-rot">Nome completo</span>
                <input class="agd-input" type="text" name="nome" required autocomplete="name" placeholder="Como devemos chamar você">
              </label>
              <label class="field agd-campo">
                <span class="agd-rot">E-mail</span>
                <input class="agd-input" type="email" name="email" required autocomplete="email" placeholder="voce@email.com">
              </label>
              <label class="field agd-campo">
                <span class="agd-rot">Telefone</span>
                <input class="agd-input" type="tel" name="telefone" required autocomplete="tel" placeholder="(00) 00000-0000">
              </label>
              <label class="field agd-campo is-full">
                <span class="agd-rot">Assunto <span class="agd-rot-op">opcional</span></span>
                <input class="agd-input" type="text" name="assunto" placeholder="Em uma linha, o que você quer tratar">
              </label>
            </div>

            <div class="agd-envio">
              <button class="btn btn-primary agd-confirmar" type="submit">
                Confirmar reunião
                <svg width="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"><path d="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. -->
              <p class="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. -->
            <p class="agd-ok" role="status" tabindex="-1" hidden></p>
          </fieldset>

        </form>
      </div>

      <div class="agd-acoes" hidden>
        <button class="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. -->
      <div class="agd-sem-js">
        <p class="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>
        <a class="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 .3s var(--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 .3s var(--ease),border-color .3s var(--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 .3s var(--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:14px 6px 12px;
  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 .3s var(--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 .3s var(--ease);
}
.agd-dia-mes{
  font-family:var(--sans);font-size:11px;letter-spacing:.1em;
  color:var(--fg-2);
  transition:color .3s var(--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 .3s var(--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 .32s var(--ease),background .32s var(--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:15px 8px;
}
.agd-hora-txt{
  font-family:var(--display);font-weight:700;
  font-size:15px;letter-spacing:.02em;color:var(--fg-2);
  transition:color .3s var(--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:1fr 1fr;gap:0 24px;
  margin-top:clamp(20px,3vh,30px);
  padding:18px 20px;
  border:1px solid var(--rule-2);
  border-radius:var(--radius-media);
}
.agd-resumo-item{display:flex;align-items:baseline;flex-wrap:wrap;gap:0 12px}
.agd-resumo-rot{
  flex:0 0 100%;
  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 .3s var(--ease),border-color .3s var(--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:1fr 1fr;gap:0 26px;
  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:1 1 320px;
  --nota-size:11.5px;
  --nota-max:62ch;
  line-height:1.7;
}

.agd-ok{
  margin-top:20px;padding:16px 18px;
  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 .62s var(--ease),
             filter .62s var(--ease),
             transform .62s var(--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 .5s var(--ease),
             filter .5s var(--ease),
             transform .5s var(--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ão
    for (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 2
    var 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');
      return true;
    }

    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');
        else if (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 não usar


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:

  1. 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ê.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.


Como usar

<link rel="stylesheet" href="componentes/conversao/agendamento-reuniao/estilo.css">
<script src="componentes/conversao/agendamento-reuniao/script.js" defer></script>

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


Gotchas


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: