← Biblioteca componentes/hero/campo-de-conversa/ficha.md CódigoTela cheia
hero/campo-de-conversa

Hero com campo de conversa

🟢 estavel complexidade média prefixo cnv- hero above-the-fold campo textarea conversa chips intencao formulario sem-javascript progressive-enhancement lgpd compliance assessoria escuro

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

<link rel="stylesheet" href="componentes/hero/campo-de-conversa/estilo.css">
<script src="componentes/hero/campo-de-conversa/script.js"></script>

markup.html · 189 linhas · 9,7 KB

<!-- ==========================================================================
     HERO COM CAMPO DE CONVERSA — a primeira dobra é um campo, não um botão
     Prefixo: cnv-
     Requer: core (tokens.css, base.css, motion.css, motion.js) + estilo.css
             + script.js (opcional — ver abaixo)
     Usa as primitivas .field, .btn-disc, .nota, .visually-hidden e .grain.

     A IDEIA. O hero pergunta, e a resposta cabe na própria dobra: olho,
     pergunta em duas linhas, uma frase de apoio e — no lugar do par de botões
     de sempre — uma CAIXA DE TEXTO grande com um disco de envio no canto e
     três chips de intenção embaixo. Quem já sabe o que quer escreve; quem não
     sabe clica num chip e cai na mesma conversa com o assunto já declarado. O
     formulário longo fica para depois, quando houver motivo para pedir nome e
     telefone.

     >>> FUNCIONA SEM JAVASCRIPT, E ISSO É REQUISITO, NÃO BRINDE. <<<
     O bloco é um <form action method="post"> de verdade. O disco é
     type="submit" e cada chip é <button type="submit" name="intencao"
     value="…"> — o NAVEGADOR monta o envio com o par nome/valor do botão que
     foi acionado, sem uma linha de script. O script.js desta pasta faz duas
     coisas e só: cresce a altura do campo conforme o texto e anuncia o
     contador quando falta pouco para o teto. Tire o script e a dobra continua
     inteira; tire o CSS e ela continua um formulário.

     POR QUE method="post", E NÃO get. Com get o texto que a pessoa escreveu
     vai para a barra de endereço, para o histórico do navegador, para o
     Referer do próximo pedido e para o log do servidor. Numa assessoria a
     primeira frase costuma trazer o assunto patrimonial inteiro ("vou vender
     minha empresa em 2027") — isso não pode virar URL. POST mantém o texto no
     corpo do pedido.

     POR QUE O CAMPO É `required` E OS CHIPS SÃO `formnovalidate`. Os dois
     caminhos saem do MESMO formulário: sem o formnovalidate, a validação
     nativa barraria o chip por causa de um campo de texto vazio que o chip
     nem usa — e o navegador mostraria "Preencha este campo" apontando para
     uma caixa que a pessoa deliberadamente não quis preencher. Com ele, o
     chip sempre passa e o disco continua exigindo texto.

     A RESSALVA É OBRIGATÓRIA (ver ficha.md, seção Compliance). Ela veste a
     primitiva .nota do core e é apontada pelo aria-describedby do campo: quem
     usa leitor de tela recebe o aviso AO ENTRAR no campo, não depois de
     enviar. Apagá-la transforma uma porta de conversa em captação de dado
     sensível sem aviso.

     A ARTE DE FUNDO É OPCIONAL. Apague o bloco .cnv-arte inteiro e a dobra
     continua legível — a pergunta lê --fg sobre --bg, e o scrim existe para o
     caso contrário: manter esse mesmo contraste quando há foto por baixo.

     O RODAPÉ DE ATRIBUTOS TAMBÉM É OPCIONAL. Apague a <ul class="cnv-atrib">
     e nada se desloca: ela é o último bloco da coluna.

     ENTRADA. Toda a coreografia é do core (.ttl no título, data-rv/data-d nos
     blocos). Nenhum data-rv divide nó com transform próprio — §5.
     ========================================================================== -->

<section class="cnv sec-dark" data-cnv>

  <!-- ARTE DE FUNDO — opcional, sangrada (fica FORA do .wrap, §4).
       Três camadas por cima da foto, e cada uma faz uma coisa:
         .cnv-scrim  dois gradientes chapados, para o texto ter fundo
         .cnv-veu    o véu com backdrop-filter, MASCARADO em gradiente (§6)
         .grain      o ruído do core, para a foto não ler como banner -->
  <div class="cnv-arte" aria-hidden="true">
    <img class="cnv-arte-img" src="../../../showroom/demo/ph-wide-4.webp" alt="" decoding="async">
    <div class="cnv-scrim"></div>
    <div class="cnv-veu"></div>
    <div class="grain"></div>
  </div>

  <div class="wrap cnv-body">

    <p class="eyebrow cnv-olho" data-rv>Assessoria de investimentos</p>

    <!-- A quebra é decidida aqui, no <br>, e o CSS desliga o text-wrap:balance
         que o reset do core dá a todo h1 — ver estilo.css. -->
    <h1 class="display-xl cnv-title ttl">O que você quer resolver<br>com o seu patrimônio?</h1>

    <p class="sec-lead cnv-lead" data-rv data-d="1">
      Escreva em uma frase. Um assessor lê, responde por e-mail e, se fizer
      sentido, marca uma conversa. Sem custo e sem compromisso.
    </p>

    <!-- O action aponta para a página que recebe a conversa. Troque pelo
         endpoint real antes de publicar: aqui ele é relativo de propósito,
         para não fingir um domínio que não existe. -->
    <form class="cnv-form" action="conversa" method="post" data-cnv-form>

      <div class="cnv-slot" data-rv data-d="2">
        <div class="field cnv-campo">
          <label for="cnv-msg">Sua mensagem</label>

          <div class="cnv-linha">
            <textarea id="cnv-msg" class="cnv-texto" name="mensagem" rows="2"
                      maxlength="600" required
                      placeholder="Ex.: herdei uma carteira e não sei por onde começar"
                      aria-describedby="cnv-ressalva cnv-conta"></textarea>

            <button class="btn-disc cnv-enviar" type="submit"
                    aria-label="Enviar mensagem e falar com um assessor">
              <svg class="cnv-seta" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                   stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"
                   aria-hidden="true" focusable="false">
                <path d="M5 12h13M12 6l6 6-6 6"/>
              </svg>
            </button>
          </div>
        </div>

        <!-- Contador SÓ para leitor de tela, e só quando falta pouco: o
             script escreve aqui a partir de 60 caracteres restantes, de 20 em
             20. Anunciar a cada tecla tornaria o campo impossível de usar com
             leitor de tela ligado. Nasce vazio — sem script não há o que
             dizer, e o maxlength do markup continua valendo sozinho. -->
        <p class="visually-hidden" id="cnv-conta" role="status" aria-live="polite" data-cnv-conta
           data-cnv-rot-faltam="Faltam {n} caracteres."
           data-cnv-rot-limite="Limite de {n} caracteres atingido."></p>
      </div>

      <!-- CHIPS DE INTENÇÃO — submit de verdade, sem script.
           name="intencao" é o mesmo nos três; o value é o que muda. O
           formnovalidate é o que deixa o chip passar com o campo vazio (ver o
           cabeçalho). Três é o número calibrado e dois funcionam; o QUARTO
           começa a competir com a leitura da pergunta, e no celular — onde
           eles empilham de borda a borda — vira uma segunda tela de opções. -->
      <ul class="cnv-chips" data-rv data-d="3">
        <li>
          <button class="cnv-chip" type="submit" name="intencao" value="organizar-carteira" formnovalidate>
            Organizar minha carteira
          </button>
        </li>
        <li>
          <button class="cnv-chip" type="submit" name="intencao" value="planejar-sucessao" formnovalidate>
            Planejar a sucessão
          </button>
        </li>
        <li>
          <button class="cnv-chip" type="submit" name="intencao" value="vender-empresa" formnovalidate>
            Vender minha empresa
          </button>
        </li>
      </ul>

      <!-- ANEXO — ESCRITO E DESLIGADO DE PROPÓSITO. NÃO DESCOMENTE SEM LER.
           O equivalente ao "anexe uma foto" da referência, aqui, seria extrato
           ou print de carteira: dado pessoal SENSÍVEL de terceiros inclusive
           (cônjuge, herdeiros, sócios). Antes de ligar, quatro perguntas
           precisam de resposta escrita, e a ficha.md detalha cada uma:
             1. o site TEM backend para receber arquivo? Um <input type="file">
                num formulário que ninguém processa é um campo que promete e
                descarta — e ainda exige enctype="multipart/form-data" no
                <form>, que este markup não declara justamente por isso;
             2. qual a BASE LEGAL da LGPD para esse tratamento, e ela está
                dita na tela, ao lado do campo, e não no rodapé;
             3. o CANAL é seguro do envio ao armazenamento (TLS, bucket
                privado, acesso nominal) — anexo de carteira em caixa de
                e-mail compartilhada não é;
             4. qual o prazo de RETENÇÃO e quem apaga.
           Enquanto as quatro não tiverem dono, a conversa começa por texto: é
           mais barato pedir o documento depois, por um canal preparado, do que
           guardar o que não se sabe proteger.

      <label class="cnv-anexo" for="cnv-arquivo">
        <svg class="cnv-anexo-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             stroke-width="1.6" stroke-linecap="round" aria-hidden="true" focusable="false">
          <path d="M16 7.5l-6.6 6.6a2.5 2.5 0 003.5 3.5l6.6-6.6a4.5 4.5 0 00-6.4-6.4l-6.6 6.6a6.5 6.5 0 009.2 9.2l5.9-5.9"/>
        </svg>
        <span class="cnv-anexo-txt">Anexar um documento</span>
        <input class="visually-hidden" id="cnv-arquivo" type="file" name="anexo" accept=".pdf">
      </label>
      -->

      <p class="nota cnv-nota" id="cnv-ressalva">
        Esta conversa não é recomendação de investimento e não substitui a
        análise de perfil exigida na abertura de conta. Não escreva dados
        sensíveis — CPF, número de conta, senha ou saldo.
      </p>

    </form>

    <!-- Rodapé de atributos: o que a casa é, em três linhas curtas. Apagável
         sem quebrar a dobra. -->
    <ul class="cnv-atrib" data-rv data-d="3">
      <li>Consultoria de valores mobiliários autorizada</li>
      <li>Modelo fee-based, sem comissão por produto</li>
      <li>Famílias, empresários e holdings familiares</li>
    </ul>

  </div>
</section>

estilo.css · 314 linhas · 14,1 KB

/* ==========================================================================
   HERO COM CAMPO DE CONVERSA — prefixo cnv-
   A primeira dobra é um campo: pergunta grande, caixa de texto com disco de
   envio e três chips de intenção.

   Tokens usados:
     --fg --fg-2 --fg-muted     tinta, pelo escopo .sec-dark da seção
     --rule --rule-2            filete da ressalva e borda dos chips
     --accent                   borda do chip em hover, ponto dos atributos
     --dark-rgb                 o scrim sobre a foto
     --sans                     tipografia do chip
     --radius-pill              forma do chip
     --nav-h                    folga do menu fixo
     --ease                     curva do projeto
   O fundo da seção é --bg, escrito pelo `section{background:var(--bg)}` do
   core; a família da pergunta é --display, escrita pela .display-xl. Nenhum
   dos dois aparece neste arquivo de propósito — quem os lê é o core.

   Primitivas do core que este arquivo NÃO reescreve, só alimenta:
     .display-xl   o desenho da pergunta (parafusos --display-xl/-lh, abaixo)
     .field        rótulo em caixa alta, controle sem caixa, régua que acende
     .btn-disc     o disco de envio (parafuso --disc-size)
     .sec-lead     a frase de apoio
     .nota         a ressalva — aqui só entram margem, alinhamento e filete,
                   que é exatamente o que a primitiva deixou local
     .visually-hidden  o contador que só o leitor de tela recebe
     .grain        o ruído sobre a foto
   ========================================================================== */

.cnv{
  /* PERGUNTA. Alimenta os dois parafusos da .display-xl do core. O teto é
     calibrado pela FRAÇÃO DO BLOCO que a linha mais longa ocupa, não pelo
     número: a 1440 o corpo resolve em 57.6px e "O que você quer resolver"
     mede 660px dentro dos 720px do bloco — 92%. É essa fração que se
     persegue quando a display do tema muda, e a folga de 8% é o que absorve
     uma face mais larga sem estourar a quebra do <br>. Medido: a 4.4vw a
     mesma linha dava 726px, 6px MAIS larga que o campo logo abaixo, e a
     pergunta passava a pender para fora da caixa que ela apresenta. */
  --display-xl:clamp(30px,4vw,60px);
  /* 1.04, e não o .95 padrão da primitiva: aquele serve a título de uma ou
     duas palavras, sem descendente para colidir. Aqui são duas linhas de
     frase inteira, com o "q" de "que" encontrando o "p" de "patrimônio". */
  --display-xl-lh:1.04;

  /* Corpo do que a pessoa escreve. É o maior texto editável da casa de
     propósito: a caixa tem de parecer um lugar de escrever, não um campo de
     cadastro. */
  --cnv-texto:clamp(17px,1.6vw,22px);

  /* Teto de crescimento da caixa. O script cresce a altura até aqui e depois
     deixa rolar por dentro — sem teto, um texto longo empurra os chips e a
     ressalva para fora da tela, e a dobra deixa de ser dobra. O valor é lido
     de volta pelo script em getComputedStyle: é o CSS que manda no limite. */
  --cnv-teto:34vh;

  /* Largura do bloco central: pergunta, apoio, campo e chips. Não é a coluna
     — a coluna continua sendo o .wrap (§4). É a medida de leitura de uma
     pergunta curta com um campo embaixo. */
  --cnv-max:720px;

  position:relative;
  overflow:hidden;
  min-height:100vh;
}

/* ---------------------------------------------------------------- arte */
/* Sangra porque está FORA do .wrap — nenhum margin negativo, nenhum 100vw
   (§4). O bloco inteiro é opcional: sem ele a pergunta lê --fg sobre --bg e o
   contraste é o do escopo. */
.cnv-arte{position:absolute;inset:0;z-index:0;overflow:hidden}
.cnv-arte-img{
  width:100%;height:100%;
  object-fit:cover;object-position:50% 38%;
  /* A foto é atmosfera, não assunto: o que precisa ser lido aqui é a
     pergunta. Em .34 ela some atrás do scrim quando a tela é estreita e
     reaparece quando há espaço — que é o comportamento certo para um fundo. */
  opacity:.34;
  filter:grayscale(.35);
}

/* SCRIM — dois gradientes chapados. Um vertical, que segura o topo (onde
   passa o menu fixo) e o pé (a emenda com a próxima dobra); um radial, que
   escurece o centro, onde moram a pergunta e o campo. Cor por token:
   rgba(var(--dark-rgb),…) lê o token e não inventa canal (§3). */
.cnv-scrim{
  position:absolute;inset:0;
  background:
    radial-gradient(120% 90% at 50% 52%,
      rgba(var(--dark-rgb),.88) 0%,
      rgba(var(--dark-rgb),.62) 46%,
      rgba(var(--dark-rgb),.30) 100%),
    linear-gradient(to bottom,
      rgba(var(--dark-rgb),.78) 0%,
      rgba(var(--dark-rgb),.20) 30%,
      rgba(var(--dark-rgb),.42) 66%,
      rgb(var(--dark-rgb)) 100%);
}

/* VÉU — backdrop-filter SEMPRE com máscara em gradiente (§6). Caixa reta
   produziria uma borda dura atravessando a foto na horizontal, e o olho lê
   isso como "faixa chapada em cima da imagem". A máscara entrega o desfoque
   cheio embaixo — onde está o campo, que é o que precisa de fundo calmo — e
   zero no alto, onde a foto ainda tem de respirar.
   O @supports evita a camada inteira onde backdrop-filter não existe: sem
   ele sobraria um retângulo sem efeito nenhum, e o scrim já basta. */
@supports (backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px)){
  .cnv-veu{
    position:absolute;inset:0;pointer-events:none;
    backdrop-filter:blur(7px) saturate(112%);
    -webkit-backdrop-filter:blur(7px) saturate(112%);
    -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.92) 44%,transparent 100%);
    mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.92) 44%,transparent 100%);
  }
}

/* ---------------------------------------------------------------- corpo */
/* Centrado nos dois eixos, como a referência. O texto do CAMPO não acompanha
   — ver .cnv-campo, que volta a alinhar à esquerda. */
.cnv-body{
  position:relative;z-index:2;
  min-height:100vh;
  display:flex;flex-direction:column;justify-content:center;
  text-align:center;
  padding-top:calc(var(--nav-h) + clamp(28px,5vh,64px));
  padding-bottom:clamp(40px,7vh,88px);
}

/* A pergunta divide a MESMA medida do campo, de propósito: ela é o rótulo da
   caixa que vem embaixo, e uma headline mais larga que o campo faz a dobra ler
   como duas peças empilhadas em vez de uma pergunta com um lugar de responder. */
/* O olho precisa de `align-self`, e a razão foi medida: a primitiva .eyebrow é
   `display:inline-flex`, e um flex item é BLOQUIFICADO — vira `flex` e passa a
   esticar na largura inteira da coluna (1096px a 1440), com o conteúdo dela
   ancorado à esquerda. O `text-align:center` do .cnv-body não alcança: ele
   alinha texto, e ali dentro o que existe é uma caixa flex. O sintoma é o olho
   sozinho na margem esquerda enquanto o resto da dobra está centrado. */
.cnv-olho{align-self:center}

.cnv-title{
  max-width:var(--cnv-max);margin:18px auto 0;
  /* O reset do core dá text-wrap:balance a todo h1..h4, e a .display-xl já o
     devolve a initial. A declaração fica aqui assim mesmo: quem trocar a
     primitiva por uma classe própria perde a quebra do <br> em silêncio, que
     é o defeito mais caro de achar deste arquivo. */
  text-wrap:initial;
}

/* Só a medida e a margem: o desenho é a primitiva .sec-lead do core.
   Seletor duplo de propósito — .cnv-lead e .sec-lead empatam em (0,1,0), e
   sem o ancestral quem venceria seria a folha carregada por último (§6.4). */
.cnv .cnv-lead{
  max-width:50ch;margin:22px auto 0;
  font-size:clamp(16px,1.4vw,19px);
  /* Texto centrado em três linhas deixa a última órfã — "compromisso." sozinho
     no meio da dobra. `balance` reparte as três, e aqui ele é seguro: o reset
     do core só dá balance a h1..h4, e o Chrome desiste do cálculo acima de
     seis linhas, que este apoio nunca alcança (o slot para em 28 palavras). */
  text-wrap:balance;
}

/* ---------------------------------------------------------------- campo */
.cnv-form{
  max-width:var(--cnv-max);
  width:100%;
  margin:clamp(28px,4vh,44px) auto 0;
}

/* O campo é a primitiva .field do core: régua embaixo, rótulo em caixa alta,
   borda que acende no foco. O que sobra para cá é o alinhamento — dentro de
   uma dobra centrada, rótulo e texto digitado continuam à esquerda, porque
   texto que a PESSOA escreve centralizado é ilegível a partir da segunda
   linha. */
.cnv-campo{text-align:left}

/* A linha é o par caixa + disco. align-items:flex-end ancora o disco no pé da
   caixa: quando o texto cresce, o disco desce junto e continua colado à régua,
   em vez de ficar boiando no meio de um campo de cinco linhas. */
.cnv-linha{display:flex;align-items:flex-end;gap:clamp(12px,1.6vw,20px)}

/* Seletor duplo pelo mesmo motivo do .cnv-lead, e aqui ele é obrigatório por
   outra razão: `.field textarea` do core vale (0,1,1) e `.cnv-texto` sozinha
   valeria (0,1,0) — perderia o corpo de 15px da primitiva. Com o ancestral
   são (0,2,0) e a conta fecha sem depender de ordem de folha. */
.cnv .cnv-texto{
  flex:1 1 auto;min-width:0;
  font-size:var(--cnv-texto);
  line-height:1.45;
  /* Sem teto o campo cresce para sempre e empurra chips e ressalva para fora
     da dobra; com teto, o excedente rola por dentro. */
  max-height:var(--cnv-teto);
  overflow-y:auto;
  /* SEM SCRIPT a alça nativa é a única forma de ver um texto longo inteiro —
     por isso ela nasce ligada. Quem a desliga é a regra logo abaixo, e só
     depois de o script assumir o crescimento. */
  resize:vertical;
}

/* O script põe .is-auto na raiz quando assume a altura. As duas coisas não
   podem conviver: a alça guarda uma altura que o próximo caractere digitado
   sobrescreveria, e a pessoa veria o campo "desfazer" o que ela arrastou. */
.cnv.is-auto .cnv-texto{resize:none}

/* Disco de envio: a primitiva .btn-disc do core, com o parafuso dela. 52px e
   não os 42 do padrão porque aqui ele não é seta de carrossel — é o botão
   principal da primeira dobra, e divide a linha com um texto de 22px. */
.cnv-enviar{--disc-size:clamp(44px,4vw,52px);margin-bottom:2px}
.cnv-seta{width:19px;height:19px}

/* ---------------------------------------------------------------- chips */
.cnv-chips{
  display:flex;flex-wrap:wrap;justify-content:center;
  gap:10px;
  margin-top:20px;
}

/* `background:none` e `appearance:none` não são decoração: o <button> nativo
   pinta o cromo do sistema por baixo e `color-scheme:only light` (tokens.css)
   o deixa CINZA-CLARO em toda parte. Sem as duas linhas, o chip fica com
   fundo #f0f0f0 sob texto quase branco. O core registra o mesmo caso na
   primitiva .btn, medido no conversao/diagnostico-perfil. */
.cnv-chip{
  background:none;-webkit-appearance:none;appearance:none;
  font-family:var(--sans);font-weight:400;
  font-size:13.5px;line-height:1.3;letter-spacing:.01em;
  color:var(--fg-2);
  border:1px solid var(--rule-2);
  border-radius:var(--radius-pill);
  padding:11px 18px;
  transition:color .3s var(--ease),border-color .3s var(--ease),background-color .3s var(--ease);
}
/* Sem deslocamento e sem magnetismo, como a primitiva .btn da casa: o que
   muda no hover é a tinta e a borda. Um chip que salta obriga o olho a
   reencontrar os outros dois. */
.cnv-chip:hover{
  color:var(--fg);
  border-color:var(--accent);
  background-color:color-mix(in srgb,var(--accent) 9%,transparent);
}

/* ---------------------------------------------------------------- anexo */
/* Desligado no markup, e o CSS fica aqui de propósito: quem descomentar o
   bloco não precisa reescrever a forma, só responder às quatro perguntas da
   LGPD que o comentário do markup.html lista. O <input type="file"> real vive
   escondido pela .visually-hidden do core — fora da tela, dentro da árvore de
   acessibilidade e focável pelo teclado. */
.cnv-anexo{
  display:inline-flex;align-items:center;gap:9px;
  margin-top:16px;
  font-size:13px;color:var(--fg-muted);
  cursor:pointer;
  transition:color .3s var(--ease);
}
.cnv-anexo:hover{color:var(--fg-2)}
.cnv-anexo-ico{width:15px;height:15px;flex:0 0 auto}

/* ---------------------------------------------------------------- ressalva */
/* A primitiva .nota do core entrega o trio tipográfico (12.5px, entrelinha
   1.6, --fg-muted). Daqui sai o que ela deixou local: margem, alinhamento e
   filete. À esquerda e não centrada — três linhas de ressalva centradas viram
   um triângulo que ninguém lê até o fim. */
.cnv-nota{
  --nota-max:none;
  text-align:left;
  margin-top:22px;
  padding-top:16px;
  border-top:1px solid var(--rule);
}

/* ---------------------------------------------------------------- atributos */
.cnv-atrib{
  display:flex;flex-wrap:wrap;justify-content:center;
  gap:10px clamp(20px,3vw,40px);
  margin-top:clamp(30px,5vh,52px);
}
.cnv-atrib li{
  display:flex;align-items:center;gap:9px;
  font-size:12.5px;line-height:1.5;letter-spacing:.02em;
  color:var(--fg-muted);
}
.cnv-atrib li::before{
  content:"";flex:0 0 auto;
  width:4px;height:4px;border-radius:50%;
  background:var(--accent);
}

/* ---------------------------------------------------------------- mobile */
@media(max-width:760px){
  /* 100vh no celular conta com a barra do navegador recolhida: a dobra fica
     alta demais e os chips caem abaixo da linha d'água — o único erro fatal
     possível num hero cuja ação é o campo. Deixar a altura ser o conteúdo é o
     que garante o campo à vista. Mesma correção do hero/centrado-prova-social. */
  .cnv{min-height:0}
  .cnv-body{
    min-height:0;
    padding-top:calc(var(--nav-h) + 30px);
    padding-bottom:52px;
  }
  .cnv .cnv-lead{max-width:40ch}
  /* A caixa perde metade do teto: com o teclado virtual aberto sobra cerca de
     45% da tela, e um campo de 34vh empurraria o disco de envio para debaixo
     do teclado. */
  .cnv{--cnv-teto:24vh}
  /* Os chips passam a caber um por linha e ficam de borda a borda do bloco:
     três pílulas de 13.5px em 390px quebram em duas linhas desalinhadas, e a
     fileira centrada vira escada. */
  .cnv-chips{flex-direction:column;align-items:stretch}
  .cnv-chip{width:100%;padding:13px 18px}
  .cnv-atrib{
    /* Em coluna, alinhados à esquerda: três linhas centradas com o ponto na
       frente desenham uma seta para o nada. */
    flex-direction:column;align-items:flex-start;
    text-align:left;
  }
}

script.js · 149 linhas · 7,0 KB

/* ==========================================================================
   HERO COM CAMPO DE CONVERSA — melhoria progressiva, e só isso.

   >>> O ENVIO NÃO PASSA POR AQUI. <<<
   Este arquivo não escuta submit, não valida, não monta requisição e não
   trava o formulário. O <form action method="post"> do markup é de verdade e
   quem envia é o NAVEGADOR — inclusive os três chips, que são
   <button type="submit" name="intencao" value="…"> e levam o próprio par
   nome/valor no envio sem uma linha de JavaScript. É a diferença desta peça
   para o conversao/cta-formulario-lead, cujo script finge um envio que não
   acontece; tirar este arquivo do ar não perde nenhuma conversa.

   O que ele faz são duas melhorias, as duas invisíveis quando não existem:

     1. A CAIXA CRESCE com o texto, até o teto declarado em --cnv-teto (CSS).
        Sem o script a caixa fica nas duas linhas do `rows` e a alça nativa de
        redimensionar continua lá, que é o motivo de ela nascer ligada no CSS.

     2. O CONTADOR ANUNCIA quando falta pouco para o `maxlength`, e só então.
        A região é aria-live="polite" dentro de .visually-hidden: ninguém a vê,
        e quem usa leitor de tela ouve "Faltam 40 caracteres" em vez de nada e
        depois um campo que simplesmente para de aceitar texto.

   POR QUE NÃO ANUNCIAR A CADA TECLA. Uma região viva que muda a cada
   caractere produz fala contínua sobre o que a pessoa está digitando — o
   leitor de tela passa a disputar com o próprio eco do teclado e a digitação
   fica impossível. Daí os dois freios: só abaixo de AVISO caracteres
   restantes, e só de PASSO em PASSO.

   MOVIMENTO REDUZIDO não tem o que desligar aqui: a altura muda sem
   transição, no mesmo quadro da tecla, porque campo que cresce animado
   atrasa o cursor em relação ao que se digita. Não há nada andando sozinho, e
   por isso o arquivo não consulta LIB.reduce.

   A COPY MORA NO MARKUP, como no LIB.pausavel do core: as duas frases do
   contador vêm de data-cnv-rot-faltam / data-cnv-rot-limite, com {n} no lugar
   do número. Nenhum redator vai procurar texto dentro de um .js.
   ========================================================================== */

(function () {

  /* Abaixo de quantos caracteres restantes o contador começa a falar, e de
     quanto em quanto ele volta a falar depois disso. */
  var AVISO = 60;
  var PASSO = 20;

  /* Parte da raiz do componente e aceita mais de uma na mesma página (§7.6) —
     raro num hero, comum numa página de demonstração da biblioteca. */
  var raizes = document.querySelectorAll('[data-cnv]');
  if (!raizes.length) return;                                   // guard

  Array.prototype.forEach.call(raizes, function (raiz) {
    var campo = raiz.querySelector('.cnv-texto');
    if (!campo) return;                                         // guard

    var conta = raiz.querySelector('[data-cnv-conta]');
    var balde = -1;          // último degrau de PASSO já anunciado

    /* ------------------------------------------------------ crescer */
    /* O teto é lido do CSS, não cravado aqui: quem decide o limite é
       --cnv-teto, e o mobile já o baixa na media query. Ler de volta é o que
       impede o número de existir em dois lugares e divergir. */
    function crescer() {
      campo.style.height = 'auto';
      var teto = parseFloat(getComputedStyle(campo).maxHeight);
      var altura = campo.scrollHeight;
      if (teto && altura > teto) altura = teto;
      campo.style.height = altura + 'px';
    }

    /* ------------------------------------------------------ contador */
    function anunciar() {
      if (!conta) return;
      var limite = campo.maxLength;
      if (!limite || limite < 0) return;

      var restam = limite - campo.value.length;

      /* Longe do teto não há o que dizer — e apagar a região importa: sem
         isto o último aviso fica pendurado e um leitor de tela pode relê-lo
         ao reentrar no campo, dizendo "faltam 20" com o campo vazio. */
      if (restam > AVISO) {
        if (conta.textContent) conta.textContent = '';
        balde = -1;
        return;
      }

      var degrau = Math.floor(restam / PASSO);
      if (degrau === balde) return;
      balde = degrau;

      /* `restam` MENOR que zero é caso real, e foi medido: o `maxlength` só
         limita o que se digita ou cola, não o que chega por outro caminho —
         valor escrito por script, restauração de sessão, preenchimento
         automático do navegador. Sem o `<= 0` o anúncio saía "Faltam -228
         caracteres", que é pior que silêncio. */
      var rotulo = restam <= 0
        ? (conta.getAttribute('data-cnv-rot-limite') || '')
        : (conta.getAttribute('data-cnv-rot-faltam') || '');
      conta.textContent = rotulo.replace('{n}', restam <= 0 ? limite : restam);
    }

    function aoDigitar() { crescer(); anunciar(); }

    campo.addEventListener('input', aoDigitar);

    /* O CSS só desliga a alça de redimensionar DEPOIS que o script assume a
       altura — antes disso ela é a única forma de ver um texto longo inteiro. */
    raiz.classList.add('is-auto');
    crescer();

    /* Largura nova muda a quebra de linha e, com ela, a altura necessária.
       Guarda a largura para não recalcular em toda mudança de altura — no
       celular, abrir o teclado virtual dispara resize sem mudar a largura, e
       recalcular ali faria a caixa pular no exato momento em que a pessoa vai
       digitar. {passive:true} porque este ouvinte não cancela nada (§7.3). */
    var larguraAntes = window.innerWidth;
    addEventListener('resize', function () {
      if (window.innerWidth === larguraAntes) return;
      larguraAntes = window.innerWidth;
      crescer();
    }, { passive: true });

    /* ------------------------------------------------------ chips, rota 2 */
    /* Por padrão o chip SUBMETE — é a rota sem JavaScript, e é a boa quando o
       destino é uma página de conversa que já sabe o que fazer com
       `intencao`. Quem prefere que o chip apenas PREENCHA o campo, para a
       pessoa completar a frase antes de enviar, declara no markup:

         <section class="cnv sec-dark" data-cnv data-cnv-chips="preencher">

       Aí o clique é interceptado, o texto do chip vira o começo da mensagem e
       o foco vai para o fim do que foi escrito. Sem script, o mesmo markup
       volta a submeter: nenhuma das duas rotas depende da outra. */
    if (raiz.getAttribute('data-cnv-chips') !== 'preencher') return;

    Array.prototype.forEach.call(raiz.querySelectorAll('.cnv-chip'), function (chip) {
      chip.addEventListener('click', function (e) {
        e.preventDefault();
        campo.value = chip.textContent.trim() + ': ';
        campo.focus();
        /* O cursor vai para o FIM do texto colado, senão ele fica no início e
           a pessoa escreve antes da intenção que acabou de escolher. */
        campo.setSelectionRange(campo.value.length, campo.value.length);
        aoDigitar();
      });
    });
  });
})();

A primeira dobra não é um botão: é um campo. Olho, pergunta em duas linhas, uma frase de apoio e, logo abaixo, uma caixa de texto grande com um disco de envio ancorado no canto e três chips de intenção embaixo — "Organizar minha carteira", "Planejar a sucessão", "Vender minha empresa". Sob o campo, a ressalva. Quem já sabe o que quer, escreve; quem não sabe, clica num chip e cai na mesma conversa com o assunto já declarado. O formulário longo fica para depois, quando houver motivo para pedir nome e telefone.

O que sustenta a dobra é ela funcionar sem JavaScript. O bloco é um <form method="post"> de verdade: o disco é type="submit" e cada chip é um <button type="submit" name="intencao" value="…"> — o navegador monta o envio com o par nome/valor do botão acionado, e foi assim que se mediu, com o JavaScript desligado. O script.js faz duas melhorias e nada mais: cresce a altura da caixa conforme o texto e anuncia o contador quando falta pouco para o maxlength. Tire o script e a dobra continua inteira; ele não escuta submit, não valida e não trava nada.


Compliance

Três coisas, e as três são forma, não só texto.

1. A ressalva é slot exigido. É o único slot obrigatório desta dobra, e diz três coisas nesta ordem: a conversa não é recomendação de investimento; não substitui a análise de perfil exigida na abertura de conta; e não se escreve dado sensível ali — CPF, número de conta, senha, saldo. Ela veste a primitiva .nota do core, fica ao lado do campo, onde a pessoa escreve, e não no rodapé — e o aria-describedby do <textarea> aponta para ela, de modo que quem usa leitor de tela recebe o aviso ao entrar no campo, não depois de enviar. Medido: a ressalva sai em --fg-muted a 12.5px com 5.74:1 sobre o pior pixel do fundo com foto e 6.05:1 sem foto — acima do mínimo de 4.5:1 para texto normal.

Um campo aberto numa primeira dobra é um convite a escrever o que vier. Sem a ressalva, uma porta de conversa vira captação de dado sensível sem aviso, e a resposta do assessor do outro lado vira, na leitura de quem escreveu, uma recomendação.

2. O anexo fica desligado, e está escrito por quê. O original anexa uma foto; o equivalente aqui seria extrato ou carteira — dado pessoal sensível, de terceiros inclusive (cônjuge, herdeiros, sócios). O <label class="cnv-anexo"> com o <input type="file"> está escrito e comentado no markup.html, com quatro perguntas que precisam de resposta antes de descomentar:

Pergunta Por que ela trava o campo
O site tem backend para receber arquivo? Um <input type="file"> num formulário que ninguém processa é um campo que promete e descarta. E exige enctype="multipart/form-data" no <form>, que este markup não declara justamente por isso.
Qual a base legal da LGPD? Consentimento ou legítimo interesse mudam o que a tela precisa dizer, e o aviso vai ao lado do campo, não no rodapé.
O canal é seguro do envio ao armazenamento? TLS, bucket privado, acesso nominal. Anexo de carteira caindo numa caixa de e-mail compartilhada do comercial não é.
Qual o prazo de retenção, e quem apaga? Dado que ninguém sabe apagar é dado guardado para sempre.

Enquanto as quatro não tiverem dono, a conversa começa por texto: é mais barato pedir o documento depois, por um canal preparado, do que guardar o que não se sabe proteger. Está como variação experimental no componente.json de propósito.

3. method="post", nunca get. Com get, o texto escrito vai para a barra de endereço, para o histórico do navegador, para o Referer do próximo pedido e para o log do servidor. Numa assessoria, a primeira frase costuma trazer o assunto patrimonial inteiro — "vou vender minha empresa em 2027". Isso não pode virar URL. E a copy de exemplo não promete retorno, não cita rentabilidade e não nomeia instituição: os três chips falam de organizar, planejar e vender, que são serviços, não resultados.


Quando usar

Quando não usar

Nenhum dos outros cinco heroes da casa é "campo primeiro", e é essa a vaga que este ocupa: os cinco terminam num <a class="btn"> que leva a pessoa para outro lugar. Aqui a ação acontece na própria dobra.


Padrão editorial

Slot Regra
Olho 2 a 4 palavras. O que a casa é, não o que promete: "Assessoria de investimentos". A promessa é a pergunta. Opcional.
Pergunta (h1) 5 a 9 palavras, uma pergunta aberta terminada em "?", quebrada em duas linhas por <br> no markup. Enunciado que não é pergunta deixa a caixa sem assunto. A linha mais longa tem de caber em ~92% de --cnv-max — medido: 660px dentro de 720 a 1440.
Apoio 14 a 28 palavras. O que acontece depois de enviar: quem lê, em quanto tempo, e quanto custa, mesmo que seja nada. Não repete a pergunta.
Rótulo do campo 1 a 3 palavras. Sai em caixa alta de 10.5px pela primitiva .field. Não é instrução — a instrução é o placeholder.
Exemplo (placeholder) 7 a 12 palavras, um caso concreto na voz de quem escreve: "herdei uma carteira e não sei por onde começar". Nunca "digite sua mensagem". O exemplo ensina o tamanho e o tom da resposta esperada. Sem número, sem valor, sem nome de instituição. Teto medido: ~60 caracteres — acima disso a frase não cabe nas duas linhas do campo a 390px e corta no meio.
Rótulo do disco (aria-label) 2 a 7 palavras. O disco não tem texto visível: é a única coisa que o leitor de tela anuncia. Diz a ação e o destino.
Chips 2 a 4 palavras cada, três (dois funcionam). Primeira pessoa de quem chega, em paralelismo: verbo no infinitivo + objeto nos três. O value vai em kebab-case, é o que o destino roteia e não é copy.
Ressalva 25 a 45 palavras. Exigida. Ver Compliance.
Frases do contador Duas, em data-cnv-rot-faltam e data-cnv-rot-limite, com {n} no lugar do número. Só o leitor de tela as recebe.
Atributos do rodapé 3 itens de 4 a 7 palavras, do mesmo tamanho — a fileira é centrada e um item longo desequilibra a linha. Fato verificável (registro, modelo de remuneração, público), nunca adjetivo e nunca número de resultado. Opcional.

O que mantém a dobra coesa é a pergunta e os chips serem a mesma frase em dois níveis de esforço: a pergunta é aberta, os chips são as três respostas mais comuns a ela. Se um chip não responde à pergunta do h1, ele vira um menu, e aí a caixa de texto passa a parecer o campo "outros".

Especificação dos assets

Um só, e opcional: a foto de fundo.


Como usar

<link rel="stylesheet" href="componentes/hero/campo-de-conversa/estilo.css">
<script src="componentes/hero/campo-de-conversa/script.js" defer></script>

Do core precisa de tokens.css, base.css (pelas primitivas .wrap, .field, .btn-disc, .nota, .visually-hidden, .display-xl, .sec-lead, .eyebrow, .grain) e do par motion.css/motion.js, que é quem faz a entrada.

Antes de publicar, três linhas do markup:

  1. action="conversa" → o endpoint real. Ele tem de tratar dois envios: o do disco (mensagem preenchida) e o dos chips (mensagem vazia + intencao).
  2. O placeholder e os três value dos chips, que são o vocabulário da casa.
  3. A ressalva, revisada por quem responde por compliance no cliente.

Ganchos

Gancho Papel
[data-cnv] A seção. O script parte daqui, nunca do documento, e aceita mais de uma na página.
[data-cnv-chips="preencher"] Na seção. Liga a variação em que o chip preenche o campo em vez de submeter.
[data-cnv-form] No <form>. Marcação de leitura: o script não escuta submit.
[data-cnv-conta] A região viva do contador, com data-cnv-rot-faltam e data-cnv-rot-limite. {n} vira o número.
.cnv.is-auto Estado que o script põe ao assumir a altura da caixa. É o que desliga a alça nativa de redimensionar.
.cnv-arte / .cnv-arte-img / .cnv-scrim / .cnv-veu A arte de fundo, em bloco apagável.
.cnv-title / .cnv-lead / .cnv-olho Pergunta, apoio e olho.
.cnv-form / .cnv-slot / .cnv-campo / .cnv-linha O formulário, o invólucro de data-rv, o .field e a linha caixa + disco.
.cnv-texto / .cnv-enviar / .cnv-seta A caixa, o disco (.btn-disc) e o ícone.
.cnv-chips / .cnv-chip A fileira e cada atalho.
.cnv-anexo / .cnv-anexo-ico / .cnv-anexo-txt Escritos no CSS, comentados no markup. Ver Compliance.
.cnv-nota / .cnv-atrib A ressalva e o rodapé de atributos.

Parafusos

Variável Padrão O que faz
--display-xl clamp(30px,4vw,60px) Corpo da pergunta. Parafuso da primitiva .display-xl, alimentado na raiz. Persiga a fração (~92% de --cnv-max na linha mais longa), não o número.
--display-xl-lh 1.04 Entrelinha da pergunta. O .95 padrão da primitiva serve a título de uma ou duas palavras, sem descendente para colidir.
--cnv-texto clamp(17px,1.6vw,22px) Corpo do texto editável. É o maior texto editável da casa de propósito.
--cnv-teto 34vh (24vh no mobile) Teto de crescimento da caixa. O script lê este valor de volta do CSS — mudar aqui muda o limite real.
--cnv-max 720px Medida do bloco central: pergunta, apoio, campo e chips. Não é a coluna; a coluna continua sendo o .wrap.
--disc-size clamp(44px,4vw,52px) no .cnv-enviar Parafuso da primitiva .btn-disc, que traz 42px. Aqui o disco não é seta de carrossel: é o botão principal da dobra.
--nota-size / --nota-max 12.5px / none aqui Parafusos da primitiva .nota. A medida é livre porque quem limita a linha é o bloco de 720px.

Variações


Gotchas


Movimento reduzido / toque

Movimento reduzido desliga tudo pelo core: medido com --reduce, os seis nós de entrada ficam em opacity:1, filter:none, transform:none. Não há o que desligar no script — a caixa cresce sem transição, no mesmo quadro da tecla, porque campo que cresce animado atrasa o cursor em relação ao que se digita. Por isso o script.js não consulta LIB.reduce: não há nada andando sozinho.

Toque. Nada depende de hover: o hover do chip é acabamento (tinta e borda), e os três alvos — caixa, disco e chips — nascem visíveis e clicáveis. No celular os chips ficam com 46px de altura e largura cheia, e o disco com 44px, que é o piso de alvo de toque. O <textarea> abre o teclado nativo, e é por isso que o teto cai para 24vh: o que não pode acontecer é o disco de envio ficar debaixo do teclado.

Teclado. A ordem de tabulação é a ordem da leitura: caixa → disco → chip 1 → chip 2 → chip 3. Cada um é um <button> de verdade, com Enter e Espaço nativos. Dentro da caixa, Enter quebra linha — não envia —, o que é o comportamento certo para um campo em que se escreve uma frase inteira.


Histórico

Projeto Onde Data Diferenças
referência: skinboost-design-review.vercel.app dobra #inicio — hero com campo e chips 2026-09-21 referência visual, não origem de código

Origem. Escrito do zero a partir da observação da primeira dobra de skinboost-design-review.vercel.app. O original roda three.js, GSAP/ScrollTrigger, ícones Phosphor e assets de Figma, e nada disso atravessa (§11): a stack da casa é HTML+CSS+JS vanilla, sem lib. O que veio de lá foi a ordem de leitura — pergunta, campo, atalhos — e a proporção entre a headline e a caixa. A referência está registrada em projetos/referencias-externas.md.

O que mudou na tradução:

Divergências conhecidas vs. a referência: