← Biblioteca componentes/conteudo/bento-recursos-expansivel/ficha.md CódigoTela cheia
conteudo/bento-recursos-expansivel

Bento de recursos com parágrafo expansível

🟢 estavel complexidade média prefixo bento- bento grade recursos features card expansivel acordeao texto-sobre-foto veu card-escuro aria-expanded secao-clara

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

<link rel="stylesheet" href="componentes/conteudo/bento-recursos-expansivel/estilo.css">
<script src="componentes/conteudo/bento-recursos-expansivel/script.js"></script>

markup.html · 240 linhas · 11,8 KB

<!-- ==========================================================================
     BENTO DE RECURSOS — grade desigual, texto na foto e parágrafo que abre
     Prefixo: bento-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js
     Sem lib. A expansão são ~60 linhas de vanilla.

     A DOBRA. Cabeçalho à esquerda e, abaixo, uma grade de três colunas com
     duas linhas de peso diferente:

       linha 1 — um card largo de foto (duas colunas) e, ao lado, um card
                 escuro estreito com lista e botão;
       linha 2 — três cards escuros iguais, foto em cima e corpo embaixo.

     O desequilíbrio é o ponto: a linha 1 diz o que o produto é, a linha 2
     lista o que ele faz. Uma grade regular de seis cards diria que as seis
     coisas têm o mesmo peso — e não têm.

     CARD ESCURO EM DOBRA CLARA. Os quatro cards escuros declaram o próprio
     escopo de tom no markup (class="sec-dark" e class="sec-deep"), e não no
     CSS. Sem isso eles herdariam o --fg escuro da seção clara e o texto
     sumiria dentro deles. O componente continua sem sobrescrever --fg em
     lugar nenhum (contrato §3): ele só declara em qual escopo cada card
     vive. Os dois escuros são vizinhos de propósito — #111 e #161616 no
     tema — pela regra do tom vizinho.

     TEXTO NA FOTO (contrato §6). O título e o parágrafo do card largo ficam
     DENTRO da imagem, ancorados no rodapé, sobre a mesma pilha do
     conteudo/pilares-texto-na-foto: img -> scrim -> veil mascarado -> texto.
     Não viram legenda abaixo da foto.

     O GRUPO FLUTUANTE é aria-hidden. É a encenação de uma interface — uma
     pílula de status e dois cartões de produto —, não conteúdo: quem ouve a
     página receberia "Adicionado à carteira, CRA — agro, IPCA + 7,1%, R$ 800 mil"
     sem contexto nenhum, enquanto o título e o parágrafo logo abaixo já
     dizem o que o recurso é. Não há nada focável lá dentro.

     A EXPANSÃO é clique, não hover. Na referência o texto completo aparece
     ao passar o ponteiro; aqui o gatilho é um <button> de verdade, com
     aria-expanded e aria-controls apontando para o parágrafo — o que dá
     teclado e toque de graça, e é a mesma receita do listas/acordeao-faq.
     ========================================================================== -->

<section class="sec-light sec-pad bento" data-bento>
  <div class="wrap">

    <!-- Cabeçalho À ESQUERDA, e por isso não é o .sec-head do core (aquele
         centraliza). Só título e apoio, sem olho, como na referência. -->
    <div class="bento-head">
      <h2 class="sec-title" data-split>O que entra em toda assessoria</h2>
      <p class="sec-lead" data-rv data-d="1">
        Nada de taxa de performance escondida, produto empurrado ou relatório que
        chega dois meses depois. O que está aqui embaixo vale desde o primeiro
        diagnóstico, sem custo a mais.
      </p>
    </div>

    <div class="bento-grid">

      <!-- ==================================================== LINHA 1, card largo -->
      <article class="bento-wide" data-rv>
        <img class="bento-img" src="../../../showroom/demo/ph-wide-1.webp"
             alt="Casal em um vinhedo ao entardecer" decoding="async">
        <div class="bento-scrim"></div>
        <div class="bento-veil"></div>

        <!-- Encenação de interface, não conteúdo — ver o cabeçalho. -->
        <div class="bento-float" aria-hidden="true" data-rv data-d="2">
          <span class="bento-status">
            <svg class="bento-glyph" viewBox="0 0 14 14" focusable="false">
              <path d="M1.6 2.3h1.8l1.5 6.3h5.5l1.3-4.4H4.2" fill="none"
                    stroke="currentColor" stroke-width="1.3"
                    stroke-linecap="round" stroke-linejoin="round"/>
              <circle cx="5.9" cy="11.3" r="1.05" fill="currentColor"/>
              <circle cx="9.9" cy="11.3" r="1.05" fill="currentColor"/>
            </svg>
            Adicionado à carteira
          </span>

          <span class="bento-mini">
            <img class="bento-mini-thumb" src="../../../showroom/demo/ph-6.webp"
                 alt="" loading="lazy" decoding="async">
            <span class="bento-mini-info">
              <span class="bento-mini-name">CRA — agro, IPCA + 7,1%</span>
              <span class="bento-mini-price">R$ 800 mil</span>
            </span>
          </span>

          <span class="bento-mini bento-mini-alt">
            <img class="bento-mini-thumb" src="../../../showroom/demo/ph-8.webp"
                 alt="" loading="lazy" decoding="async">
            <span class="bento-mini-info">
              <span class="bento-mini-name">Fundo imobiliário de tijolo</span>
              <span class="bento-mini-price">R$ 240 mil</span>
            </span>
          </span>
        </div>

        <div class="bento-lab">
          <h3 class="bento-lab-title">Uma carteira desenhada na sua frente</h3>
          <p class="bento-lab-text">
            Você vê o ativo entrar, o motivo da escolha e o efeito no todo, na mesma
            tela. Nada é alocado sem você entender por quê.
          </p>
        </div>
      </article>

      <!-- ==================================================== LINHA 1, card escuro -->
      <!-- sec-dark: o card é uma ilha escura dentro da dobra clara. -->
      <div class="sec-dark bento-side" data-rv data-d="1">
        <h3 class="bento-side-title">Portal do cliente<br>incluso na assessoria</h3>

        <ul class="bento-list">
          <li class="bento-li">
            <svg class="bento-check" viewBox="0 0 20 20" aria-hidden="true" focusable="false">
              <circle cx="10" cy="10" r="9" fill="none" stroke="currentColor"
                      stroke-width="1.1" opacity=".4"/>
              <path d="M6 10.3l2.7 2.7 5.4-5.6" fill="none" stroke="currentColor"
                    stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            Posição consolidada por instituição
          </li>
          <li class="bento-li">
            <svg class="bento-check" viewBox="0 0 20 20" aria-hidden="true" focusable="false">
              <circle cx="10" cy="10" r="9" fill="none" stroke="currentColor"
                      stroke-width="1.1" opacity=".4"/>
              <path d="M6 10.3l2.7 2.7 5.4-5.6" fill="none" stroke="currentColor"
                    stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            Informe de rendimentos pronto
          </li>
          <li class="bento-li">
            <svg class="bento-check" viewBox="0 0 20 20" aria-hidden="true" focusable="false">
              <circle cx="10" cy="10" r="9" fill="none" stroke="currentColor"
                      stroke-width="1.1" opacity=".4"/>
              <path d="M6 10.3l2.7 2.7 5.4-5.6" fill="none" stroke="currentColor"
                    stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            Evolução de cada objetivo
          </li>
        </ul>

        <!-- Botão é primitiva do core. O chevron entra como <svg> porque o
             .btn do core já o desloca 4px no hover. -->
        <a class="btn btn-primary bento-cta" href="#area">
          Acessar o portal
          <svg class="bento-arrow" viewBox="0 0 14 14" aria-hidden="true" focusable="false">
            <path d="M2.6 7h8.2M7.4 3.6L10.8 7l-3.4 3.4" fill="none"
                  stroke="currentColor" stroke-width="1.5"
                  stroke-linecap="round" stroke-linejoin="round"/>
          </svg>
        </a>
      </div>

      <!-- ==================================================== LINHA 2 -->
      <!-- Os ids do aria-controls são únicos na PÁGINA, não no componente:
           duas dobras destas na mesma página pedem renumeração. -->
      <article class="sec-deep bento-card" data-bento-card data-rv>
        <div class="bento-media">
          <img class="bento-media-img" src="../../../showroom/demo/ph-wide-2.webp"
               alt="Homem observando a cidade pela janela" style="--bento-pos:50% 42%"
               loading="lazy" decoding="async">
          <div class="bento-fade"></div>
        </div>
        <div class="bento-body">
          <div class="bento-row">
            <h3 class="bento-title">Relatório consolidado</h3>
            <button class="bento-toggle" type="button" data-bento-toggle
                    aria-expanded="false" aria-controls="bento-detalhe-1"
                    aria-label="Relatório consolidado">
              <svg class="bento-chevron" viewBox="0 0 14 14" aria-hidden="true" focusable="false">
                <path d="M3.2 5.2L7 9l3.8-3.8" fill="none" stroke="currentColor"
                      stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
              </svg>
            </button>
          </div>
          <p class="bento-text" id="bento-detalhe-1" data-bento-text>
            Todas as instituições em uma folha só: posição, resultado do período e
            imposto a pagar. Chega no dia 5, sempre no mesmo formato, e vale
            para pessoa física e para a holding.
          </p>
        </div>
      </article>

      <article class="sec-deep bento-card" data-bento-card data-rv data-d="1">
        <div class="bento-media">
          <img class="bento-media-img" src="../../../showroom/demo/ph-wide-4.webp"
               alt="Sala de reunião vazia com vista para a cidade" style="--bento-pos:50% 54%"
               loading="lazy" decoding="async">
          <div class="bento-fade"></div>
        </div>
        <div class="bento-body">
          <div class="bento-row">
            <h3 class="bento-title">Comitê de investimentos</h3>
            <button class="bento-toggle" type="button" data-bento-toggle
                    aria-expanded="false" aria-controls="bento-detalhe-2"
                    aria-label="Comitê de investimentos">
              <svg class="bento-chevron" viewBox="0 0 14 14" aria-hidden="true" focusable="false">
                <path d="M3.2 5.2L7 9l3.8-3.8" fill="none" stroke="currentColor"
                      stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
              </svg>
            </button>
          </div>
          <p class="bento-text" id="bento-detalhe-2" data-bento-text>
            A cada trimestre a carteira volta à mesa: o que mudou no cenário, o que
            saiu da tese e o que precisa ser rebalanceado. Você recebe a ata com
            a decisão e o motivo dela.
          </p>
        </div>
      </article>

      <article class="sec-deep bento-card" data-bento-card data-rv data-d="2">
        <div class="bento-media">
          <img class="bento-media-img" src="../../../showroom/demo/ph-wide-3.webp"
               alt="Veleiro em mar aberto" style="--bento-pos:50% 50%"
               loading="lazy" decoding="async">
          <div class="bento-fade"></div>
        </div>
        <div class="bento-body">
          <div class="bento-row">
            <h3 class="bento-title">Painel de posição</h3>
            <button class="bento-toggle" type="button" data-bento-toggle
                    aria-expanded="false" aria-controls="bento-detalhe-3"
                    aria-label="Painel de posição">
              <svg class="bento-chevron" viewBox="0 0 14 14" aria-hidden="true" focusable="false">
                <path d="M3.2 5.2L7 9l3.8-3.8" fill="none" stroke="currentColor"
                      stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
              </svg>
            </button>
          </div>
          <p class="bento-text" id="bento-detalhe-3" data-bento-text>
            Patrimônio, rentabilidade e exposição por classe na mesma tela,
            atualizados a cada fechamento. Dá para comparar dois períodos lado a
            lado e levar tudo para uma planilha.
          </p>
        </div>
      </article>

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

estilo.css · 392 linhas · 16,6 KB

/* ==========================================================================
   BENTO DE RECURSOS — prefixo bento-
   Grade de três colunas em duas linhas de peso diferente: um card largo de
   foto com o texto por dentro, um card escuro estreito ao lado, e três cards
   escuros iguais embaixo, cada um com o parágrafo que abre no clique.

   Tokens usados:
     --max --pad          só via .wrap, que o markup usa — nada aqui mexe na coluna
     --sec-gap            respiro da dobra, via .sec-pad do core
     --display --ease     tipografia de título e curva do projeto
     --radius-media       raio dos cards, por multiplicação (ver --bento-radius)
     --radius-pill        pílula de status e botão redondo
     --accent             cor de ação: status, check, hover do gatilho
     --light --dark       o texto DENTRO da foto e os cartõezinhos brancos
     --dark-rgb           o scrim sobre a foto
     --bg --fg --fg-2 --rule-2   herdados do escopo que CADA CARD declara

   OS QUATRO CARDS ESCUROS declaram o próprio escopo no markup: .sec-dark no
   da linha 1, .sec-deep nos três da linha 2. Por isso as regras daqui leem
   --bg e --fg como qualquer componente leria — a inversão de tom acontece na
   classe do core, não numa sobrescrita local (contrato §3). O efeito colateral
   é o que se quer: em tema flat e claro, os mesmos cards saem claros sem
   tocar numa linha deste arquivo.
   ========================================================================== */

.bento{
  /* Vão da grade. 20px na referência a 1440; encolhe junto com a coluna. */
  --bento-gap:clamp(12px,1.4vw,20px);

  /* RAIO POR MULTIPLICAÇÃO, e não valor cravado. O tema define o card em
     --radius-media (16px no kirvano); o painel de dobra da referência é uma
     vez e meia isso. Em projeto flat --radius-media é 0, e 0 vezes 1.5 é 0:
     a dobra sai reta sem uma media query nem um segundo token. */
  --bento-radius:calc(var(--radius-media) * 1.5);

  /* Altura da linha 1. A referência mede 460 numa coluna de 1136 — os 32vw
     acertam esse número a 1440 e derretem junto com a tela. */
  --bento-tall:clamp(360px,32vw,460px);

  /* PISO da linha 2 (472 na referência, contra 460 da linha 1 — as duas
     linhas têm quase a mesma altura, e é isso que faz o bento parecer bento
     e não duas dobras empilhadas).

     É piso e não altura: o texto de três linhas mais o cabeçalho não chegam
     a 472 sozinhos, e a folga que sobra no pé do card é justamente para onde
     o parágrafo cresce. Os primeiros cliques abrem DENTRO do card, sem
     empurrar a página; só quando o texto passa da folga é que a dobra cede
     altura. Encolher este valor devolve o pulo da página ao primeiro clique. */
  --bento-card-h:clamp(380px,33vw,472px);

  /* Altura da mídia no topo dos cards da linha 2 (236 na referência, que é
     exatamente metade do card). */
  --bento-media-h:clamp(170px,16.4vw,236px);

  --bento-pad:clamp(20px,1.95vw,28px);        /* respiro dos cards da linha 2 */
  --bento-pad-wide:clamp(22px,2.2vw,32px);    /* respiro do card largo e do escuro */
  --bento-head-max:760px;
  --bento-float:328px;                        /* largura do grupo de cartões */

  /* O PARÁGRAFO FECHADO, em LINHAS e não em pixels: a entrelinha entra na
     conta, então trocar o corpo do texto não desalinha mais nada.

     Três é o meio do caminho medido. A referência mostra ~5 linhas fechadas
     e revela o resto no hover — o que quase não é revelação, porque já se
     leu quase tudo. Duas linhas fazem o contrário: escondem tanto que o
     título vira o único conteúdo do card. Três mostram o assunto e cortam
     no argumento. */
  --bento-lh:1.55;
  --bento-lines:3;
}

/* ---------------------------------------------------------------- cabeçalho */
/* À esquerda, e por isso não é o .sec-head do core, que centraliza. */
.bento-head{max-width:var(--bento-head-max)}

/* O margin-top que o core dá ao .sec-title existe para o vão até o .eyebrow.
   Esta dobra não tem olho — sem zerar, sobra um degrau solto no topo. */
.bento-head .sec-title{margin-top:0}

/* ---------------------------------------------------------------- grade */
/* minmax(0,1fr) e não 1fr: a coluna precisa poder encolher abaixo do
   conteúdo, senão a foto do card largo define a largura da grade inteira. */
.bento-grid{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:var(--bento-gap);
  margin-top:clamp(32px,3.4vw,48px);
}

/* ============================================================ card largo */
/* isolation:isolate dá contexto de empilhamento próprio: o desfoque do véu
   não vaza para o card vizinho. Mesma razão do conteudo/pilares-texto-na-foto.

   space-between com dois filhos é o que prende o grupo de cartões no topo e o
   texto no rodapé. Os dois ficam NO FLUXO de propósito, e não posicionados
   sobre a foto: a foto é que é absoluta. Com os dois absolutos — que é o
   desenho da referência — o card teria altura fixa e, em coluna estreita, o
   grupo de cima e o texto de baixo cresceriam um por cima do outro. Assim o
   card não pode se sobrepor a si mesmo: o min-height é piso, não teto. */
.bento-wide{
  grid-column:span 2;
  position:relative;
  display:flex;flex-direction:column;justify-content:space-between;
  min-height:var(--bento-tall);
  overflow:hidden;isolation:isolate;
  border-radius:var(--bento-radius);
}

.bento-img{
  position:absolute;inset:0;
  width:100%;height:100%;object-fit:cover;
  object-position:var(--bento-pos,50% 50%);
}

/* Pilha do §6: scrim garante contraste sempre, véu separa o texto do detalhe
   da foto. Um sem o outro não resolve — só gradiente deixa o texto sobre
   textura, só desfoque deixa claro demais em foto clara. */
.bento-scrim{
  position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(to top,rgba(var(--dark-rgb),.92) 0%,rgba(var(--dark-rgb),.66) 26%,rgba(var(--dark-rgb),.2) 56%,transparent 78%);
}

/* Véu desfocado COM máscara em degradê. Sem a máscara vira faixa chapada com
   linha dura contra a parte nítida da foto (contrato §6). */
.bento-veil{
  position:absolute;left:0;right:0;bottom:0;height:58%;z-index:1;pointer-events:none;
  backdrop-filter:blur(9px) saturate(1.1);
  -webkit-backdrop-filter:blur(9px) saturate(1.1);
  -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 44%,transparent 100%);
  mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 44%,transparent 100%);
}

/* ---- grupo de cartões ---- */
/* position:relative só pelo z-index — ele precisa ficar acima do scrim e do
   véu, que são absolutos com z-index 1.

   A largura é DECLARADA, e não deixada por conta do conteúdo: os cartões lá
   dentro têm largura em min(100%,…), e uma caixa que se dimensiona pelos
   filhos com filhos que se dimensionam pela caixa é uma conta circular — o
   navegador a resolve, mas por um caminho que ninguém previu. Com a largura
   escrita aqui, os 100% dos filhos têm de quê ser 100%.

   align-items:flex-start é o que faz cada cartão ter a própria largura em vez
   de esticar até a do grupo. */
.bento-float{
  position:relative;z-index:2;
  width:min(100%,calc(var(--bento-float) + var(--bento-pad-wide) * 2));
  padding:var(--bento-pad-wide);
  display:flex;flex-direction:column;align-items:flex-start;
  gap:12px;
}

/* Pílula de status: ilha de marca, como o painel de bio do midia/carrossel-perfis.
   Pinta --accent e escreve em --dark, e não em --fg — assim ela lê igual sobre
   foto clara ou escura, e num tema claro ou escuro. */
.bento-status{
  display:inline-flex;align-items:center;gap:8px;
  padding:8px 14px;
  border-radius:var(--radius-pill);
  background:var(--accent);color:var(--dark);
  font-size:12px;font-weight:600;line-height:1;letter-spacing:normal;
  white-space:nowrap;
}
.bento-glyph{flex:0 0 auto;width:14px;height:14px}

/* Cartão de produto. O segundo é mais estreito e recuado — é o que dá a
   sensação de pilha, e não de lista. As larguras são teto, não medida fixa:
   o min() as devolve à largura do grupo em tela estreita. */
.bento-mini{
  display:flex;align-items:center;gap:12px;
  width:min(100%,279px);
  padding:10px;
  border-radius:var(--radius-media);
  background:color-mix(in srgb,var(--light) 95%,transparent);
  color:var(--dark);
}
.bento-mini-alt{
  width:min(100%,209px);
  margin-inline-start:clamp(14px,2.2vw,32px);
  background:color-mix(in srgb,var(--light) 90%,transparent);
}

.bento-mini-thumb{
  flex:0 0 auto;width:40px;height:40px;
  object-fit:cover;
  border-radius:calc(var(--radius-media) * .625);
}
.bento-mini-info{display:flex;flex-direction:column;gap:2px;min-width:0}
.bento-mini-name{
  font-size:13px;line-height:1.25;letter-spacing:normal;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.bento-mini-price{font-size:13px;line-height:1.25;font-weight:600;letter-spacing:normal}

/* ---- texto DENTRO da foto ---- */
/* Fica no fluxo, como o grupo de cartões; quem o prende no rodapé é o
   space-between do card. O z-index o mantém acima do scrim e do véu. */
.bento-lab{position:relative;z-index:2;padding:var(--bento-pad-wide)}

/* O texto vive dentro da foto escura, então é claro SEMPRE — não segue o
   escopo de tom da seção, que aqui é clara. Mesma decisão do
   conteudo/pilares-texto-na-foto.

   As larguras são as medidas da referência (600 e 540 numa caixa de 751), e
   não uma conta em ch: em ch elas mudariam de tamanho junto com a fonte do
   tema, e o que se persegue aqui é a MANCHA — o título ocupando quatro
   quintos do card e o parágrafo terminando antes dele. O min() as devolve à
   largura da caixa quando o card encolhe. */
.bento-lab-title{
  font-family:var(--display);font-weight:700;
  font-size:clamp(20px,1.95vw,28px);line-height:1.1;letter-spacing:-.02em;
  color:var(--light);
  max-width:min(100%,600px);
}
.bento-lab-text{
  margin-top:16px;
  font-size:clamp(14px,1.15vw,16px);line-height:1.5;
  color:color-mix(in srgb,var(--light) 72%,transparent);
  max-width:min(100%,540px);
}

/* ============================================================ card escuro da linha 1 */
/* background:var(--bg) e não uma cor: o --bg vem do .sec-dark que o card
   declara no markup. O base.css só pinta <section> — um card precisa dizer. */
.bento-side{
  display:flex;flex-direction:column;
  min-height:var(--bento-tall);
  padding:var(--bento-pad-wide);
  border-radius:var(--bento-radius);
  background:var(--bg);
}

/* text-wrap:initial pelo mesmo motivo da primitiva de título gigante do core:
   o reset dá balance a todo h1..h4, e balancear por cima de um <br> escrito
   à mão reintroduz a quebra automática que o <br> existe para evitar. */
.bento-side-title{
  font-family:var(--display);font-weight:700;
  font-size:clamp(21px,2.1vw,30px);line-height:1.05;letter-spacing:-.02em;
  color:var(--fg);
  text-wrap:initial;
}

.bento-list{
  display:flex;flex-direction:column;gap:14px;
  margin-top:clamp(20px,2vw,28px);
}
.bento-li{
  display:flex;align-items:center;gap:10px;
  font-size:15px;line-height:1.4;color:var(--fg-2);
}
.bento-check{flex:0 0 auto;width:20px;height:20px;color:var(--accent)}

/* margin-top:auto empurra o botão para o rodapé do card, qualquer que seja o
   tamanho da lista. align-self impede que o .btn do core, dentro de uma
   coluna flex, estique até a largura inteira. */
.bento-cta{margin-top:auto;align-self:flex-start}
.bento-arrow{flex:0 0 auto;width:14px;height:14px}

/* ============================================================ cards da linha 2 */
/* min-height é PISO: o card cresce quando o parágrafo abre além da folga.
   Ver --bento-card-h lá em cima — a folga é parte do desenho, não sobra. */
.bento-card{
  position:relative;
  display:flex;flex-direction:column;
  min-height:var(--bento-card-h);
  overflow:hidden;
  border-radius:var(--bento-radius);
  background:var(--bg);
}

.bento-media{
  position:relative;flex:0 0 auto;
  height:var(--bento-media-h);
  overflow:hidden;
}
.bento-media-img{
  width:100%;height:100%;object-fit:cover;
  object-position:var(--bento-pos,50% 50%);
}

/* Degradê que dissolve a base da foto no corpo do card. Lê --bg, então é
   sempre exatamente a cor do card embaixo dele — inclusive se o tema mudar. */
.bento-fade{
  position:absolute;left:0;right:0;bottom:0;
  height:clamp(60px,8vw,96px);
  pointer-events:none;
  background:linear-gradient(to top,var(--bg),transparent);
}

/* O topo é menor que os outros três lados: a foto acima já dá respiro, e
   repetir os 28px afastaria o título da imagem que ele nomeia. */
.bento-body{
  display:flex;flex-direction:column;
  padding:var(--bento-pad);
  padding-top:calc(var(--bento-pad) * .58);
}

.bento-row{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:12px;
}
/* Sem max-width: quem impede o título de espremer o gatilho é o flex:0 0 auto
   do próprio gatilho. Título comprido quebra em mais linhas e empurra o card
   para baixo, que é o comportamento certo — o padrão editorial da ficha pede
   duas ou três palavras justamente para isso não acontecer. */
.bento-title{
  font-family:var(--display);font-weight:700;
  font-size:clamp(17px,1.4vw,20px);line-height:1.15;letter-spacing:-.02em;
  color:var(--fg);
}

/* O gatilho é <button> de verdade: teclado, foco e leitor de tela vêm de
   graça, e o anel do :focus-visible do core cai certo por cima do círculo. */
.bento-toggle{
  flex:0 0 auto;
  width:32px;height:32px;padding:0;
  display:inline-flex;align-items:center;justify-content:center;
  /* O círculo é pequeno o bastante para o desenho nativo de botão aparecer
     por baixo do nosso no iOS — daí o appearance zerado, que o .faq-question
     do listas/acordeao-faq não precisa por ser uma faixa de texto inteira. */
  -webkit-appearance:none;appearance:none;
  background:none;
  border:1px solid var(--rule-2);
  border-radius:var(--radius-pill);
  color:var(--fg-2);
  transition:color .3s var(--ease),border-color .3s var(--ease);
}
.bento-toggle:hover{color:var(--accent);border-color:var(--accent)}

.bento-chevron{width:13px;height:13px;transition:transform .4s var(--ease)}
.bento-card.is-open .bento-chevron{transform:rotate(180deg)}

/* PARÁGRAFO QUE ABRE. Fechado, ele tem a altura de --bento-lines linhas; o
   script mede a altura real e escreve o max-height no clique — não dá para
   animar height:auto, e um max-height chutado alto deixa a abertura lenta e o
   fechamento com atraso (mesma receita do listas/acordeao-faq). */
.bento-text{
  position:relative;
  margin-top:12px;
  font-size:clamp(14px,1.15vw,16px);
  line-height:var(--bento-lh);
  color:var(--fg-2);
  max-height:calc(var(--bento-lh) * 1em * var(--bento-lines));
  overflow:hidden;
  transition:max-height .5s var(--ease);
}

/* A última linha visível some no fundo do card: é o sinal de que há mais
   texto ali. Some quando abre — sem ele, o parágrafo aberto terminaria
   esmaecido, o que leria como corte e não como fim. */
.bento-text::after{
  content:"";
  position:absolute;left:0;right:0;bottom:0;
  height:1.5em;
  pointer-events:none;
  background:linear-gradient(to top,var(--bg),transparent);
  transition:opacity .35s var(--ease);
}
.bento-card.is-open .bento-text::after{opacity:0}

/* ---------------------------------------------------------------- movimento reduzido */
/* A entrada da dobra é do core e o core já a desliga. O que sobra aqui é o
   que este componente anima por conta própria: a altura do parágrafo, o giro
   do chevron e o desvanecer do degradê. Sem transição, o clique continua
   abrindo e fechando — só que de uma vez. */
@media(prefers-reduced-motion:reduce){
  .bento-text{transition:none}
  .bento-text::after{transition:none}
  .bento-chevron{transition:none}
}

/* ---------------------------------------------------------------- mobile */
/* Duas colunas: o card largo ocupa a linha inteira e, embaixo, o card escuro
   e o primeiro da linha 2 dividem a seguinte. O bento continua desigual, que
   é a razão de ele existir — três colunas iguais seriam uma tabela. */
@media(max-width:1040px){
  .bento-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}

@media(max-width:680px){
  /* Uma coluna. O span:2 precisa ser desfeito na mão, senão o navegador cria
     uma segunda coluna implícita só para caber o card largo. */
  .bento-grid{grid-template-columns:minmax(0,1fr)}
  .bento-wide{grid-column:auto}
  /* O card escuro perde o piso de altura: ele tem três itens de lista e um
     botão, e esticar isso até 360 abre um vão morto no meio. O card largo
     mantém o piso, porque ali a foto é metade do argumento. */
  .bento-side{min-height:0}
  .bento{--bento-media-h:200px}
  /* Em coluna estreita o recuo do segundo cartão come a largura útil dele. */
  .bento-mini-alt{margin-inline-start:0}
}

script.js · 137 linhas · 5,9 KB

/* ==========================================================================
   BENTO DE RECURSOS — o parágrafo que abre no clique

   Só isso. A grade, o texto na foto e o degradê são CSS e não passam por
   aqui. O que o script faz é o que o CSS sozinho não sabe fazer: descobrir a
   altura REAL do parágrafo, para o max-height ter para onde animar.

   Cada card é independente: abrir um não fecha os outros. Não é um acordeão
   de perguntas, onde a resposta aberta é o assunto da vez — são três
   recursos paralelos, e comparar dois abertos lado a lado é uso legítimo.

   Nenhum listener de scroll: quem observa a entrada da dobra é o
   IntersectionObserver do core, pelos data-rv do markup.

   MOVIMENTO REDUZIDO: com a preferência ligada, o parágrafo aberto recebe
   max-height:none em vez de um valor medido. Não é só economia — com a
   transição desligada pelo CSS não há o que animar, e um número congelado
   voltaria a cortar o texto na primeira reflow (fonte que chega atrasada,
   zoom, coluna que muda). Sem número, não há o que ficar velho — e por isso
   o remedir do resize também não precisa rodar nesse caso.
   ========================================================================== */

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

  /* A flag do core é a fonte. O fallback existe para o componente colado
     sozinho, sem motion.js — e repete a regra do ?motion=on. */
  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 iniciar(raiz) {
    /* Parte da raiz do próprio componente, nunca do documento: duas dobras
       destas na mesma página não podem misturar os cards uma da outra. */
    var cards = raiz.querySelectorAll('[data-bento-card]');
    if (!cards.length) return;

    var reduz = reduzido();
    var itens = [];

    /* ------------------------------------------------------------ abrir e fechar */

    /* scrollHeight lido com o parágrafo FECHADO devolve a altura do conteúdo,
       e não a da caixa — é para isso que serve o overflow:hidden do CSS. Medir
       aqui, e não no load: se a fonte ainda não tinha chegado, a altura estaria
       errada por uma linha inteira.

       Nada de suspender o teto (max-height:none) para medir: ler scrollHeight
       força o navegador a assentar o estilo, e ele assentaria o "none" —
       que não interpola com comprimento nenhum. A transição seguinte sairia
       de um valor não animável e o parágrafo abriria seco, sem animação. */
    function abrir(item) {
      item.card.classList.add('is-open');
      item.botao.setAttribute('aria-expanded', 'true');
      item.texto.style.maxHeight = reduz ? 'none' : item.texto.scrollHeight + 'px';
    }

    function fechar(item) {
      item.card.classList.remove('is-open');
      item.botao.setAttribute('aria-expanded', 'false');
      /* String vazia devolve o parágrafo ao teto do CSS (--bento-lines
         linhas). Escrever o valor fechado aqui seria manter a mesma conta em
         dois arquivos. */
      item.texto.style.maxHeight = '';
    }

    /* ------------------------------------------------------------ remedir */

    /* Janela mais estreita, mais linhas: o número congelado no clique cortaria
       o fim do texto. E janela mais larga, menos linhas: o card ficaria alto
       à toa. Os dois pedem a mesma coisa — fechar, medir de novo, reabrir.

       Redimensionar não é animar: a transição sai do caminho enquanto isso
       acontece, senão o parágrafo pisca fechando e abrindo a cada arrasto da
       borda da janela. A ordem importa e não é decorativa —

         1. desliga a transição no estilo inline;
         2. devolve o teto do CSS;
         3. lê scrollHeight, o que assenta os dois passos acima de uma vez;
         4. escreve a altura nova, ainda sem transição;
         5. assenta de novo, para o valor novo virar o ponto de partida;
         6. devolve a transição ao CSS.

       Sem o passo 5, a transição voltaria a valer no mesmo assentamento em
       que a altura muda, e o pisco reapareceria. */
    function remedir(item) {
      var texto = item.texto;
      texto.style.transition = 'none';
      texto.style.maxHeight = '';
      var alto = texto.scrollHeight;
      texto.style.maxHeight = alto + 'px';
      void texto.offsetHeight;
      texto.style.transition = '';
    }

    /* ------------------------------------------------------------ ligação */

    function ligar(item) {
      item.botao.addEventListener('click', function () {
        if (item.card.classList.contains('is-open')) fechar(item); else abrir(item);
      });
    }

    for (var i = 0; i < cards.length; i++) {
      var card = cards[i];
      var botao = card.querySelector('[data-bento-toggle]');
      var texto = card.querySelector('[data-bento-text]');
      /* Card sem gatilho ou sem parágrafo é card estático — sai de fininho.
         O <button> do markup já traz aria-expanded="false", então o estado
         anunciado continua certo mesmo sem script nenhum. */
      if (!botao || !texto) continue;
      var item = { card: card, botao: botao, texto: texto };
      itens.push(item);
      ligar(item);
    }

    if (!itens.length) return;

    /* Uma medida por quadro. remedir() força layout duas vezes, e fazer isso a
       cada evento de resize trava o redimensionamento da janela. */
    var pedido = 0;
    addEventListener('resize', function () {
      if (pedido || reduz) return;
      pedido = requestAnimationFrame(function () {
        pedido = 0;
        for (var j = 0; j < itens.length; j++) {
          if (itens[j].card.classList.contains('is-open')) remedir(itens[j]);
        }
      });
    }, { passive: true });
  }

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

Dobra clara com o cabeçalho encostado à esquerda e, abaixo, uma grade de três colunas em duas linhas de peso diferente. Em cima, um card largo de duas colunas onde uma foto ocupa o card inteiro: um grupo de cartõezinhos de interface flutua no canto superior esquerdo e o título e o parágrafo ficam ancorados no rodapé, dentro da imagem, sobre gradiente e desfoque mascarado. Ao lado dele, um card escuro estreito com título em duas linhas, três itens com ícone de check e um botão no pé. Embaixo, três cards escuros iguais, foto no topo e corpo em baixo, cada um com um botão redondo de chevron que abre o parágrafo.

Resolve a dobra de "o que o produto faz" quando os recursos não têm o mesmo peso. Uma grade regular de seis cards afirma que as seis coisas valem o mesmo; o bento hierarquiza sem escrever "principal" em lugar nenhum — a linha 1 diz o que o produto é, a linha 2 lista o que ele faz. E o parágrafo que abre no clique dá aos cards de baixo o espaço de duas linhas na página e o de dez para quem quiser.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Título da dobra Até 40 caracteres — o max-width:22ch do .sec-title do core o quebra em duas linhas a partir de ~25. Entra palavra a palavra (data-split), e frase longa demora a assentar.
Apoio da dobra Duas a três linhas. Diz o que as seis coisas têm em comum, não o que cada uma é.
Pílula de status Duas a três palavras, no presente e já concluído ("Adicionado à carteira"). É a encenação de um estado, não um botão.
Cartão de ativo Nome até 22 caracteres (corta com reticências) + valor. Dois cartões, nunca três: o terceiro esbarra no texto do rodapé.
Título do card largo 35 a 50 caracteres, duas linhas. É a promessa do produto inteiro.
Parágrafo do card largo Duas a três linhas. Termina antes da borda do título, e é isso que dá o degrau.
Título do card escuro Duas linhas curtas, quebra escrita no <br>.
Itens da lista Três, sempre. Até 30 caracteres cada — dois deles quebrando em duas linhas desalinha a coluna toda.
Botão Verbo na primeira pessoa ("Criar minha área"). Texto do core, em caixa alta pelo .btn.
Título dos cards de baixo Duas ou três palavras. É rótulo de recurso, não frase.
Parágrafo dos cards de baixo 180 a 260 caracteres. Abaixo de 180 ele cabe nas duas linhas fechadas e o botão abre para nada; acima de 260 o card aberto fica desproporcional em relação aos irmãos.

O que mantém a dobra coesa é o paralelismo dos três de baixo: se um parágrafo começa explicando o que a pessoa faz, todos começam. Os três títulos têm de ter o mesmo número de palavras — é a única coisa que impede a fila de baixo de parecer improvisada.

Especificação dos assets


Como usar

<link rel="stylesheet" href="componentes/conteudo/bento-recursos-expansivel/estilo.css">
<script src="componentes/conteudo/bento-recursos-expansivel/script.js"></script>

Os quatro cards escuros declaram o próprio escopo de tom no markup — class="sec-dark" no da linha 1 e class="sec-deep" nos três de baixo. Sem isso eles herdariam o --fg escuro da seção clara e o texto sumiria dentro deles. O CSS do componente não sobrescreve --fg em lugar nenhum: só lê --bg e --fg de quem cada card declarou ser.

Ganchos

Gancho Papel
[data-bento] Raiz. O script parte daqui, nunca do documento.
[data-bento-card] Card da linha 2 que tem parágrafo expansível.
[data-bento-toggle] O <button> redondo. Leva aria-expanded e aria-controls.
[data-bento-text] O parágrafo que abre. É o alvo do aria-controls.
--bento-gap Vão da grade. clamp(12px,1.4vw,20px).
--bento-radius Raio dos cards, derivado de --radius-media × 1.5. Em tema flat vira 0 sozinho.
--bento-tall Altura da linha 1. clamp(360px,32vw,460px).
--bento-card-h Piso de altura dos cards da linha 2. clamp(380px,33vw,472px). A folga que sobra é para onde o parágrafo cresce.
--bento-media-h Altura da foto no topo dos cards de baixo. clamp(170px,16.4vw,236px) — metade do card.
--bento-pad / --bento-pad-wide Respiro dos cards da linha 2 e dos da linha 1.
--bento-float Largura do grupo de cartõezinhos. 328px.
--bento-head-max Largura do cabeçalho. 760px.
--bento-lh / --bento-lines Entrelinha e número de linhas visíveis do parágrafo fechado (1.55 e 3). A altura fechada é a multiplicação das duas.
--bento-pos object-position de cada foto, no style da própria <img>.
.is-open No card da linha 2, enquanto o parágrafo está aberto. Quem põe e tira é o script.

Variações


Gotchas


Histórico

Projeto Onde Data Diferenças
referência: Kirvano https://www.kirvano.com/ — dobra "Recursos poderosos e gratuitos" 2026-09-01 referência visual, não origem de código

Origem. Escrito do zero a partir da observação da dobra de recursos de kirvano.com, medida no navegador a 1440 × 900 com getComputedStyle e getBoundingClientRect. Nada foi copiado: o site não declara licença, e o que veio de lá foi a medida e a ideia de hierarquizar recursos por tamanho de card. Status externo — nunca rodou em produção nossa. O primeiro site que usar promove, e a ficha registra qual foi. A referência está registrada em projetos/referencias-externas.md.

Medido na referência: seção padding:96px 0 sobre #f1ece1; cabeçalho max-width:760 com margin-bottom:48, H2 32px/1.1 com as palavras em span e P a 20 de distância, 18px; grade repeat(3,1fr) com gap:20 numa coluna de 1136. Linha 1 — card A span 2, 751 × 460, raio 24, overflow:hidden, foto cobrindo mais scrim, grupo flutuante 328 × 176 no topo-esquerda com gap:12 (pílula rgb(13,122,59) de 154 × 32, cartão branco 279 × 60 em rgba(255,255,255,.95) com raio 16, segundo cartão 209 × 60 em rgba(255,255,255,.9) com margin-left:32) e, no rodapé, H3 28px/1.1 em caixa 600 × 77 mais P a 16 de distância, 16px, rgba(255,255,255,.7), caixa 540; card B 365 × 460, #111, raio 24, padding:32, H3 30px/1.05 em duas linhas com <br>, ul a 28 com gap:14 e itens de 24 (gap:10, ícone de check), CTA a margin-top:auto, 178 × 52, menta, raio 12, com chevron. Linha 2 — três cards 365 × 472 em #131313, raio 24, overflow:hidden, mídia de 236 no topo com degradê inferior de 96, corpo p-7 pt-4 de 236 com fila 309 × 50 (H3 20px leading-tight mais botão circular 32 × 32, borda rgba(255,255,255,.2), raio 50%, chevron para baixo) e P a 12 de distância, 16px, 309 de largura; camada de hover absoluta em z-index:20, opacity de 0 a 1 em .3s cubic-bezier(.4,0,.2,1), mostrando a versão com o texto completo.

Normalizações aplicadas na tradução:

Divergências conhecidas vs. a referência: