← Biblioteca componentes/prova/grade-logos-miolo/ficha.md CódigoTela cheia
prova/grade-logos-miolo

Grade de logos com miolo de texto

🟢 estavel complexidade alta prefixo mesh- logos prova-social integracao grade malha hachura movimento-continuo parceiros saas escuro

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

<link rel="stylesheet" href="componentes/prova/grade-logos-miolo/estilo.css">
<script src="componentes/prova/grade-logos-miolo/script.js"></script>

markup.html · 113 linhas · 5,4 KB

<!-- ==========================================================================
     GRADE DE LOGOS COM MIOLO DE TEXTO
     Prefixo: mesh-
     Requer: core/tokens.css, core/base.css, core/motion.css, core/motion.js
             + este estilo.css e este script.js

     Uma malha de células. O texto da dobra ocupa o centro dela, e os logos
     moram nas células de fora. Célula sem logo mostra hachura, não vazio.

     De tempo em tempo dois logos vizinhos TROCAM DE CÉLULA, deslizando. É o
     efeito da referência: a grade parece um painel vivo em vez de uma lista.
     Um logo pode trocar com uma célula vazia — e aí ele anda, e a hachura
     aparece atrás dele.

     O MIOLO VEM PRIMEIRO no HTML, e isso é obrigatório: ele tem posição
     explícita na grade, e o CSS Grid coloca os itens posicionados antes de
     distribuir os automáticos. Assim as 24 células se acomodam em volta
     dele sozinhas — nenhuma delas declara linha ou coluna. Mudar a área do
     miolo (é o que as media queries fazem) reorganiza tudo de graça.

     As células estão em ordem de leitura: linha 1 inteira, depois as duas
     de cada lado do miolo, depois a linha 5 inteira.
     ========================================================================== -->

<section class="mesh sec-dark sec-pad" data-mesh>
  <div class="wrap">
    <div class="mesh-grid" data-mesh-grid>

      <!-- ---------------- o miolo ---------------- -->
      <div class="mesh-core">
        <div class="sec-head mesh-head">
          <span class="eyebrow" data-rv>Onde atuamos</span>
          <h2 class="sec-title ttl mesh-title">A instituição da sua <em>preferência</em> já está aqui.</h2>
          <p class="sec-lead mesh-lead" data-rv data-d="1">
            Operamos nas principais instituições do mercado. Sem vínculo com nenhuma
            delas, e sem custo a mais para você.
          </p>
        </div>
      </div>

      <!-- ---------------- linha 1 ---------------- -->
      <div class="mesh-cell" data-mesh-cell></div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-1.svg" alt="Meridian">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-2.svg" alt="Vantor">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo mesh-logo-tall" data-mesh-logo src="../../../showroom/demo/logo-selo.svg" alt="Quadrant">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-4.svg" alt="Kessler">
      </div>
      <div class="mesh-cell" data-mesh-cell></div>

      <!-- ---------------- linha 2 ---------------- -->
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-5.svg" alt="Northbay">
      </div>
      <div class="mesh-cell" data-mesh-cell></div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-6.svg" alt="Solvane">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo mesh-logo-tall" data-mesh-logo src="../../../showroom/demo/logo-selo.svg" alt="Quadrant">
      </div>

      <!-- ---------------- linha 3 ---------------- -->
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-3.svg" alt="Altamira">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-1.svg" alt="Meridian">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-2.svg" alt="Vantor">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-4.svg" alt="Kessler">
      </div>

      <!-- ---------------- linha 4 ---------------- -->
      <div class="mesh-cell" data-mesh-cell></div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-6.svg" alt="Solvane">
      </div>
      <div class="mesh-cell" data-mesh-cell></div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-5.svg" alt="Northbay">
      </div>

      <!-- ---------------- linha 5 ---------------- -->
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-4.svg" alt="Kessler">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-2.svg" alt="Vantor">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo mesh-logo-tall" data-mesh-logo src="../../../showroom/demo/logo-selo.svg" alt="Quadrant">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-5.svg" alt="Northbay">
      </div>
      <div class="mesh-cell" data-mesh-cell>
        <img class="mesh-logo" data-mesh-logo src="../../../showroom/demo/logo-3.svg" alt="Altamira">
      </div>
      <div class="mesh-cell" data-mesh-cell></div>

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

estilo.css · 162 linhas · 6,4 KB

/* ==========================================================================
   GRADE DE LOGOS COM MIOLO DE TEXTO
   Prefixo: mesh-
   Tokens usados: --bg --fg-2 --rule --accent --dark --display --sans
                  --sec-gap --ease
   ========================================================================== */

/* ---- a malha ----
   Seis colunas, e as duas do meio mais largas: é o que dá ao miolo uma
   caixa de texto utilizável. Medido na referência — célula de fora 194px,
   do meio 259px, ou seja 1,33 vez. Aqui é 1,5 porque a coluna do projeto
   (--max) é mais estreita que a do site de origem, e sem esse fio a mais
   o título quebraria em cinco linhas.

   As LINHAS DE GRADE são o gap: o fundo da malha é a cor do filete e o
   fundo de cada célula é o da seção, então o vão de 1px vira linha. Assim
   não há borda dupla entre vizinhas nem canto sobrando na ponta — o que
   aconteceria com border em cada célula. */
.mesh-grid{
  --mesh-row:clamp(96px,10.6vw,140px);
  --mesh-line:1px;

  display:grid;
  grid-template-columns:1fr 1fr 1.5fr 1.5fr 1fr 1fr;
  /* minmax e não altura fixa: a linha cresce se o miolo pedir mais que o
     vão reservado. Sem isso o texto transborda e a linha da grade corta a
     última frase — foi o que aconteceu em 768px, onde o miolo passa a
     ocupar a largura inteira e o texto quebra em mais linhas.
     No desktop não muda nada: o miolo cabe nas três linhas de sobra. E como
     nas quebras estreitas ele é o único item da linha dele, crescer não
     distorce célula nenhuma. */
  grid-auto-rows:minmax(var(--mesh-row),auto);
  gap:var(--mesh-line);
  background:var(--rule);
  border:var(--mesh-line) solid var(--rule);
}

/* ---- o miolo ----
   Posição explícita: colunas 3 e 4, linhas 2 a 4. É a única coisa na malha
   que declara onde fica, e é por isso que as células se distribuem sozinhas
   em volta. */
.mesh-core{
  grid-column:3 / span 2;
  grid-row:2 / span 3;
  background:var(--bg);
  display:grid;
  place-items:center;
  padding:clamp(18px,2.4vw,40px);
}
.mesh-head{--head-max:100%}

/* O título e o apoio vêm das primitivas do core (h2.sec-title, .sec-lead).
   Aqui só o que a caixa estreita do miolo pede: o teto de 22ch da primitiva
   deixaria o título com linha órfã dentro de 470px. */
.mesh-title{max-width:none}
.mesh-lead{max-width:38ch;margin-inline:auto}

/* ---- a célula ---- */
.mesh-cell{
  position:relative;
  background:var(--bg);
  display:grid;
  place-items:center;
}

/* ---- hachura da célula vazia ----
   Pseudo-elemento e não background da célula: a hachura precisa de opacidade
   própria para dissolver quando um logo chega ou sai, e mexer na opacidade
   da célula levaria o logo junto.

   Quem liga e desliga o .is-empty é o script, e a hachura nasce apagada.
   Colado num Elementor sem o script.js, a malha fica sem hachura nenhuma:
   linhas e logos, parados. Feio? Não — só mais sóbrio. Foi a escolha certa
   contra a alternativa, que era nascer acesa em toda célula e pintar
   hachura por trás dos logos.

   Não dá para resolver isso com :not(:has(> [data-mesh-logo])): o logo
   nunca sai da sua célula no HTML (ver abaixo), então o seletor acusaria
   como vazia justamente a célula de onde o logo saiu, e brigaria com o
   .is-empty a cada onda. */
.mesh-cell::after{
  content:"";
  position:absolute;inset:0;
  opacity:0;
  transition:opacity .55s var(--ease);
  background-image:repeating-linear-gradient(135deg,
    var(--rule) 0 1px,
    transparent 1px 7px);
}
.mesh-cell.is-empty::after{opacity:1}

/* ---- o logo ----
   O deslocamento vem do script em --mesh-dx/--mesh-dy, em pixels, e é
   medido do centro da célula de CASA até o centro da célula onde o logo
   está agora. O logo nunca sai da sua célula no HTML: só se desloca.

   Por que assim, e não movendo o nó no DOM ou trocando grid-column: porque
   nenhuma das duas alternativas anima. Trocar a coluna reposiciona sem
   transição, e mover o nó exigiria um FLIP à mão com transitionend para
   cada logo, a cada onda. Deslocamento em transform anima de graça, e a
   ordem de leitura do HTML fica estável para leitor de tela. */
/* Caixa em porcentagem da célula, e o object-fit resolve o resto. Foi a
   correção de um erro medido: com height em clamp mais max-width, quem
   mordia primeiro era a largura, e um wordmark de 5,4:1 acabava com 18px de
   altura numa célula de 140. Caixa única serve marca quadrada e wordmark
   comprido sem cada um pedir sua regra. */
.mesh-logo{
  width:66%;
  height:42%;
  object-fit:contain;
  opacity:.5;
  transform:translate3d(var(--mesh-dx,0),var(--mesh-dy,0),0);
  transition:transform 1.05s var(--ease),opacity .3s;
  will-change:transform;
}
.mesh-logo:hover{opacity:1}

/* Logo em viagem passa por cima das linhas e das vizinhas. Sem isto ele
   desaparece por baixo da célula do lado no meio do caminho. */
.mesh-logo.is-moving{z-index:2;position:relative}

/* Selo e marca quadrada pesam menos que wordmark na mesma caixa: ganham
   altura para equilibrar. Mesma lógica do marquee-logos. */
.mesh-logo-tall{height:58%}

/* Reposicionamento sem animação: o script marca a malha durante o resize,
   porque recalcular os centros com transição ligada faz todos os logos
   deslizarem de uma vez a cada pixel arrastado. */
.mesh-grid.is-still .mesh-logo{transition:none}

@media(prefers-reduced-motion:reduce){
  .mesh-logo{transition:none}
  .mesh-cell::after{transition:none}
}

/* ==========================================================================
   MOBILE
   Só duas coisas mudam, e o resto se reorganiza sozinho: o número de
   colunas e a área do miolo. As 24 células não declaram posição nenhuma,
   então o Grid as redistribui em volta da nova área do miolo de graça —
   era esse o ponto de deixá-las automáticas.

   O miolo passa a ocupar a largura inteira: em coluna estreita, um miolo
   de duas colunas no meio da malha deixa uma faixa de células de 40px de
   cada lado, e logo nenhum caberia ali.
   ========================================================================== */
@media(max-width:980px){
  .mesh-grid{
    grid-template-columns:1fr 1fr 1fr 1fr;
    --mesh-row:clamp(88px,14vw,120px);
  }
  .mesh-core{grid-column:1 / -1;grid-row:3 / span 2}
}

@media(max-width:560px){
  .mesh-grid{
    grid-template-columns:1fr 1fr 1fr;
    --mesh-row:clamp(76px,22vw,100px);
  }
  .mesh-core{grid-row:3 / span 3}
  .mesh-logo{width:74%}
}

script.js · 237 linhas · 9,2 KB

/* ==========================================================================
   GRADE DE LOGOS COM MIOLO — a troca de células

   A cada onda, alguns pares de células VIZINHAS trocam de conteúdo. Um logo
   pode trocar com outro logo ou com uma célula vazia; no segundo caso ele
   simplesmente anda, e a hachura acende atrás dele.

   O modelo é o mais simples que anima:

     - cada logo tem uma célula de CASA, que é onde ele está no HTML, e nunca
       sai dela. O que muda é um deslocamento em pixels;
     - o script mantém um mapa "célula -> logo que está nela agora";
     - trocar A com B é reescrever duas entradas do mapa e recalcular os
       deslocamentos dos logos envolvidos.

   Ou seja: nenhum nó muda de lugar no DOM e nenhuma propriedade de grade é
   reescrita. Sai de graça a animação (é transform), a ordem de leitura
   estável para leitor de tela, e a ausência de FLIP à mão.

   A geometria — quem é vizinho de quem — é DEDUZIDA das posições medidas,
   não escrita aqui. Assim mudar a grade no CSS (as media queries mudam de 6
   para 4 e para 3 colunas, e movem o miolo) não pede uma linha de JS.
   ========================================================================== */

(function () {
  var raizes = document.querySelectorAll('[data-mesh]');
  if (!raizes.length) return;

  var ONDA  = 2050;   /* intervalo entre ondas, em ms — medido na referência */
  var PARES = 3;      /* pares que trocam por onda */
  var VIAGEM = 1100;  /* deve casar com a transição do estilo.css */

  /* Flag do core (contrato §7.5). O plano B existe para o componente rodar
     colado num Elementor onde o motion.js não entrou. */
  function reduzido() {
    if (window.LIB && typeof LIB.reduce === 'boolean') return LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  /* Agrupa valores próximos num índice de faixa. É o que transforma
     coordenadas em pixels em coluna e linha sem o script saber quantas
     colunas a grade tem. A tolerância é generosa porque o centro de uma
     célula varia um pixel ou dois com arredondamento de subpixel. */
  function faixas(valores, tol) {
    var ord = valores.slice().sort(function (a, b) { return a - b; });
    var marcos = [];
    for (var i = 0; i < ord.length; i++) {
      if (!marcos.length || ord[i] - marcos[marcos.length - 1] > tol) marcos.push(ord[i]);
    }
    return function indice(v) {
      var melhor = 0, dist = Infinity;
      for (var j = 0; j < marcos.length; j++) {
        var d = Math.abs(marcos[j] - v);
        if (d < dist) { dist = d; melhor = j; }
      }
      return melhor;
    };
  }

  function iniciar(raiz) {
    var grade  = raiz.querySelector('[data-mesh-grid]');
    var celulas = raiz.querySelectorAll('[data-mesh-cell]');
    if (!grade || celulas.length < 2) return;

    /* ocupante[i] = o logo que está na célula i agora, ou null.
       casa[k]     = índice da célula onde o logo k mora no HTML. */
    var ocupante = [], casa = [], logos = [];
    var centro = [];      /* {x,y} de cada célula */
    var col = [], lin = [];

    var relogio = null, visivel = false, jaApareceu = false;

    function medir() {
      var xs = [], ys = [];
      centro = [];
      for (var i = 0; i < celulas.length; i++) {
        var r = celulas[i].getBoundingClientRect();
        var c = { x: r.left + r.width / 2, y: r.top + r.height / 2 };
        centro.push(c);
        xs.push(c.x); ys.push(c.y);
      }
      /* Tolerância: um terço da menor célula. Menos que isso e duas colunas
         estreitas viram uma; mais e a mesma coluna vira duas. */
      var menor = celulas[0].getBoundingClientRect();
      var tol = Math.max(Math.min(menor.width, menor.height) / 3, 8);
      var ix = faixas(xs, tol), iy = faixas(ys, tol);
      col = []; lin = [];
      for (var j = 0; j < centro.length; j++) {
        col.push(ix(centro[j].x));
        lin.push(iy(centro[j].y));
      }
    }

    /* Deslocamento do logo: do centro da casa até o centro de onde ele está.
       Escreve só as duas variáveis; quem monta o transform é o CSS. */
    function posicionar(k) {
      var logo = logos[k];
      var aqui = -1;
      for (var i = 0; i < ocupante.length; i++) if (ocupante[i] === logo) { aqui = i; break; }
      if (aqui < 0) return;
      var de = centro[casa[k]], para = centro[aqui];
      logo.style.setProperty('--mesh-dx', Math.round(para.x - de.x) + 'px');
      logo.style.setProperty('--mesh-dy', Math.round(para.y - de.y) + 'px');
    }

    function pintarVazias() {
      for (var i = 0; i < celulas.length; i++) {
        if (ocupante[i]) celulas[i].classList.remove('is-empty');
        else celulas[i].classList.add('is-empty');
      }
    }

    /* Vizinhos de uma célula: distância 1 na malha, contando coluna e linha.
       O miolo não é uma célula, então o que está de um lado dele e do outro
       simplesmente não é vizinho — e é assim que deve ser: um logo atravessar
       o texto por trás seria pior que não atravessar. */
    function vizinhos(i) {
      var out = [];
      for (var j = 0; j < celulas.length; j++) {
        if (j === i) continue;
        if (Math.abs(col[i] - col[j]) + Math.abs(lin[i] - lin[j]) === 1) out.push(j);
      }
      return out;
    }

    function trocar(a, b) {
      var t = ocupante[a];
      ocupante[a] = ocupante[b];
      ocupante[b] = t;

      var envolvidos = [ocupante[a], ocupante[b]];
      for (var e = 0; e < envolvidos.length; e++) {
        if (!envolvidos[e]) continue;
        envolvidos[e].classList.add('is-moving');
      }
      for (var k = 0; k < logos.length; k++) {
        if (logos[k] === ocupante[a] || logos[k] === ocupante[b]) posicionar(k);
      }
      pintarVazias();

      /* O z-index só vale durante a viagem: mantido para sempre, todo logo
         acabaria empilhado acima de todo mundo e a malha perderia a ordem. */
      setTimeout(function () {
        for (var e = 0; e < envolvidos.length; e++) {
          if (envolvidos[e]) envolvidos[e].classList.remove('is-moving');
        }
      }, VIAGEM);
    }

    function onda() {
      var usadas = {};
      var tentativas = 0;
      var feitos = 0;
      var quantos = parseInt(raiz.getAttribute('data-mesh-pairs'), 10) || PARES;

      /* Sorteia pares até conseguir os que faltam, com teto de tentativas:
         em grade pequena ou muito ocupada pode não haver par livre, e um
         while sem teto travaria a aba. */
      while (feitos < quantos && tentativas++ < 60) {
        var a = Math.floor(Math.random() * celulas.length);
        if (usadas[a]) continue;
        var viz = vizinhos(a);
        if (!viz.length) continue;
        var b = viz[Math.floor(Math.random() * viz.length)];
        if (usadas[b]) continue;
        /* Par de duas vazias não move nada: seria uma onda perdida. */
        if (!ocupante[a] && !ocupante[b]) continue;
        usadas[a] = usadas[b] = true;
        trocar(a, b);
        feitos++;
      }
    }

    function ligar() {
      if (relogio || reduzido()) return;
      relogio = setInterval(onda, ONDA);
    }
    function desligar() {
      if (relogio) { clearInterval(relogio); relogio = null; }
    }

    /* ------------------------------------------------------------ partida */
    for (var i = 0; i < celulas.length; i++) {
      var logo = celulas[i].querySelector('[data-mesh-logo]');
      ocupante.push(logo || null);
      if (logo) { logos.push(logo); casa.push(i); }
    }
    medir();
    pintarVazias();

    /* ---------------------------------------------------------- reajuste
       A grade muda de número de colunas nas media queries, então o resize
       não é só "ficou mais largo": a malha inteira se reorganiza e os
       centros mudam de lugar. Recalcula com a transição desligada, senão
       todos os logos deslizam a cada pixel arrastado. */
    var prazo = null;
    function reajustar() {
      grade.classList.add('is-still');
      medir();
      for (var k = 0; k < logos.length; k++) posicionar(k);
      /* Dois quadros: um para o navegador aplicar as posições novas sem
         transição, outro para religá-la sem que isso anime nada. */
      requestAnimationFrame(function () {
        requestAnimationFrame(function () { grade.classList.remove('is-still'); });
      });
    }
    addEventListener('resize', function () {
      clearTimeout(prazo);
      prazo = setTimeout(reajustar, 160);
    }, { passive: true });

    /* Fora da tela a malha para. É uma dobra do meio da página, e trocar
       logos que ninguém vê é gasto puro. */
    if (window.IntersectionObserver) {
      var io = new IntersectionObserver(function (es) {
        visivel = es[0].isIntersecting;
        /* Os centros medidos na partida podem estar errados se a dobra ainda
           não tinha layout — fonte que chegou depois, imagem sem dimensão
           declarada. Remede na primeira vez que ela aparece, e só nela. */
        if (visivel) {
          if (!jaApareceu) {
            jaApareceu = true;
            medir();
            for (var k = 0; k < logos.length; k++) posicionar(k);
          }
          ligar();
        } else desligar();
      }, { threshold: 0 });
      io.observe(raiz);
    } else {
      ligar();
    }
  }

  for (var r = 0; r < raizes.length; r++) iniciar(raizes[r]);
})();

Uma malha de células separadas por filete de 1px. O texto da dobra ocupa o centro dela — o miolo — e os logos moram nas células de fora. Célula sem logo não fica vazia: mostra hachura diagonal. E de dois em dois segundos alguns logos vizinhos trocam de célula, deslizando.

O problema editorial é o da prova social por logo: a lista de parceiros é sempre incompleta e sempre desatualizada, e uma fileira de doze logos convida a conferir quem falta. A malha resolve mudando a pergunta. Ela não afirma "estes são os nossos parceiros" e sim "isto é um ecossistema" — as células vazias dizem que há espaço, a troca diz que a lista se move, e o texto no meio ocupa o lugar onde estaria o logo mais importante. Ninguém conta logos numa grade que não para.

É por isso que a hachura importa e não é decoração: célula vazia lê como falta, célula hachurada lê como vaga.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Olho uma ou duas palavras, o nome da categoria: "Instituições", "Onde atuamos", "Custódia"
Título 5 a 8 palavras, com <em> no meio e não no fim. Numa caixa estreita o realce no fim cai sozinho na última linha
Apoio duas linhas. Diz o que a multicustódia poupa ("sem vínculo, e sem custo a mais"), não quantas instituições são
Logo SVG monocromático, altura óptica equalizada
Densidade 12 a 18 logos em 24 células. A referência tem 18

O texto do miolo não menciona número de instituições. "Mais de 50 instituições" envelhece e transforma a dobra numa promessa auditável; "as principais instituições do mercado" não. Se o número é o argumento, ele é um prova/stats-contador-regua, não isto.

E repetir logo na malha é legítimo — a referência repete dois. Numa grade que troca de posição a cada 2s, ninguém cataloga; o que se lê é densidade.

Especificação dos assets


Como usar

<link rel="stylesheet" href="componentes/prova/grade-logos-miolo/estilo.css">
<script src="componentes/prova/grade-logos-miolo/script.js"></script>

Célula com logo e célula vazia diferem em uma coisa: ter ou não ter o <img>.

<div class="mesh-cell" data-mesh-cell>
  <img class="mesh-logo" data-mesh-logo src="…" alt="Meridian">
</div>
<div class="mesh-cell" data-mesh-cell></div>

O miolo vem primeiro no HTML e é o único item que declara posição:

<div class="mesh-grid" data-mesh-grid>
  <div class="mesh-core"> … </div>   <!-- grid-column:3/span 2; grid-row:2/span 3 -->
  <div class="mesh-cell" data-mesh-cell></div>
  <!-- as outras 23, sem declarar linha nem coluna -->
</div>

Ganchos

Gancho Papel
[data-mesh] a seção. Liga o script
[data-mesh-grid] a malha. Recebe .is-still durante o resize
[data-mesh-cell] a célula. Ganha .is-empty quando nenhum logo está nela
[data-mesh-logo] o logo. Recebe --mesh-dx/--mesh-dy
data-mesh-pairs pares que trocam por onda. Padrão 3
.mesh-core o miolo. O único com grid-column/grid-row
.mesh-logo-tall logo quadrado, ganha altura
--mesh-row altura da linha. Padrão clamp(96px,10.6vw,140px)
--mesh-line espessura do filete. Padrão 1px

Variações


Gotchas


Histórico

Projeto Onde Data Diferenças
referência: Corvex https://usecorvex.com.br/ — dobra de integração 2026-08-31 referência visual, não origem de código

Normalizações aplicadas na tradução: a referência é Framer — classes hash, sem licença declarada, e nada foi copiado (§11). Medido nela e reproduzido: célula 194×165, colunas do meio 1,33 vez mais largas, malha 6×5 com o texto em 2 colunas por 3 linhas, hachura diagonal na célula vazia, onda de troca a cada ~2050ms com 3 a 4 pares, título 48/57.6 com tracking −0.02em.

O que é nosso: os logos saíram — são marcas de terceiros e não atravessam a tradução (§11); o preview usa os placeholders de showroom/demo/, monocromáticos e a opacity:.5. A hachura era um SVG hospedado e virou repeating-linear-gradient em --rule, sem asset. O layout era flex de cinco colunas com células empilhadas à mão; virou um CSS Grid onde só o miolo declara posição, o que faz as media queries reorganizarem tudo sem JS. O olho, o título e o apoio vêm das primitivas do core (.eyebrow, h2.sec-title, .sec-lead) em vez de tipografia própria. O movimento para quando a dobra sai da tela; a referência não para.

Divergências conhecidas vs. a referência: o olho leva o traço do .eyebrow do core, não o ponto verde-limão #B9F42E do original — o traço é a assinatura da casa e um ponto colorido pediria um token de cor novo (§9.3) para uma dobra só. O título vem em --display peso 700, e não no peso 500 da fonte de texto que a referência usa: mais afirmativo, menos técnico. As colunas do meio são 1,5 vez as de fora e não 1,33, porque o --max do projeto é mais estreito que a coluna do site de origem e sem esse fio a mais o título quebrava em cinco linhas. A densidade bate — 18 logos em 24 células, como no original — mas a mistura de formatos não: a referência tem seis ícones quadrados de marca (TikTok, WhatsApp, Shopify) que pesam muito na malha, e a biblioteca só tem um placeholder quadrado. O preview repete esse um para representar a mistura; com os logos reais do cliente a dobra fica mais próxima do original que o preview sugere.