← Biblioteca componentes/conversao/contato-escritorios/ficha.md CódigoTela cheia
conversao/contato-escritorios

Contato com canais e escritórios

🟢 estavel complexidade média prefixo ctt- contato endereco escritorio whatsapp horario canais institucional

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

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

markup.html · 91 linhas · 4,5 KB

<!-- ==========================================================================
     CONTATO COM ESCRITÓRIOS — os canais de um lado, os endereços do outro
     Prefixo: ctt-
     Requer: core (tokens, base, motion.css, motion.js) + este estilo.css e
             este script.js (opcional: só concede o "aberto agora")

     Para quem JÁ decidiu falar. À esquerda, os canais em corpo grande — cada
     linha é um link de verdade (wa.me, mailto:, tel:) que abre o aplicativo
     certo no celular. À direita, um card por escritório: cidade, endereço
     em <address>, telefone, horário e o link para o mapa.

     "ABERTO AGORA" É CONCEDIDO, NÃO ESCRITO. Cada escritório declara o
     horário (data-ctt-horario) e o fuso (data-ctt-fuso), e o script lê o
     relógio NAQUELE fuso — não no de quem olha: o escritório de São Paulo
     está aberto às 10h de São Paulo, mesmo para quem abre a página em
     Lisboa. Sem JavaScript, nenhum selo: a peça nunca diz "aberto" sem
     poder conferir. Mesmo método do dados/cards-cotacoes.

     NENHUM MAPA EMBUTIDO. Iframe de mapa carrega rastreador de terceiro,
     pede cookie e a CSP do site o bloqueia. O "Como chegar" é um link — o
     celular abre o aplicativo de mapa que a pessoa já usa.
     ========================================================================== -->

<section class="sec-deep sec-pad ctt" data-ctt>
  <div class="wrap ctt-grade">

    <div class="ctt-canais-col">
      <span class="eyebrow" data-rv>Contato</span>
      <h2 class="sec-title ttl">Fale com <em>a gente</em></h2>
      <p class="sec-lead" data-rv data-d="1">Uma conversa de meia hora, sem compromisso. Respondemos em até um dia útil.</p>

      <ul class="ctt-canais" data-rv data-d="2">
        <li>
          <a class="ctt-canal" href="https://wa.me/5511900000000" rel="noopener" target="_blank">
            <span class="ctt-canal-rot">WhatsApp</span>
            <span class="ctt-canal-val">(11) 90000-0000</span>
            <svg class="ctt-canal-seta" width="16" height="16" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 10L10 4M5 4h5v5"/></svg>
          </a>
        </li>
        <li>
          <a class="ctt-canal" href="mailto:contato@exemplo.invalid">
            <span class="ctt-canal-rot">E-mail</span>
            <span class="ctt-canal-val">contato@exemplo.invalid</span>
            <svg class="ctt-canal-seta" width="16" height="16" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 10L10 4M5 4h5v5"/></svg>
          </a>
        </li>
        <li>
          <a class="ctt-canal" href="tel:+551130000000">
            <span class="ctt-canal-rot">Telefone</span>
            <span class="ctt-canal-val">(11) 3000-0000</span>
            <svg class="ctt-canal-seta" width="16" height="16" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 10L10 4M5 4h5v5"/></svg>
          </a>
        </li>
      </ul>
    </div>

    <div class="ctt-escritorios">

      <div class="ctt-slot" data-rv data-d="1">
        <article class="ctt-card" data-ctt-horario="1-5 09:00-18:00" data-ctt-fuso="America/Sao_Paulo">
          <div class="ctt-card-topo">
            <h3 class="ctt-cidade">São Paulo</h3>
            <span class="ctt-estado" data-ctt-estado></span>
          </div>
          <address class="ctt-endereco">
            Av. Brigadeiro Faria Lima, 0000, 14º andar<br>
            Itaim Bibi · 04538-000
          </address>
          <p class="ctt-horario">Segunda a sexta, 9h às 18h</p>
          <a class="ctt-mapa" href="#mapa-sao-paulo">Como chegar</a>
        </article>
      </div>

      <div class="ctt-slot" data-rv data-d="2">
        <article class="ctt-card" data-ctt-horario="1-5 09:00-18:00" data-ctt-fuso="America/Sao_Paulo">
          <div class="ctt-card-topo">
            <h3 class="ctt-cidade">Porto Alegre</h3>
            <span class="ctt-estado" data-ctt-estado></span>
          </div>
          <address class="ctt-endereco">
            Rua Padre Chagas, 000, sala 801<br>
            Moinhos de Vento · 90570-000
          </address>
          <p class="ctt-horario">Segunda a sexta, 9h às 18h</p>
          <a class="ctt-mapa" href="#mapa-porto-alegre">Como chegar</a>
        </article>
      </div>

    </div>
  </div>
</section>

estilo.css · 81 linhas · 3,4 KB

/* ==========================================================================
   CONTATO COM ESCRITÓRIOS — ctt-
   Canais em corpo grande de um lado, um card por escritório do outro.
   Tokens usados: --bg --fg --fg-2 --fg-muted --rule --rule-2 --accent
                  --accent-ink --display --radius-media --radius-pill --ease
   ========================================================================== */

.ctt-grade{
  display:grid;grid-template-columns:minmax(0,6fr) minmax(0,5fr);
  gap:clamp(36px,6vw,96px);align-items:start;
}

/* ---------------------------------------------------------------- canais
   Cada linha é um link inteiro: rótulo pequeno, valor em corpo de display e
   a seta. É o que se toca no celular, então a linha toda é o alvo. */
.ctt-canais{margin-top:clamp(30px,5vh,48px);border-top:1px solid var(--rule)}
.ctt-canal{
  display:grid;grid-template-columns:110px minmax(0,1fr) auto;
  align-items:center;gap:16px;
  padding:20px 0;border-bottom:1px solid var(--rule);
  transition:border-color .3s;
}
.ctt-canal-rot{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--fg-muted)}
.ctt-canal-val{
  font-family:var(--display);font-weight:700;
  font-size:clamp(18px,1.9vw,26px);letter-spacing:-.018em;
  color:var(--fg);overflow-wrap:anywhere;
  transition:color .25s;
}
.ctt-canal-seta{color:var(--fg-muted);transition:color .25s}
/* Hover só troca cor (CLAUDE.md §6). */
.ctt-canal:hover{border-color:var(--rule-2)}
.ctt-canal:hover .ctt-canal-val,
.ctt-canal:hover .ctt-canal-seta{color:var(--accent-ink,var(--accent))}
.ctt-canal:focus-visible{outline:2px solid var(--accent);outline-offset:4px}

/* ---------------------------------------------------------------- escritórios */
.ctt-escritorios{display:grid;gap:18px}
.ctt-card{
  display:grid;gap:14px;
  padding:clamp(24px,2.6vw,32px);
  border:1px solid var(--rule);border-radius:var(--radius-media);
  background:color-mix(in srgb,var(--fg) 3%,var(--bg));
}
.ctt-card-topo{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}
.ctt-cidade{
  font-family:var(--display);font-weight:700;font-size:22px;letter-spacing:-.02em;color:var(--fg);
}
.ctt-endereco{font-style:normal;font-size:15px;line-height:1.65;color:var(--fg-2)}
.ctt-horario{font-size:13px;color:var(--fg-muted)}
.ctt-mapa{
  justify-self:start;
  font-family:var(--display);font-weight:700;font-size:10.5px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--fg-2);
  border-bottom:1px solid var(--rule-2);padding-bottom:3px;
  transition:color .25s,border-color .25s;
}
.ctt-mapa:hover{color:var(--accent-ink,var(--accent));border-color:currentColor}

/* O ESTADO é escrito pelo script, e só por ele. Vazio, o span não ocupa
   nada: sem JavaScript o card não diz "aberto" nem "fechado". O texto diz o
   estado por extenso — a cor do ponto repete, nunca substitui. */
.ctt-estado:empty{display:none}
.ctt-estado{
  display:inline-flex;align-items:center;gap:8px;
  font-size:12px;letter-spacing:.04em;color:var(--fg-muted);
}
.ctt-estado::before{
  content:"";width:7px;height:7px;border-radius:50%;
  background:var(--rule-2);
}
.ctt-card[data-aberto="sim"] .ctt-estado{color:var(--fg)}
.ctt-card[data-aberto="sim"] .ctt-estado::before{background:var(--accent)}

@media(max-width:900px){
  .ctt-grade{grid-template-columns:1fr}
}
@media(max-width:520px){
  .ctt-canal{grid-template-columns:minmax(0,1fr) auto;row-gap:4px;column-gap:12px}
  .ctt-canal-rot{grid-column:1 / -1}
}

script.js · 91 linhas · 3,8 KB

/* ==========================================================================
   CONTATO COM ESCRITÓRIOS — ctt-
   Concede o estado de cada escritório ("Aberto agora · fecha às 18h",
   "Fechado · abre amanhã às 9h") lendo o relógio NO FUSO DO ESCRITÓRIO.

   O horário vem em data-ctt-horario, uma ou mais regras separadas por ";":
       "1-5 09:00-18:00"                 segunda a sexta
       "1-5 09:00-18:00; 6 09:00-12:00"  e sábado de manhã
   Dias de 0 (domingo) a 6 (sábado). O fuso em data-ctt-fuso, no formato
   IANA ("America/Sao_Paulo"). Regra ilegível: nenhum selo — a peça não
   afirma o que não consegue conferir.

   A HORA NO FUSO vem do core (LIB.relogio, core/motion.js). Era uma função
   local, copiada ao caractere no conversao/cta-com-assessor; a terceira
   cópia (hero/letreiro-marca) foi o gatilho do §6, e o relógio subiu. A
   REGRA — o que conta como aberto, o texto do selo — continua aqui.

   Não anima nada; reavalia a cada minuto.
   ========================================================================== */
(function () {
  var cards = document.querySelectorAll('[data-ctt] [data-ctt-horario]');
  if (!cards.length) return;                   // guard: sem o componente, saída silenciosa
  if (!window.LIB || !LIB.relogio) return;     // core antigo: nenhum selo

  var DIAS = ['domingo', 'segunda', 'terça', 'quarta', 'quinta', 'sexta', 'sábado'];

  function minutos(hhmm) {
    var m = /^(\d{1,2}):(\d{2})$/.exec(hhmm);
    return m ? (+m[1]) * 60 + (+m[2]) : NaN;
  }
  /* 540 -> "9h"; 1110 -> "18h30" */
  function hora(min) {
    var h = Math.floor(min / 60), m = min % 60;
    return h + 'h' + (m ? (m < 10 ? '0' : '') + m : '');
  }

  /* "1-5 09:00-18:00; 6 09:00-12:00" -> [{dias:[1..5], ini, fim}, ...] */
  function ler(texto) {
    var regras = [];
    (texto || '').split(';').forEach(function (parte) {
      var m = /^\s*(\d)(?:-(\d))?\s+(\d{1,2}:\d{2})-(\d{1,2}:\d{2})\s*$/.exec(parte);
      if (!m) return;
      var de = +m[1], ate = m[2] ? +m[2] : de, dias = [];
      for (var d = de; d <= ate; d++) dias.push(d);
      var ini = minutos(m[3]), fim = minutos(m[4]);
      if (!isNaN(ini) && !isNaN(fim) && fim > ini) regras.push({ dias: dias, ini: ini, fim: fim });
    });
    return regras;
  }

  function estado(regras, agora) {
    var i, r;
    for (i = 0; i < regras.length; i++) {
      r = regras[i];
      if (r.dias.indexOf(agora.dia) >= 0 && agora.min >= r.ini && agora.min < r.fim) {
        return { aberto: true, texto: 'Aberto agora · fecha às ' + hora(r.fim) };
      }
    }
    /* Fechado: procura a próxima abertura, de hoje até daqui a uma semana. */
    for (var passo = 0; passo <= 7; passo++) {
      var dia = (agora.dia + passo) % 7, melhor = null;
      for (i = 0; i < regras.length; i++) {
        r = regras[i];
        if (r.dias.indexOf(dia) < 0) continue;
        if (passo === 0 && r.ini <= agora.min) continue;
        if (melhor === null || r.ini < melhor) melhor = r.ini;
      }
      if (melhor !== null) {
        var quando = passo === 0 ? '' : (passo === 1 ? 'amanhã ' : DIAS[dia] + ' ');
        return { aberto: false, texto: 'Fechado · abre ' + quando + 'às ' + hora(melhor) };
      }
    }
    return null;
  }

  function avaliar() {
    [].forEach.call(cards, function (card) {
      var alvo = card.querySelector('[data-ctt-estado]');
      if (!alvo) return;
      var regras = ler(card.getAttribute('data-ctt-horario'));
      var agora = LIB.relogio(card.getAttribute('data-ctt-fuso') || 'America/Sao_Paulo');
      var e = regras.length && agora ? estado(regras, agora) : null;
      if (!e) { alvo.textContent = ''; card.removeAttribute('data-aberto'); return; }
      alvo.textContent = e.texto;
      card.setAttribute('data-aberto', e.aberto ? 'sim' : 'nao');
    });
  }

  avaliar();
  setInterval(avaliar, 60000);
})();

A dobra para quem já decidiu falar. À esquerda, os canais em corpo grande — WhatsApp, e-mail, telefone —, cada linha um link inteiro que abre o aplicativo certo no celular. À direita, um card por escritório: cidade, endereço, horário e "Como chegar". E em cada card, quando há JavaScript, um selo que diz "Aberto agora · fecha às 18h" ou "Fechado · abre amanhã às 9h" — lido no relógio do fuso do escritório.

O problema que ela resolve: a pessoa que chega à página de contato não quer ser convencida, quer um número. O formulário (conversao/cta-formulario-lead) é para quem ainda está decidindo; esta dobra é o caminho curto para quem já decidiu. E o selo responde a pergunta que vem logo antes de ligar: tem alguém lá agora?


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Título 2 a 6 palavras, convite: Fale com a gente, nunca só Contato
Apoio 8 a 24 palavras: o que acontece depois e em quanto tempo — só o prazo que a casa cumpre
Canal o número ou endereço por extenso, como a pessoa digitaria. 2 a 4 canais
Cidade 1 a 3 palavras por escritório
Endereço duas linhas quebradas à mão: logradouro e número; bairro e CEP
Horário por extenso, e tem de bater com data-ctt-horario

O que mantém a peça: o canal é o dado, não a descrição. "(11) 90000-0000" em corpo grande vale mais que "Fale pelo nosso WhatsApp", porque é o que a pessoa vai digitar ou tocar.

Especificação dos assets

Nenhum. Sem mapa embutido, de propósito (ver Gotchas).


Como usar

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

O script é opcional: sem ele a dobra funciona inteira, só não aparece o selo aberto/fechado.

Cada escritório declara horário e fuso:

<article class="ctt-card" data-ctt-horario="1-5 09:00-18:00; 6 09:00-12:00" data-ctt-fuso="America/Sao_Paulo">

Ganchos

Gancho Papel
[data-ctt] a raiz
data-ctt-horario uma ou mais regras separadas por ;. Dias de 0 (domingo) a 6 (sábado), faixa ou dia único, horário HH:MM-HH:MM
data-ctt-fuso o fuso IANA do escritório. America/Sao_Paulo se ausente
[data-ctt-estado] o span que o script preenche. Vazio, não ocupa espaço
.ctt-card[data-aberto] sim ou nao, escrito pelo script. O ponto acende na cor de marca

Variações


Gotchas


Movimento reduzido / toque

Nada anda sozinho. No toque, a linha inteira do canal é o alvo, e o ponto do selo não pulsa — ele é estado, não chamada.


Histórico

Origem Quando O quê
autoral 2026-09-29 Construído do zero. Padrão de domínio comum, sem referência única

Origem. A seção de contato das bibliotecas de blocos e as páginas de contato de escritórios de advocacia e gestoras — um padrão tão difundido que não tem dono. Nada foi copiado. O registro consolidado vai em projetos/autorais.md.

Por que agora. Comparada com a taxonomia das grandes bibliotecas de seção, a casa tinha formulário, agendamento e CTA, e nenhum lugar para endereço e canal direto — que é o que todo site de assessoria precisa na página de contato.