/* ==========================================================================
   NEXUS theme-final / sections/portfolio.css
   Secao 9: portfolio de 14 produtos (eram 15 até o CEB IPes ser desativado em
   content.js) e um mostruário ESTÁTICO das verticais em .chip, acima da grade
   (era filtro clicável até a rodada "mostruário" — ver §2 e §9 abaixo para o
   histórico). O rail em marquee dos tipos saiu em 06/08/2026, por decisão do
   dono (ver §1). Territorio exclusivo deste agente junto com
   partials/portfolio.js.

   Desde agosto de 2026 TODO cartao tem moldura de midia: 1 a 3 imagens de
   galeria em crossfade (bloco 10.2) e, em onze produtos, um video que so
   toca sob o ponteiro. Tres rodadas mudaram a forma do cartao desde entao:
   a galeria trocou cena gerada por IA por mockup de tela real (content.js);
   o cartao em destaque (--feature, midia lado a lado com o texto) saiu por
   pedido do dono, que nao queria "big" nenhum na grade; e a dupla que fecha
   a cauda em faixa larga (bloco 5, `.portfolio__card--resumo`) deixou de ser
   escolhida por AUSENCIA de tagline/claim e passou a ser explicita por slug
   (FECHAMENTO_SLUGS, `partials/portfolio.js`) — CEB IPes (fechador pequeno,
   sem conteudo) e AQUANTA (faixa larga CHEIA: tagline + 3 claims + midia
   grande, rodada "Aquanta na faixa"). Hoje so existem duas formas: bloco 4
   (cartao simples, os 13 produtos regulares, midia em cima do texto, span 1
   sempre) e bloco 5 (faixa de fechamento, CEB IPes + Aquanta, esticada sem
   proporcao, e o unico ponto da grade com largura variavel, usado tambem
   para fechar a cauda sem buraco). O nome da classe (`--resumo`) ficou do
   mecanismo antigo por nao valer a pena renomear so por isso (territorio de
   outras peles tambem a referenciam, ex.: 09-skin-cult.css) — o que importa
   e o LAYOUT que ela ativa, nao o nome.

   Reaproveita .section, .section__head, .eyebrow, .lead (02-base/04-sections),
   .rail/.rail__track/.rail__item (04-sections), .grid-auto/.grid-auto--lg,
   .cluster, .cluster--between, .sr-only (02-base), .card, .card__header,
   .card__title, .card__desc, .card__footer, .badge, .badge--outline, .chip,
   .link (03-ui) e .media-frame (04-sections). Só CSS novo para o que é
   genuinamente específico desta seção: cartão de resumo com largura variável,
   badges de estado sem token novo, claims, nota e estado vazio de filtro.

   Só consome tokens de 01-tokens.css. Nenhum hex, nenhum px de raio, nenhuma
   família de fonte literal. Motion: só transform e opacity animam (§6);
   transições de cor seguem a mesma convenção já usada em 03-ui.css
   (.btn, .card, .chip), multiplicadas por var(--motion).
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Cabeçalho: gap interno perdido no wrapper

   ACHADO (agosto/2026, pedido do dono: "o portfólio tá colado diferente dos
   demais"). Medido por getBoundingClientRect em 1440px, escuro, pele
   default: o respiro da SEÇÃO (padding-block-start, a junção com a seção
   anterior) é IDÊNTICO ao de qualquer vizinho — 67,2px nas duas peles
   testadas (default e smooth, 80,28px nesta), zero diferença. O que
   diverge é DENTRO do cabeçalho: eyebrow→h2 e h2→lead mediam 16px em
   #tese/#capacidades/#confianca/#modelos (o gap de grade de
   .section__head, 02-base.css) e só 2px/0px aqui — praticamente colados.

   Causa: partials/portfolio.js embrulha eyebrow/h2/lead num <div> sem
   classe dentro de .section__head.section__head--split (a variante existe
   para "título+lead à esquerda, ação à direita" — aqui não sobrou ação
   nenhuma para a direita, só esse <div>). .section__head é display:grid
   com gap; grid só espaça FILHOS DIRETOS, e o filho direto único virou o
   <div>, não o eyebrow/h2/lead. Um <div> puro não tem gap nenhum por
   padrão, e 02-base.css zera a margem de p/h2 no reset — sobrava 0.

   Não é território desta folha mexer na estrutura (partials é de outro
   agente): a correção devolve ao <div> o MESMO grid + gap que
   .section__head já dá a filhos diretos em 02-base.css, uma linha, sem
   compensar com número novo. Depois da correção: 16px nos dois vãos, igual
   ao resto do site. */
.portfolio .section__head--split > div {
  display: grid;
  gap: var(--space-4);
}

/* --------------------------------------------------------------------------
   1. Rail de verticais (marquee) — REMOVIDO INTEIRO (06/08/2026)

   Decisão do dono, literal: "pode remover esse marquee por favor, não vamos
   usar". Saíram daqui as três regras que existiam (`.portfolio__rail`, com o
   `--rail-fade: 14%` que a rodada anterior tinha alargado,
   `.portfolio__rail-item` e o separador `::after`), e saiu de
   partials/portfolio.js o bloco `[data-marquee]` que as consumia, mais a
   função railItems(). Nada aqui ficou órfão: as três regras miravam
   exclusivamente o elemento removido.

   O RESPIRO QUE A FAIXA DAVA não se perdeu por acidente: ela tinha
   `margin-block: var(--space-10) var(--space-8)` entre o cabeçalho da seção
   e o mostruário de verticais. Sem ela, quem separa os dois é o
   `margin-block-start` do `.portfolio__toolbar` (bloco 2), ajustado nesta
   mesma rodada para o mesmo valor de topo que a faixa ocupava.

   FORA DESTE TERRITÓRIO, e por isso só registrado: `08-skin-surface.css`
   (linhas 1487, 1492 e 1504) e `10-skin-smooth.css` (linha 1621) ainda
   pintam `.portfolio__rail`/`.portfolio__rail-item`. As quatro regras
   ficam INERTES — sem elemento para casar, sem erro e sem pixel —, mesma
   tolerância já documentada aqui para `.portfolio__note`.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   2. Barra de ferramentas: mostruário de verticais

   ATÉ A RODADA "MOSTRUÁRIO" (agosto de 2026) esta barra tinha um filtro
   clicável, uma contagem sr-only (`.portfolio__status`, anunciava "N
   produtos" a cada troca de filtro) e um estado vazio (`.portfolio__empty`,
   "nenhum produto para esta vertical"). O dono pediu para tirar a
   interatividade (partials/portfolio.js, verticalItems()): sem clique, não
   há filtro para contar nem estado vazio para mostrar — os dois elementos
   saíram do HTML, e as regras deles saem daqui junto (nenhuma delas tinha
   outro consumidor, grep confirmado antes de apagar). `.portfolio__toolbar`
   fica só com a faixa de verticais dentro; `justify-content: space-between`
   também saiu por não ter mais um segundo item para empurrar para a ponta.
   -------------------------------------------------------------------------- */

/* `margin-block-start` (06/08/2026): herda o respiro que a faixa de marquee
   removida dava entre o cabeçalho da seção e o mostruário (ela tinha
   `margin-block: var(--space-10) var(--space-8)`, bloco 1). Sem isto a
   cápsula subiria e colaria no lead da seção. */
.portfolio__toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-block: var(--space-10) var(--space-8);
}

.portfolio__filters {
  --cluster-gap: var(--space-2);
  min-inline-size: 0;
}

/* --------------------------------------------------------------------------
   3. Grade
   .grid-auto/.grid-auto--lg já resolvem repeat(auto-fit, minmax(min(100%,
   var(--grid-min)), 1fr)); aqui só o valor de --grid-min é ajustado para a
   largura ideal de um cartão de produto.
   -------------------------------------------------------------------------- */

/* grid-auto-flow: dense por causa dos DOIS cartões de resumo (bloco 5.2, que
   ocupam a linha inteira via grid-column: 1 / -1): sem "dense", uma faixa de
   largura total no meio de uma sequência de cartões span 1 deixa as células
   da linha anterior definitivamente vazias, porque o algoritmo esparso só
   avança. Com "dense" os cartões span 1 seguintes voltam e preenchem essas
   células antes de a faixa forçar a próxima linha, que é exatamente o
   mecanismo que o bloco 5 usa para fechar a cauda sem cartão largo nenhum.
   A ordem de leitura do DOM e a de tabulação não mudam (a grade só
   reposiciona visualmente), e nenhum cartão fica fora da tela. */
.portfolio__grid {
  --grid-min: 320px;

  /* STRETCH, e agora ele e legitimo. Registro das tres etapas, porque a
     ordem delas e o argumento.

     Rodada 1: stretch com conteudo desigual. Todo cartao era esticado ate a
     altura do mais alto da linha e a folga ia parar DENTRO do cartao: 314px
     no AnteciPay (1280x800, 40,1% do cartao) e 557px na celula do CEB IPes
     na pele surface (1920x1080, 70,3%). Faixa morta.

     Rodada 2: `align-items: start`. Cada cartao passou a ter a altura do
     proprio conteudo. A faixa morta caiu para ~25px, mas a base da linha
     ficou irregular: 256px de dispersao em 1920x1080 na pele de vidro. Foi
     esta a leitura que o dono reprovou por escrito ("cada caixa esta de um
     tamanho"), com 550, 571, 596 e 745px na mesma linha.

     Rodada 3, esta: primeiro EQUALIZAR O CONTEUDO, depois voltar o stretch.
     Os tres equalizadores estao documentados onde moram:
       a) teto de tres claims visiveis por cartao (partials/portfolio.js);
       b) nota longa recolhida em <details> de uma linha (idem);
       c) tetos de linha em titulo, tagline e descricao (bloco 4 desta folha),
     e a folga que ainda sobra e DISTRIBUIDA dentro do corpo do cartao
     (`align-content: space-between`, bloco 4) em vez de empoçar depois do
     ultimo bloco. Distribuir e a saida que o proprio §12.2 do contrato ja
     exigia da pele de superficie: "ou a celula distribui o conteudo no
     espaco que sobra, ou a divergencia cai".

     Com o conteudo equalizado, stretch deixa de ser o problema e passa a ser
     a solucao: a base da linha fecha por construcao (dispersao zero) e o que
     sobra dentro do cartao e uma folga pequena, repartida.

     RODADA 4 (agosto de 2026): o cartão em destaque saiu (o dono pediu grade
     sem "big", todo cartão no mesmo formato — ver bloco 5, que documenta a
     remoção). Ele era também um equalizador estrutural (média e texto lado a
     lado, altura = a maior das duas). Sem ele, os três equalizadores de
     conteúdo acima são os únicos que sobram, e continuam suficientes: a
     dispersão de base medida depois da remoção está registrada no bloco 5. */
  align-items: stretch;
  grid-auto-flow: row dense;

  /* A grade mede a si mesma. É a única forma honesta de saber quantas
     trilhas existem de fato: o número depende do container (que muda de
     largura em 1560px e em 1900px pela rampa do §7.3), do gutter fluido e
     de --density, não da largura da janela. Com a container query abaixo a
     redistribuição dos destaques acompanha a grade real, e não um palpite
     de media query. */
  container-type: inline-size;
  container-name: portfolio-grid;
}

/* Fechamento da cauda da grade (nenhuma célula vazia no fim).

   RODADA 4 (agosto de 2026): SEM CARTÃO LARGO NENHUM. O dono pediu grade sem
   "big" — todo cartão no formato regular (bloco 4), span 1, mesma altura de
   linha. A rodada anterior fechava a cauda promovendo os cartões em destaque
   a 3 trilhas; sem destaque, essa alavanca não existe mais, e o fechamento
   passou para o único elemento que ainda tem largura variável por natureza:
   o PAR de cartões de resumo (bloco 5.2), que ordenar() (partials/
   portfolio.js) garante SEMPRE adjacente e por último.

   A CONTA. 15 produtos, 2 de fechamento (CEB IPes, Aquanta — por slug
   explícito desde a rodada "Aquanta na faixa", ver FECHAMENTO_SLUGS em
   partials/portfolio.js; era MAIA+CEB IPes por ausência de tagline/claim
   até a rodada anterior), sobram 13 regulares, todos span 1. Essa CONTA (13
   e 2) não muda com a troca: o Aquanta saiu do grupo regular e o MAIA
   entrou no lugar dele. Como 13 = 6×2+1 = 4×3+1 = 3×4+1, em QUALQUER contagem de
   colunas que esta grade alcança (2, 3 ou 4 — nunca mais que isso: o
   container trava em 1600px de --container-wide e --grid-min trava em
   320px, ver §7.3 de 02-base.css) os 13 cartões regulares fecham a própria
   última linha com exatamente 1 célula ocupada e (N-1) vazias.

   QUEM FECHA essas (N-1) células é o PRIMEIRO cartão de resumo, com
   grid-column: span (N-1) em vez do 1 / -1 padrão do bloco 5.2 (regra
   abaixo, por container query). Ele "gruda" ao lado do 13º cartão regular na
   mesma linha, com a MESMA largura de trilha (nenhuma distorção: um card de
   resumo com span 2 numa grade de 3 colunas mede exatamente 2 trilhas, igual
   a dois cartões regulares lado a lado). O segundo cartão de resumo, sem
   ninguém depois para fechar a própria linha, continua span 1 / -1 (largura
   total): é o único caso em que uma célula poderia sobrar, e não sobra
   porque ele É a linha inteira. `grid-auto-flow: dense` (bloco 3) é o que
   permite ao PRIMEIRO cartão de resumo entrar na célula ainda livre da linha
   anterior em vez de forçar sua própria linha nova.

   O seletor usa `:has(+ .portfolio__card--resumo)` para achar exatamente o
   PRIMEIRO dos dois (o que tem outro cartão de resumo logo depois), sem
   depender de posição numérica nem de slug — se um dia o catálogo ganhar um
   terceiro cartão de resumo, os dois primeiros continuam fechando cauda e
   só o último fica largura total, pela mesma regra.

   OS LIMIARES DE CONTAINER QUERY marcam onde a grade passa a caber 2, 3 e 4
   trilhas de --grid-min (320px) mais gap, e o gap NÃO é o mesmo nas cinco
   peles (--space-6 × --density nas quatro nomeadas, --hairline fixo só na
   surface): por isso o bloco 5 tem CINCO conjuntos de limiares, um genérico
   (a pele sem `data-skin`, index.html) e quatro escopados por
   `[data-skin="…"]`, cada um com o valor exato medido daquela pele — a
   tabela completa e o motivo de nunca arredondar para baixo estão lá, perto
   das regras. Abaixo do menor limiar de cada pele a grade tem 1 coluna só,
   onde toda linha tem exatamente 1 cartão e não existe célula para fechar (o
   cartão de resumo "fechador" nem precisa de span especial: 1/-1 já é a
   única coluna). Verificado por medição em 360, 390, 480, 600, 680, 700,
   760, 820, 860, 900, 1000, 1013, 1014, 1080, 1200, 1280, 1351, 1352, 1440,
   1560, 1728, 1900, 1920, 2200, 2560, 3200 e 3840, nas cinco páginas
   (index.html a index5.html): zero células vazias em qualquer largura, em
   qualquer linha da grade, em nenhuma das cinco peles.

   As regras estão no fim do bloco 5, e não aqui, por causa de cascata (ver
   a nota de especificidade lá: a exceção de span precisa perder para
   `.portfolio__card[hidden]`, senão o filtro por vertical deixa o cartão na
   tela). */

/* ATUALIZAÇÃO (06/08/2026): CEB IPES SAIU DO PORTFÓLIO (decisão do dono,
   content.js) E O MECANISMO DEIXOU DE DEPENDER DE IRMÃO NO DOM. A CONTA
   deste bloco (13 regulares + 2 de fechamento) valia enquanto havia DOIS
   cartões de resumo adjacentes; com o CEB IPes comentado em content.js,
   sobra só o Aquanta, sozinho no grupo de fechamento (FECHAMENTO_SLUGS =
   ['aquanta'], partials/portfolio.js). A conta continua fechando: 13
   regulares (inalterado) + 1 fechador que PRECISA encolher (span N-1) para
   ocupar exatamente as (N-1) células livres da última linha regular —
   exatamente o papel que o Aquanta já tinha quando estava emparelhado.

   O QUE MUDOU NÃO FOI O RESULTADO VISUAL DO AQUANTA (continua o mesmo
   cartão encolhido, layout vertical padrão do bloco 4), foi COMO A FOLHA
   RECONHECE ESSE PAPEL. `:has(+ .portfolio__card--resumo)` (usado em toda a
   história acima e nas regras originais deste bloco) só bate quando existe
   um SEGUNDO cartão de resumo logo depois no DOM — com o CEB IPes fora,
   nada vem depois do Aquanta, o seletor de irmão nunca casa, e o Aquanta
   cairia sem querer no papel de FAIXA LARGA (1 / -1), abrindo de volta o
   buraco de (N-1) células que este bloco inteiro existe para fechar.

   A CORREÇÃO: partials/portfolio.js calcula o papel (docking vs largo) de
   cada cartão de fechamento por ÍNDICE em FECHAMENTO_SLUGS, não por
   estrutura de DOM (ver isFechadorDocking(), lá) — regra generalizada: só o
   ÚLTIMO fechador de uma lista com 2+ pode ser faixa larga (o segundo de um
   par não compete por célula com ninguém); com 1 fechador só, ele é sempre
   docking, porque é sempre ele quem precisa fechar o resto da última linha
   regular. O resultado vira classe explícita no HTML
   (`.portfolio__card--resumo-docking` / `.portfolio__card--resumo-largo`),
   e todo `:where(:has(+ .portfolio__card--resumo))` / `:where(:not(:has(+
   .portfolio__card--resumo)))` deste bloco foi trocado pela classe
   correspondente — mesmo efeito de cascata, mesma especificidade (0,1,0,
   uma classe simples), sem depender mais de quantos cartões de resumo
   existem nem da ordem deles no documento. Se o catálogo ganhar de volta um
   segundo fechador (o próprio CEB IPes, ou outro produto), o mecanismo já
   sabe se comportar sem precisar de nenhuma edição aqui — só content.js/
   FECHAMENTO_SLUGS muda.

   Verificação em docs/shots/provas/provas-2026-08-06-selos-portfolio.md
   (matriz obrigatória do §7.1: 390, 768, 1280, 1440, 1920px). */

/* Cartão escondido pelo filtro: [hidden] nativo, reforçado para vencer o
   display: flex de .card (regra de cascata: seletor de atributo tem a mesma
   especificidade de uma classe, então o autor precisa declarar por cima).

   Esta regra vale 0,2,0 e TEM que continuar vencendo toda regra de layout
   desta folha, inclusive a exceção de span do cartão de resumo fechador
   (bloco 5.2). Medido em rodada anterior: com um seletor equivalente escrito
   solto (especificidade maior que 0,2,0), o `display` de layout vencia o
   `display: none` daqui e um cartão filtrado continuava visível. */
.portfolio__card[hidden] {
  display: none;
}

/* --------------------------------------------------------------------------
   4. Cartão de produto
   -------------------------------------------------------------------------- */

.portfolio__card {
  overflow: hidden;
  /* `position: relative` (06/08/2026, pedido do dono sobre o selo do CEB
     IPes "colado ao botão Acesso privado" — ver a nota grande em §9.1,
     mais abaixo, sobre o selo virar `position: absolute` no fechador de
     coluna estreita). Não muda nada por si só (nenhum filho não-fechador
     usa posicionamento absoluto), só declara o contexto de posicionamento
     que o selo do fechador precisa. */
  position: relative;
}

.portfolio__card:hover,
.portfolio__card:focus-within {
  border-color: hsl(var(--primary) / 0.45);
  transform: translateY(-2px);
}

/* O quadro de mídia não absorve mais folga de linha (flex: 1 0 auto virou
   flex: none). Absorver folga era o que empurrava a lista de marcadores
   para a base e abria a faixa morta; agora não há folga de linha para
   absorver, porque nenhum cartão é esticado.

   PROPORÇÃO 2 / 1, E ELA FOI ESCOLHIDA POR MEDIÇÃO, NÃO POR GOSTO.

   Antes desta rodada só seis cartões tinham mídia, e nesses seis a mídia fica
   LADO A LADO com o texto (bloco 5): custa zero de altura. Agora os quinze
   têm, e nos simples ela fica EM CIMA do texto, ou seja, entra inteira na
   altura da linha. Como a grade é `align-items: stretch`, cada pixel que a
   moldura acrescenta ao cartão simples reaparece como folga DENTRO do cartão
   em destaque da mesma linha, que continua com a altura do próprio texto.
   Isso não é hipótese: é o que a medição mostrou, e é exatamente o defeito
   que as três rodadas de padronização fecharam.

   Medido nas quatro combinações de prova (index/index2/index3, 1440x900 e
   1920x1080), pior vão interno de um cartão em destaque:

     16 / 10 (proporção do vídeo)   107, 120, 119 e 117px
     2 / 1   (esta)                  87,  97,  93 e  95px
     21 / 9                          76,  85,  78 e  83px

   (as quatro leituras são index 1440x900, index 1920x1080, index2 1440x900 e
   index3 1440x900, medidas com a mídia dos cartões simples removida do DOM
   para reproduzir a geometria anterior no mesmo documento.)

   2/1 fica na mesma casa dos 95px que a rodada 3 mediu e aprovou, e é o ponto
   onde o ganho para de compensar: 21/9 economiza mais 11px e cobra por isso um
   corte de 36% na altura de toda imagem e de 30% na do vídeo, ou seja, joga
   fora a área calma que é a composição inteira dessa direção de arte. O corte
   de 2/1 é de 25% na imagem (1200x800) e 20% no vídeo (1200x750), e cai em
   cima de céu, teto e piso, não do assunto.

   PIOR CASO DO CATÁLOGO, e ele não está na tabela acima: a pele de vidro em
   1920x1080 mede 119px, porque `--density: 1.14` estica o ritmo vertical do
   cartão simples sem esticar o texto do destaque na mesma proporção. Fica
   registrado como o número a bater se alguém voltar a este bloco.

   A DISPERSÃO DE BASE CONTINUA EM ZERO em todas as combinações medidas, antes
   e depois, que é a garantia que não podia ser perdida.

   Proporção declarada em CSS mais width/height no `<img>` são as duas metades
   da mesma garantia do §7.5: a caixa existe com a altura certa antes de
   qualquer byte de mídia chegar, então trocar de imagem, ligar o vídeo ou
   perder um arquivo para um 404 custa zero de salto de layout. */
.portfolio__card-media {
  --media-ratio: 2 / 1;
  border-radius: 0;
  border: 0;
  border-block-end: var(--hairline) solid hsl(var(--border));
  flex: none;
}

/* RODADA "TÍTULO TORTO" (agosto de 2026): O CABEÇALHO PAROU DE CRESCER, e é
   consertando uma regressão da rodada 3 registrada aqui mesmo (o parágrafo
   abaixo, mantido como histórico, é a decisão que causou o defeito).

   O RELATO DO DONO: numa mesma linha da grade, badge → título → meta liam
   com vão bem diferente de cartão para cartão (JX-AI e FireDash com vão
   grande, TerraVision e Aquanta compactos), mesmo os quatro tendo a MESMA
   sequência de zonas. Medido (getBoundingClientRect de cada zona, 1440x900):
   o vão badge→título variava de 12px (TerraVision) a 45px (JX-AI) dentro da
   MESMA linha da grade.

   A CAUSA: `flex: 3 1 auto` fazia o cabeçalho crescer junto com o corpo
   (`flex: 1` de .card__body) para preencher a altura esticada da linha, na
   proporção 3:1 — e a folga que cada cartão tinha para repartir dependia do
   PRÓPRIO conteúdo do corpo daquele cartão (quantos claims, se tinha nota),
   não da linha. Cartão de corpo raso (poucos claims) sobrava mais folga, o
   cabeçalho esticava mais, e `align-content: space-between` (grid) distribuía
   essa folga extra como vão A MAIS entre badge/título/meta/tagline — vão que
   variava cartão a cartão mesmo dentro da mesma linha da grade, exatamente o
   defeito relatado.

   A CORREÇÃO: cabeçalho sai do rateio de stretch (`flex: none`, altura =
   conteúdo próprio). As zonas internas (badge, título, meta, tagline) já têm
   altura mínima reservada por `min-block-size` (bloco 12a desta folha, de uma
   rodada anterior) e gap FIXO (`var(--space-3)`, token, não distribuído):
   com o cabeçalho não crescendo mais, essa reserva volta a valer sozinha, e
   dois cartões com a mesma forma (mesmas zonas presentes) ficam com o MESMO
   vão, ponto a ponto, não mais um valor que depende de quanto sobrou do
   corpo. Toda a folga de stretch passa a cair inteira em `.portfolio__card-
   body` (`flex: 1 1 auto`, abaixo), que já tinha o próprio
   `align-content: space-between` — o mesmo mecanismo, só que agora com toda
   a folga concentrada num único lugar consistente entre cartões de mesma
   forma, em vez de repartida de um jeito que dependia do conteúdo de cada um.

   HISTÓRICO (rodada 3, texto original, motivo por trás do "3 1 auto" que
   este bloco removeu): "O cabeçalho também cresce, e essa é a diferença
   entre folga REPARTIDA e folga só mudada de lugar. Com o crescimento
   apenas no corpo (flex: 1 de .card__body, 03-ui.css), toda a folga da
   linha caía nos um ou dois intervalos de lá: medido, um único vão de 224px
   dentro de um cartão de 678px." Essa lógica não estava errada — evitar UM
   vão morto grande é melhor que ter um só — só não previu que reservar 3
   partes de folga para o cabeçalho, numa grade onde o corpo varia muito de
   produto a produto (0 a 3 claims, nota presente ou não), faria o PRÓPRIO
   cabeçalho variar entre vizinhos de linha. */
.portfolio__card-head {
  gap: var(--space-3);
  flex: none;
}

/* BADGE + NOME NA MESMA LINHA (06/08/2026, pedido do dono: "'Em operação'
   está solto acima do nome"). Até esta rodada `.portfolio__card-top` só
   continha o badge (o contador sr-only que dividia a linha com ele saiu
   numa rodada anterior) — uma faixa quase vazia, lendo como vão morto
   acima do título. `produtoCard()` (partial) move o `<h3>` para dentro
   desta linha; `cluster--between` (02-base.css) já empurra os dois
   extremos, então só falta impedir que o título (que pode ter até 2
   linhas, `min-block-size` abaixo) empurre o badge para fora da margem
   quando ficar comprido. `align-items: flex-start` prende o badge no
   topo da primeira linha do nome, nunca centralizado entre 1 e 2 linhas. */
/* GAP MENOR ENTRE O NOME E OS CRACHÁS (06/08/2026, pedido do dono: "o vão
   entre o nome do produto e o par de crachás está grande demais; quero os
   dois próximos, mas respirando"). `--cluster-gap` governa aqui o
   ESPAÇAMENTO ENTRE AS DUAS LINHAS do cluster (o nome ocupa a primeira, o
   grupo de crachás pede `flex-basis: 100%` e cai sempre para a segunda —
   ver a nota abaixo), então baixar de `--space-3` para `--space-2` é
   exatamente o vão que ele apontou.

   O RESTO DO VÃO NÃO É GAP, E FICA REGISTRADO PARA NÃO SER "CORRIGIDO"
   DEPOIS: o nome tem faixa reservada de 2 linhas (bloco 12a), então num
   produto de nome curto sobra uma linha vazia entre o texto e os crachás.
   Essa linha é o preço do alinhamento pedido no MESMO dia ("não pode ficar
   título mais para cima e outro mais para baixo") — tirá-la devolveria o
   título torto entre vizinhos de grade. Medido: dos 14 produtos, os nomes
   longos ("Atlantis Technologies", "MAIA Investimentos", "Agrícola
   Wehrmann", "Cenix Tecnologia") ocupam de fato 2 linhas na coluna de
   ~320px, que é o maior caso real e por isso é a régua. */
.portfolio__card-top {
  --cluster-gap: var(--space-2);
  align-items: flex-start;
  /* nowrap -> wrap (06/08/2026, reforma editorial: o crachá de acesso
     virou um SEGUNDO crachá, ver `.portfolio__card-badges` acima). Dois
     crachás lado a lado ocupam mais largura que um só; com `nowrap` e o
     título em `min-inline-size: 0` (abaixo), o navegador não tinha pra
     onde jogar a sobra e passou a quebrar o NOME no meio do caractere
     ("FireDash" virava "FireDas"/"h") em vez de respeitar palavra —
     regressão pega em captura de tela, não em código (nenhum erro de
     console, só layout errado). `wrap` dá ao grupo de crachás uma
     segunda linha pra cair quando não cabe do lado do nome; no caso comum
     (nome curto, 1 crachá só) nada muda, os dois continuam na mesma
     linha. */
  flex-wrap: wrap;
}

.portfolio__card-top .card__title {
  flex: 1 1 auto;
  min-inline-size: 0;
}

/* Teto de duas linhas no nome do produto. Nenhum dos 15 nomes passa de duas
   linhas em nenhuma trilha da matriz do §7.1; o teto existe para que uma
   futura entrada longa em content.js não desalinhe a linha inteira da
   grade sem ninguém perceber. */
.portfolio__card .card__title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* Badge MENOR (mesma rodada, mesmo pedido): ao lado do título ele não
   precisa mais do porte de "faixa própria" que tinha sozinho na linha.
   `flex: none` e `white-space: nowrap` mantêm a largura intrínseca do
   texto do estado (hoje até "Em desenvolvimento"/"Em finalização", os
   mais longos do catálogo) mesmo quando o título ao lado é comprido e
   ocupa o resto da linha. */
.portfolio__card-top .portfolio__badge {
  flex: none;
  align-self: flex-start;
  padding-block: calc(var(--space-1) * 0.6);
  padding-inline: var(--space-2);
  font-size: calc(var(--fs-xs) * 0.94);
  white-space: nowrap;
}

/* CRACHÁ DE ACESSO (06/08/2026, reforma editorial, itens B/I da spec do
   dono — ver a nota grande em partials/portfolio.js/produtoCard()).
   `.portfolio__card-badges` é o SEGUNDO filho de `.portfolio__card-top`
   (`cluster--between` só sabe empurrar DOIS extremos; o grupo de crachás
   precisa ser um nó só para o nome continuar isolado à esquerda). Mesmo
   tratamento de tamanho do crachá de maturidade — os dois precisam
   colar como um par visual, não como dois pesos diferentes — e mesmo par
   `--border`/dashed que `.portfolio__badge[data-tone="privado"]` já usa
   pra "restrito", reaproveitado em vez de inventado (mesmo raciocínio de
   `.case-block__acesso`, sections/cases.css, o modelo copiado aqui). */
/* CRACHÁS SEMPRE NUMA LINHA PRÓPRIA, NUNCA "ÀS VEZES" (06/08/2026, achado do
   dono: no cartão do Projeto MEL o par "Em operação · Público" caía ABAIXO
   do nome, enquanto nos outros cartões ficava do LADO do nome — inconsistente
   entre cartões vizinhos). MEDIDO antes da correção, 1440px, tema escuro: dos
   15 cartões, 13 já quebravam para a segunda linha (FireDash, Projeto MEL,
   AnteciPay, Atlantis, JX-AI, LAYERS, MAIA, TerraVision, Wehrmann, Circuit
   Launch, Cenix, INCO-LATAM, CEB IPes) e só 2 ficavam do lado do nome
   (LegalX, Aquanta) — o "comum" relatado pelo dono já era minoria; a mistura
   é que lia como defeito.

   CAUSA: `.portfolio__card-top` (flex-wrap: wrap, acima) só quebra o grupo de
   crachás para a linha de baixo quando a LARGURA DISPONÍVEL não alcança nome
   + os dois crachás juntos — e essa soma varia produto a produto (nome curto
   como "LegalX" cabe, "Projeto MEL"/"FireDash" não, para o MESMO par de
   textos de crachá "Em operação"/"Público"). Depende do comprimento do nome
   e do rótulo de estado/acesso de cada produto, não de um defeito pontual de
   um cartão só — por isso "uniformizar" (pedido do dono) não tem solução por
   encolher texto (rótulo mais longo do catálogo é "Em desenvolvimento", e
   ainda existe "Acesso restrito"/"Projeto confidencial" do lado do crachá de
   acesso:nenhum teto de fonte razoável garante 1 linha nos 15 ao mesmo
   tempo).

   A CORREÇÃO escolhe o lado que a MAIORIA já mostrava: `flex-basis: 100%`
   faz o grupo de crachás pedir a largura inteira da linha, o que SEMPRE força
   a quebra (não importa o comprimento do nome nem dos rótulos) — os crachás
   ficam abaixo do nome em TODO cartão, sempre, sem exceção e sem depender de
   viewport. `flex-wrap: wrap` em `.portfolio__card-top` continua necessário
   (é o que permite essa segunda linha existir); só o comportamento
   "às vezes cabe do lado" saiu. */
.portfolio__card-badges {
  --cluster-gap: var(--space-2);
  flex: none;
  flex-basis: 100%;
}

.portfolio__badge-acesso {
  flex: none;
  align-self: flex-start;
  padding-block: calc(var(--space-1) * 0.6);
  padding-inline: var(--space-2);
  font-size: calc(var(--fs-xs) * 0.94);
  white-space: nowrap;
  background-color: transparent;
}

.portfolio__badge-acesso[data-tone="privado"] {
  border-style: dashed;
  color: hsl(var(--muted-foreground));
}

.portfolio__meta {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: hsl(var(--muted-foreground));
}

/* Tinta: --faint-foreground. `--border` NÃO é tinta de glifo, é divisor estático
   (o próprio 01-tokens.css diz isso na nota de --input), e usada em `color`
   media 1,00:1 na pele de vidro no escuro, ou seja, o ponto simplesmente não
   existia na tela; 1,25:1 no claro e 1,30:1 na pele padrão no escuro. Sem o
   separador a linha colapsa para "Água e saneamento Produto proprietário",
   sem nada dividindo os dois campos: é defeito de leitura, não cosmético.
   A subordinação do ponto continua existindo, e vem do tamanho do glifo e do
   espaço em volta, não de tinta ilegível. */
.portfolio__meta-sep {
  color: hsl(var(--faint-foreground));
}

.portfolio__tagline {
  font-family: var(--font-display);
  font-size: var(--fs-lead);
  font-weight: 600;
  line-height: var(--lh-tight);
  color: hsl(var(--foreground));

  /* Teto de TRÊS linhas. Existe porque a tagline mais longa do catálogo
     ("Dados orbitais. Inteligência territorial. Decisões operacionais.", do
     LAYERS) chega a quatro linhas numa trilha de 320px enquanto a maioria
     ocupa uma, e cada linha a mais desalinha a linha inteira da grade.

     Três e não dois, apesar de dois equalizar melhor: com dois, as taglines
     do LAYERS e do Atlantis apareciam cortadas no meio da frase ("Tecnologias
     replicáveis para soberania…"), e tagline é frase de marca, não parágrafo
     corrido. Três linhas não corta nenhuma das quinze em nenhum viewport da
     matriz do §7.1, e o teto continua valendo como rede contra uma entrada
     futura mais longa em content.js. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

/* A folga que sobra depois da equalização é REPARTIDA entre descrição e
   marcadores (a nota saiu do corpo, ver a nota grande no bloco do
   cabeçalho acima), em vez de empoçar depois do último bloco. É o que
   transforma o stretch de defeito em acabamento: desde a rodada "título
   torto" o corpo do cartão é DE FATO o único filho que cresce (`flex: 1` de
   .card__body em 03-ui.css: `.portfolio__card-head` virou `flex: none`,
   acima), então é aqui que TODA a folga de stretch chega, e `space-between`
   a divide pelos intervalos existentes em vez de deixar uma faixa morta
   única no pé — o mesmo raciocínio de antes, só que agora concentrado num
   lugar só em vez de repartido também pelo cabeçalho (que era a fonte da
   assimetria entre cartões vizinhos). */
.portfolio__card-body {
  gap: var(--space-5);
  flex: 1 1 auto;
  align-content: space-between;
}

/* Teto de TRÊS linhas na descrição (era quatro). É um dos equalizadores de
   altura, e é o que tira a diferença que sobrava entre cartões simples: as
   descrições do catálogo variam de duas a sete linhas na mesma trilha, e
   cada linha a mais vale uns 20px de base desalinhada.

   Três em vez de quatro porque a quarta linha era justamente a que só
   aparecia em metade dos cartões: com quatro, a diferença entre um cartão
   de descrição curta e um de descrição longa continuava valendo uma linha
   inteira dentro da mesma faixa da grade. Nenhum texto sai do documento:
   o parágrafo continua inteiro no DOM e é lido inteiro por leitor de tela,
   e o "Ver produto" leva ao texto completo do produto. Não confundir com
   "texto cortado" do §7.1, que é conteúdo estourando caixa por acidente.

   line-clamp sem prefixo entra junto por progressive enhancement: onde o
   motor já implementa a propriedade padronizada, ela vale; onde não,
   permanece a receita -webkit-, que é a que Chrome, Safari e Firefox
   entendem hoje. */
.portfolio__desc {
  font-size: var(--fs-sm);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

/* Cartão sem marcador nenhum: a descrição sobe de posto.

   Regra genérica, por presença de `.portfolio__claims` no DOM (produzido só
   quando `p.claims.length`), não por lista de produto — não precisa de
   manutenção quando o catálogo muda quem tem claim. Hoje só o CEB IPes cai
   nela (FireDash e MAIA ganharam 3 claims reais em rodadas posteriores, ver
   content.js); nesses cartões sem marcador a descrição é a única prosa que
   existe. Ela ganha o corpo de texto padrão e mais duas linhas de teto: é
   hierarquia honesta (sem lista, o parágrafo é o conteúdo) e é o terceiro
   equalizador de altura, porque encurta a distância entre o cartão mais
   magro e o vizinho de linha. */
.portfolio__card:not(:has(.portfolio__claims)) .portfolio__desc {
  font-size: var(--fs-base);
  -webkit-line-clamp: 5;
  line-clamp: 5;
}

/* Badge de estado: tom por atributo de dados, sem token de cor novo. A
   ressalva antiga apontava para um --success/--warning que ia ser criado em
   01-tokens.css; não vai: .badge--success e .badge--warning foram removidas
   de 03-ui.css §3, com a justificativa registrada lá. Este seletor já era a
   solução certa e continua sendo: "ativo" usa a própria --primary, os demais
   ficam neutros e se diferenciam por estilo de borda.

   O QUE MUDOU: "ativo" vira SUPERFÍCIE PINTADA, não tinta (defeito 2 da
   décima primeira rodada). A regra antiga escrevia --primary como `color`
   sobre o cartão, e ali o piso é 4,5 contra --card e contra --surface-1..3.
   A paleta não garante esse par: ela garante a primária como preenchimento
   (4,5 contra --primary-foreground) e como limite (3 contra o fundo). Em
   preset cromático o crachá caía para 2,5:1.

   O papel deste crachá é DESTAQUE, então a saída é a outra do §2.5: o
   elemento vira o preenchimento e o texto vai para o par que a paleta
   garante. É também o que separa melhor os dois tons, porque agora "ativo" e
   "privado" diferem em preenchimento e não só em espessura de filete. E cai
   junto o `hsl(var(--primary) / 0.5)` da borda, que era uma aposta sobre o
   que estava pintado atrás do crachá em cada pele.

   DUAS PRECAUÇÕES DE CASCATA, e as duas são cicatriz registrada no veredito.

   Primeira, `background` e não `background-color`: a pele de vidro pinta
   `.portfolio__badge` com `background: var(--glass-sheen) hsl(var(--card)/…)`,
   ou seja, com uma IMAGEM por cima da cor. Escrever só a cor deixaria o
   sheen vivo em cima da primária, que foi exatamente o defeito de
   07-studio.css:1123 (chip do painel a 1,02:1). O atalho zera os dois
   longhands de uma vez.

   Segunda, `.badge` entra no seletor só para pesar: a regra da pele mede
   0,2,0 e carrega DEPOIS desta folha, então um seletor de peso igual
   perderia por ordem. Com `.badge.portfolio__badge[data-tone]` a conta vai a
   0,3,0 e o preenchimento sobrevive nas três peles. Não é aumento de
   especificidade por gosto: é o mínimo que faz um par medido de contraste
   valer onde a pele repinta a superfície. */
.badge.portfolio__badge[data-tone="ativo"] {
  background: hsl(var(--primary));
  border-color: hsl(var(--primary));
  color: hsl(var(--primary-foreground));
}

.portfolio__badge[data-tone="privado"] {
  border-style: dashed;
  color: hsl(var(--muted-foreground));
}

.portfolio__claims {
  display: grid;
  gap: var(--space-2);
}

.portfolio__claim {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: hsl(var(--muted-foreground));
}

/* Teto de duas linhas por marcador. Junto com o teto de três marcadores
   (partials/portfolio.js), fixa a lista em no máximo seis linhas: sem ele,
   um único claim de três linhas ("Monitoramento de mais de 20 milhões de
   processos no Judiciário brasileiro") desfaz a equalização da linha
   inteira da grade. */
.portfolio__claim > span {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.portfolio__claim .nx-icon {
  flex: none;
  color: hsl(var(--primary));
  transform: translateY(0.15em);
}

/* ATUAÇÃO (06/08/2026, reforma editorial, item D da spec do dono).
   `p.atuacao` (partials/portfolio.js) é a string já formatada com
   interponto — texto de rodapé de corpo, mais discreto que os claims (que
   têm ícone de check e carregam o fato principal), então mono/pequeno/
   --muted-foreground, no mesmo registro visual de `.portfolio__meta`
   (mesma fonte, `--font-mono`) sem repetir a MESMA regra (a meta vive no
   cabeçalho, isto vive no corpo, depois dos claims — `card__body` já é
   `display: grid; gap: var(--space-4)`, então o espaçamento entre os dois
   blocos vem de graça, sem margin local). */
.portfolio__atuacao {
  font-family: var(--font-mono);
  font-size: var(--fs-mono);
  color: hsl(var(--muted-foreground));
}

/* `.portfolio__note*` REMOVIDO (agosto de 2026, rodada "título torto"). O
   disclosure "Nota ⌄" saiu do cartão por pedido do dono (partials/
   portfolio.js, notaHTML() removida) — as seis regras que viviam aqui
   (moldura do <details>, rótulo, chevron, texto e a correção do retângulo
   fantasma de contraste) perderam o alvo e viraram código morto.

   ORFÃOS FORA DESTE TERRITÓRIO, registrados para quem for mexer lá: o efeito
   `cult-popover` (assets/js/core/cult-fx.js, `alvo: '.portfolio__note'`) e o
   `x-extra-expandir` (assets/js/core/extra-fx.js, mesmo alvo) continuam
   catalogados, mas os dois casam zero elemento agora — não são território
   deste agente (core/*.js), e os dois já toleram seletor vazio (mesmo padrão
   `qsa(seletor)` que o resto do catálogo de efeitos usa), então não há erro
   de console nem exceção: os efeitos só ficam sem alvo para aplicar. */

.portfolio__link {
  font-size: var(--fs-sm);
}

/* CTA "ACESSO PRIVADO" (agosto de 2026). Os dois produtos sem link público
   (JX-AI e o cartão-resumo CEB IPes, `link: null` em content.js) ganham
   rodapé com este texto estático no lugar de "Ver produto" — antes eles
   simplesmente não tinham rodapé nenhum. NÃO usa `.link`: aquela classe
   desenha sublinhado permanente (`::after`) e estende a área de toque
   (`::before`) — as duas são pistas visuais de "isto é clicável" que aqui
   seriam falsas (marcação também não usa `<a>`: é `<span>`, sem href
   fantasma). A tinta usa `--muted-foreground`, o mesmo par já usado por
   `.portfolio__badge[data-tone="privado"]` (acima) para "não interativo,
   mas com o mesmo peso visual" — mesma decisão de contraste, mesmo token,
   reutilizada em vez de inventar uma nova regra local. */
.portfolio__link--privado {
  display: inline-flex;
  align-items: center;
  color: hsl(var(--muted-foreground));
  font-weight: 600;
  cursor: default;
}

/* --------------------------------------------------------------------------
   5. Cartão de fechamento: faixa de largura total, e fechamento de cauda

   RODADA 4 (agosto de 2026, histórico): O CARTÃO EM DESTAQUE SAIU. O dono
   pediu grade sem "big": os cartões com produto (tagline ou claim) são
   todos span 1, mesmo formato, mesma altura de linha, midia sempre em cima
   do texto (bloco 4). O que sobra com largura variável são só os DOIS
   cartões de fechamento abaixo, e é essa variação, já existente e aprovada
   pelo dono, que fecha a cauda da grade sem reintroduzir hierarquia nenhuma
   entre os cartões regulares. A conta de trilhas e os limiares de container
   query estão na nota grande do bloco 3 ("Fechamento da cauda da grade").

   RODADA "AQUANTA NA FAIXA" (agosto de 2026): quem entra na dupla mudou.
   Antes era MAIA + CEB IPes, os dois sem tagline/claim (isResumoProduto()),
   e a faixa larga (o segundo do par) ficava majoritariamente vazia por
   natureza — não tinha copy para distribuir. O dono pediu o oposto: a faixa
   larga PREENCHIDA, com o Aquanta (tagline + 3 claims publicados) no lugar.
   A dupla agora é CEB IPes (continua sem tagline/claim, o fechador PEQUENO)
   + Aquanta (faixa larga CHEIA), escolhida por SLUG explícito
   (FECHAMENTO_SLUGS, `partials/portfolio.js`, junto com a ordenação que
   garante os dois sempre adjacentes e por último nesta ordem — ordenar()).
   O MAIA, que perdeu o lugar do Aquanta no grupo, ganhou tagline/claims
   reais (content.js) e virou cartão regular, span 1, bloco 4 — a conta de
   "13 regulares + 2 de fechamento" não muda.

   O que esta folha faz é o layout, e ele não mudou com a troca de quem
   ocupa cada papel: por padrão, linha inteira e altura compacta; o PRIMEIRO
   dos dois, quando tem um segundo logo depois, encolhe o bastante para
   fechar a última linha dos cartões regulares em vez de abrir a própria
   linha. O card__header/card__body deste bloco renderizam tagline/claims
   normalmente quando existem (produtoCard() não distingue HTML por classe) —
   é por isso que o Aquanta, com os dois campos preenchidos, aparece com a
   copy inteira na faixa, sem precisar de marcação nova.

   Por que largura total é o padrão, e não um cartão mais curto: numa grade
   com `align-items: stretch` a altura de um cartão é a da LINHA, então um
   item com metade do conteúdo dos vizinhos só pode terminar de duas formas,
   com folga dentro dele ou com a base da linha desalinhada. Sozinho na
   própria linha, ele não tem vizinho para acompanhar: a altura volta a ser a
   do próprio conteúdo, sem faixa morta e sem dispersão — e no caso do
   Aquanta essa altura já vem naturalmente maior, porque o conteúdo dele
   (tagline + 3 claims + descrição) é o de um cartão regular completo, não o
   meio-conteúdo que a faixa foi desenhada para acomodar originalmente.
   -------------------------------------------------------------------------- */

.portfolio__card--resumo {
  grid-column: 1 / -1;
}

/* O FECHADOR NÃO ESTICA MAIS (agosto de 2026, rodada "MAIA em foco"). Bug
   relatado pelo dono: um dos dois cartões de resumo lia como "cartão em
   destaque" na grade, contradizendo a rodada 4 acima ("SEM CARTÃO LARGO
   NENHUM"). Medido em 1440x900: o cartão-fechador (o PRIMEIRO dos dois,
   `:has(+ .portfolio__card--resumo)`, regra abaixo) media 1009x823 —
   `align-items: stretch` (bloco 3) esticava esse cartão até a altura do
   vizinho de linha (um cartão comum de conteúdo cheio), porque ele COMPARTILHA
   linha com um cartão regular. Isso é 3,5x a área de um cartão comum
   (320x729 = 233.280px², contra 830.407px² do fechador), e é maior até que a
   outra faixa de resumo (a que fecha a própria linha sozinha, 1354x182 =
   246.428px², compacta porque não tem vizinho para ser esticada por). A
   ILUSÃO DE TAMANHO era do MECANISMO (esticar SEMPRE que há vizinho de
   linha), não do produto: independente de qual dos dois cartões de resumo
   cai na posição de fechador, ele ganharia essa altura emprestada.

   `align-self: start` tira ESTE cartão do rateio de `align-items: stretch`
   sem mudar nada para o vizinho regular da mesma linha (que não tem
   `align-self`, então continua herdando `stretch` do container) nem para a
   largura das trilhas (span continua vindo das regras de container query
   abaixo). O fechador passa a medir a altura do PRÓPRIO conteúdo — igual à
   faixa que já era compacta — e nenhum cartão de fechamento volta a ser a
   maior área da grade, seja ele o CEB IPes, o MAIA de quando ainda ocupava
   este papel, ou qualquer produto que um dia entre nesse grupo. */
.portfolio__card--resumo-docking {
  align-self: start;
}

/* SEGUNDA METADE DO MESMO BUG: `align-self: start` tirou o fechador do
   rateio de STRETCH, mas sozinho não bastava — medido depois de aplicar só
   aquela regra, o fechador continuava 773px de altura (idêntico ao vizinho
   de linha, por COINCIDÊNCIA de conteúdo, não porque parou de ser grande).
   A causa agora é outra: o fechador usa o layout vertical do bloco 4 (mídia
   em cima do texto, `.portfolio__card-media` com `aspect-ratio: 2 / 1`,
   bloco 4 acima), e esse 2/1 é dimensionado para um cartão de 1 coluna
   (~320px de largura → ~160px de mídia). O fechador span 2 ou span 3 chega a
   ~664-1352px de largura — a MESMA proporção 2/1 vira ~330-676px só de
   mídia, maior que um cartão regular inteiro.

   `max-block-size` trava a mídia do fechador numa fração de `--grid-min` (o
   token que define a largura de 1 trilha em toda a grade — não é número
   mágico, é o mesmo valor que já rege o resto do arquivo). Abaixo do teto,
   `object-fit: cover` (04-sections.css) já garante que a imagem cobre a
   moldura sem distorcer; só entra em jogo quando o span é 2 ou 3, porque em
   span 1 a largura real já é ~--grid-min e o teto quase não é atingido.

   TETO SUBIU DE /2 PARA ×0,85 (05/08/2026, pedido do dono — mesma rodada da
   inversão CEB IPes ↔ Aquanta em partials/portfolio.js/FECHAMENTO_SLUGS,
   "a imagem maiorzinha pra caber melhor as coisas"). Na altura antiga
   (--grid-min / 2 = 160px), a mídia deste fechador ficava MUITO mais
   letterboxed que o 2/1 nativo do arquivo (1600×800): span 2 a 1008px de
   container mede ~1008×160 = 6,3:1; span 3 a 1352px mede ~1352×160 = 8,45:1
   — uma tira fina, quase irreconhecível como a mesma arte da galeria. Em
   ×0,85 (272px) as mesmas larguras caem para 3,7:1 e 4,97:1, e em span 1
   (664px) para 2,44:1 — perto do 2/1 nativo, sem chegar lá (que exigiria
   remover o teto, e devolveria o bug do comentário acima: mídia maior que
   um cartão inteiro). O card continua `align-self: start` (regra acima):
   crescer a mídia não estica o fechador para acompanhar a linha, só torna
   ele mesmo mais alto — texto (badge/título/descrição/rodapé) segue com o
   próprio espaço reservado abaixo, sem disputa de layout. */
/* ×0,85 -> ×1 (06/08/2026, achado de varredura: "última linha da grade com
   alturas visivelmente desiguais" — o fechador compacto, hoje o AQUANTA
   desde a inversão CEB<->Aquanta, medido bem mais baixo que o cartão
   regular vizinho de linha, na pele surface e em outras). O fechador
   nunca vai IGUALAR exatamente um cartão regular por este teto sozinho —
   ele continua `align-self: start` (não estica pra acompanhar a linha,
   de propósito, ver a nota grande acima sobre a ilusão de "cartão em
   destaque" que essa mesma investida já causou uma vez) — mas uma mídia
   maior aumenta a altura NATURAL do cartão (mais pixel de moldura antes
   do texto começar), estreitando a diferença sem reintroduzir stretch.
   ×1 é o teto MÁXIMO que este comentário já considerava seguro (o texto
   histórico documentava ×1 como "voltaria o bug de mídia maior que um
   cartão inteiro" só na ausência do teto — com teto em ×1, a mídia do
   fechador span 3 fica do tamanho de --grid-min, ainda menor que um
   cartão regular inteiro, que soma mídia + cabeçalho + corpo + rodapé). */
.portfolio__card--resumo-docking > .portfolio__card-media {
  max-block-size: var(--grid-min, 320px);
}

/* O CARTÃO DE RESUMO FECHADOR: só o PRIMEIRO dos dois, só quando existe um
   segundo logo depois (`:has(+ .portfolio__card--resumo)`), só nas larguras
   de container onde os 13 cartões regulares terminam a própria linha com
   células livres (ver a conta no bloco 3). Fora dessas faixas — no menor
   limiar de cada pele abaixo, onde a grade ainda tem 1 coluna só e não
   sobra célula nenhuma para fechar — o cartão continua largura total pela
   regra acima.

   `:where()` em volta do `:has()` zera a especificidade extra: sem isso o
   seletor valia 0,2,0, empatava com `.portfolio__card[hidden]` (bloco 3) e,
   por vir depois no arquivo, passava a vencer o filtro por vertical — o
   mesmo defeito que o comentário do bloco 3 registra para a versão anterior
   deste mecanismo. Com :where(), o seletor volta a 0,1,0 e `[hidden]`
   continua imbatível.

   LIMIARES POR PELE, E POR QUE NÃO HÁ UM SÓ NÚMERO PARA AS CINCO.

   `repeat(auto-fit, minmax(320px, 1fr))` cabe N trilhas quando o container
   mede pelo menos N×320 + (N−1)×gap — é o PRÓPRIO limiar que o motor usa
   para decidir quantas trilhas cabem, então é ele, exato, sem arredondar,
   que entra no `min-width` abaixo (a fração de pixel importa: testado ao
   vivo, um limiar arredondado PARA CIMA "por segurança" abre uma janela real
   de largura onde a grade já tem a trilha nova mas a regra de span ainda não
   percebeu — exatamente o defeito que este bloco existe para evitar). O gap
   NÃO é igual nas cinco peles: é `var(--space-6)` (1,5rem × --density) nas
   quatro nomeadas abaixo, e `var(--hairline)` (1px fixo) só na pele surface
   (08-skin-surface.css via 12-secoes.css:750, "grade de células coladas").

     pele      --density  gap medido   N=2 em      N=3 em       N=4 em
     (nenhuma)      1     24px         664px       1008px       1352px
     glass       1,14     27,36px      667,36px    1014,72px    1362,08px
     cult        0,92     22,08px      662,08px    1004,16px    1346,24px
     smooth      1,12     26,88px      666,88px    1013,76px    1360,64px
     surface     n/a      1px (fixo)   641px       962px        1283px

   A pele sem nome (nenhum atributo `data-skin`, index.html) usa a regra
   genérica, sem seletor de pele: é a que "ganha" quando nenhuma das quatro
   scoped abaixo casa. As quatro scoped têm `[data-skin="…"]` na frente, que
   pesa mais que a genérica (0,2,0 contra 0,1,0, ambas com :where() por
   dentro) e por isso vencem sem depender de ordem no arquivo.

   SÓ `min-width`, SEM `max-width`, E É DE PROPÓSITO. A primeira versão desta
   regra usava faixas fechadas (`min-width: 666px) and (max-width: 1009px)`,
   depois `min-width: 1010px`...), com limiares em número inteiro. Mas a
   largura real do container é um FLOAT (medido: 1353,6px num container que
   `Math.round` mostrava como "1354"), e `1353,6px` não satisfaz nem
   `max-width: 1353px` nem `min-width: 1354px` — as duas faixas vizinhas
   ficavam com um intervalo de terra de ninguém entre elas, do tamanho do
   arredondamento, onde NENHUMA regra de span batia e o cartão caía de volta
   no `1 / -1` da regra base. Medido ao vivo: exatamente esse intervalo, a
   1440px de viewport nesta pele.

   A correção é não ter fronteira nenhuma para errar: cada faixa aqui é só
   `min-width`, sem teto, e as três estão em ORDEM CRESCENTE no arquivo. Num
   container largo o bastante, as três condições ficam verdadeiras ao mesmo
   tempo, e como a especificidade das três é idêntica, quem decide é a ORDEM
   NO ARQUIVO — a última regra que casar vence. Span 3 (o de maior
   min-width) está sempre por último dentro do seu grupo de pele, então
   sempre que ele bate, ele já venceu span 2 e span 1 pela cascata comum,
   sem depender de nenhum ponto de corte exato. */
@container portfolio-grid (min-width: 664px) {
  .portfolio__card--resumo-docking {
    grid-column: span 1;
  }
}

@container portfolio-grid (min-width: 1008px) {
  .portfolio__card--resumo-docking {
    grid-column: span 2;
  }
}

@container portfolio-grid (min-width: 1352px) {
  .portfolio__card--resumo-docking {
    grid-column: span 3;
  }
}

@container portfolio-grid (min-width: 667.36px) {
  [data-skin="glass"] .portfolio__card--resumo-docking {
    grid-column: span 1;
  }
}

@container portfolio-grid (min-width: 1014.72px) {
  [data-skin="glass"] .portfolio__card--resumo-docking {
    grid-column: span 2;
  }
}

@container portfolio-grid (min-width: 1362.08px) {
  [data-skin="glass"] .portfolio__card--resumo-docking {
    grid-column: span 3;
  }
}

@container portfolio-grid (min-width: 662.08px) {
  [data-skin="cult"] .portfolio__card--resumo-docking {
    grid-column: span 1;
  }
}

@container portfolio-grid (min-width: 1004.16px) {
  [data-skin="cult"] .portfolio__card--resumo-docking {
    grid-column: span 2;
  }
}

@container portfolio-grid (min-width: 1346.24px) {
  [data-skin="cult"] .portfolio__card--resumo-docking {
    grid-column: span 3;
  }
}

@container portfolio-grid (min-width: 666.88px) {
  [data-skin="smooth"] .portfolio__card--resumo-docking {
    grid-column: span 1;
  }
}

@container portfolio-grid (min-width: 1013.76px) {
  [data-skin="smooth"] .portfolio__card--resumo-docking {
    grid-column: span 2;
  }
}

@container portfolio-grid (min-width: 1360.64px) {
  [data-skin="smooth"] .portfolio__card--resumo-docking {
    grid-column: span 3;
  }
}

/* PELE SURFACE: gap fixo em --hairline (1px), não --space-6. Trilhas cabem
   bem mais cedo que nas outras quatro (ver a tabela acima), daí os limiares
   próprios e bem menores. */
@container portfolio-grid (min-width: 641px) {
  [data-skin="surface"] .portfolio__card--resumo-docking {
    grid-column: span 1;
  }
}

@container portfolio-grid (min-width: 962px) {
  [data-skin="surface"] .portfolio__card--resumo-docking {
    grid-column: span 2;
  }
}

@container portfolio-grid (min-width: 1283px) {
  [data-skin="surface"] .portfolio__card--resumo-docking {
    grid-column: span 3;
  }
}

/* O LAYOUT HORIZONTAL (mídia à esquerda, texto em três colunas) é só do
   cartão de resumo em largura TOTAL — na prática, sempre o segundo/último,
   único que nunca encolhe. O fechador, span 1 a 3 trilhas conforme acima,
   fica ESTREITO demais para o mesmo grid-template em muitas larguras (uma
   trilha só, ~320px, não cabe mídia + 3 colunas de texto): ele usa o layout
   vertical padrão do bloco 4, igual a qualquer cartão simples, só que com
   menos conteúdo no corpo. `:where(:not(:has(+ .portfolio__card--resumo)))`
   é a mesma proteção de especificidade do seletor acima, aplicada ao
   inverso (o segundo/último da dupla). */
@media (min-width: 860px) {
  .portfolio__card--resumo-largo {
    display: grid;
    grid-template-columns: minmax(0, 0.62fr) minmax(0, 1fr) minmax(0, 1.5fr) auto;
    align-items: center;
    /* TETO MÍNIMO (06/08/2026, pedido do dono: "CEB IPes no fechador
       ficou pequeno demais"). A INVERSÃO CEB IPes <-> AQUANTA (mesma
       rodada, FECHAMENTO_SLUGS em partials/portfolio.js) trocou QUEM
       ocupa esta faixa de largura total: era o Aquanta (tagline + 3
       claims + descrição, altura natural generosa, "mídia grande" já
       resolvida pelo próprio texto — histórico logo abaixo) e voltou a
       ser o CEB IPes (case privado: sem tagline, sem claim, só descrição
       de uma linha e "Acesso privado") — o texto mínimo que a nota
       histórica descrevia como o problema ORIGINAL desta faixa, antes de
       o Aquanta entrar. Medido: ~145-163px de altura total do cartão nos
       viewports testados, contra 680-696px do vizinho de linha regular —
       lê como uma tira achatada, não como faixa "grande".
       `min-block-size` dá um piso à FAIXA (não à mídia sozinha, que já
       acompanha via `block-size: 100%` abaixo): suficiente para a mídia
       parecer moldura de verdade, sem depender do texto ter volume. Não
       reintroduz o bug antigo do fechador COMPACTO virar "destaque"
       (align-self: start, bloco 5.2 acima) porque este seletor é o
       INVERSO — a faixa larga, que já era 1/-1 fixo por natureza, sem
       vizinho de linha para competir por tamanho. */
    min-block-size: 15rem;
  }

  /* A MÍDIA DA FAIXA NÃO PODE TER ALTURA PRÓPRIA, e essa é a única coisa que
     importa aqui.

     Histórico (até a rodada anterior): a faixa existia porque MAIA e CEB
     IPes tinham cerca de metade do conteúdo dos outros cartões, e a altura
     dela era a do próprio texto, quase sempre baixo. RODADA "AQUANTA NA
     FAIXA": a faixa agora é do Aquanta, com tagline + 3 claims + descrição —
     texto de cartão regular completo, não meio-conteúdo — então a altura
     natural dela já é maior por si só (é exatamente o "mídia grande" que o
     dono pediu: a moldura acompanha um texto mais alto, sem precisar de
     proporção própria para isso). O RACIOCÍNIO da regra não muda: uma
     moldura de proporção fixa nessa linha passaria a DITAR a altura da
     faixa em vez de acompanhar o texto — numa largura de container de
     1500px a coluna de mídia vale uns 300px, e 16/10 disso são 187px de
     piso, que já não bate com a altura real do texto em vários viewports.
     A faixa perderia a altura orgânica que a torna "cheia" em vez de
     "esticada por regra".

     Então aqui a moldura abre mão da proporção (`--media-ratio: auto`) e
     estica na linha (`align-self: stretch`, `block-size: 100%`), pelo mesmo
     motivo do cartão em destaque que existia até a rodada 4: quem dita a
     altura é o texto ao lado, a moldura acompanha, e o
     `object-fit: cover` de 04-sections.css corta a imagem sem distorcer. */
  .portfolio__card--resumo-largo > .portfolio__card-media {
    grid-column: 1;
    --media-ratio: auto;
    align-self: stretch;
    block-size: 100%;
    border-block-end: 0;
    border-inline-end: var(--hairline) solid hsl(var(--border));
  }

  .portfolio__card--resumo-largo > .card__header {
    padding-block-end: var(--space-6);
  }

  .portfolio__card--resumo-largo > .card__body {
    padding-inline-start: 0;
  }

  /* O rodapé perde o respiro de topo que faz sentido numa pilha vertical e
     que aqui só empurraria o link para fora do eixo dos outros dois blocos. */
  .portfolio__card--resumo-largo > .card__footer {
    padding-inline-start: 0;
  }

  /* SELO COLADO NO BOTÃO (06/08/2026, pedido do dono com captura do CEB
     IPes: "Acesso privado" e o selo brigando pelo mesmo espaço, na 4ª
     coluna — `auto`, largura de conteúdo — do grid de 4 colunas acima).
     Causa: nas outras 14 cartas o `.card__footer` é a largura INTEIRA do
     cartão (`justify-content: space-between`, §10.4, sobra de sobra entre
     botão e selo); aqui a coluna do rodapé é `auto` — mede só o que o
     BOTÃO precisa — porque as 3 primeiras colunas (mídia/cabeçalho/corpo)
     já reivindicam o resto da largura. `space-between` sem folga extra
     empurra os dois filhos pra junto um do outro, sem respiro.

     Regra do dono, aplicada aqui e não nas outras 14: "selo ancorado no
     canto inferior direito, nunca disputando espaço com botão". O selo sai
     do FLUXO do rodapé (`position: absolute`, ancorado no `.portfolio__card`
     via o `position: relative` de §4) e passa a MEDIR a partir do canto do
     CARTÃO inteiro, não da coluna estreita do botão — o mesmo respiro
     `--space-6` que já é o padding do rodapé nas outras cartas, agora como
     inset em vez de gap de flex. O botão sozinho no rodapé, sem disputa,
     senta no início da coluna (comportamento padrão de flex com 1 filho
     só). CONFERIDO: a OUTRA carta de resumo (a que estica span 2-3,
     `:has(+ .portfolio__card--resumo)`, hoje Aquanta) usa o layout vertical
     PADRÃO do bloco 4 — mesmo `.card__footer` de largura total dos
     cartões comuns, sem esta coluna estreita — não reproduz o problema,
     sem regra própria aqui. */
  .portfolio__card--resumo-largo .portfolio__logo-marca {
    position: absolute;
    inset-block-end: var(--space-6);
    inset-inline-end: var(--space-6);
  }
}

/* --------------------------------------------------------------------------
   6. Telas grandes: grade não fica rala em 4K
   -------------------------------------------------------------------------- */

/* Antes esta faixa subia --grid-min para 360px. O efeito era o oposto do
   pretendido: com os 1504px úteis do container-wide, 360px de trilha mínima
   fazem caber apenas 3 colunas (3 x 360 + 48 = 1128, e a quarta pediria
   1512). Em 1920x1080 a grade ficava com três colunas de 485px e a cauda
   sobrava com duas trilhas vazias. Mantendo os 320px de base cabem quatro
   colunas de 364px, a densidade certa para um monitor de 24", e a cauda de
   19 trilhas sobra em uma só. */
@media (min-width: 1900px) {
  .portfolio__grid {
    --grid-min: 320px;
  }
}

/* §7 (toque mínimo e rolagem) removida na rodada "mostruário", agosto de
   2026: as duas regras que existiam aqui (`.portfolio__toolbar` em coluna,
   `.portfolio__status` reordenado para o topo) resolviam a barra de
   ferramentas com DOIS filhos — a faixa de filtros e a contagem sr-only.
   `.portfolio__status` saiu (§2, acima) e `.portfolio__toolbar` ficou com
   um filho só; empilhar em coluna algo que já é o único item da linha não
   muda pixel nenhum, então a regra ficaria no arquivo sem consumidor real. */

/* --------------------------------------------------------------------------
   8. Título e lead: hierarquia

   "Melhorar o título", nesta rodada. Mesma correção aplicada em #govtech e
   em #contato, e ela é uma só para as três: o tracking de display serve a
   todos os portes e fica solto no porte de título de seção. O degrau é
   escrito como calc SOBRE --tracking-display, e é de um centésimo, porque o
   piso combinado é -0.04em: abaixo disso as hastes se tocam e a linha lê como
   apertada, que é trocar um defeito por outro.

   O lead passa a ser mais estreito que o título e medido na PRÓPRIA fonte
   (em, não ch do pai), para haver hierarquia entre as duas linhas em vez de
   dois blocos do mesmo peso e da mesma largura.
   -------------------------------------------------------------------------- */

.portfolio #portfolio-title {
  letter-spacing: calc(var(--tracking-display) - 0.01em);
  font-weight: 680;
  hyphens: manual;
  text-wrap: balance;
  max-inline-size: min(100%, 15em);
}

.portfolio .section__head--split .lead {
  max-inline-size: min(100%, 30em);
}

/* --------------------------------------------------------------------------
   9. Mostruário de verticais: cápsula, centralização, tinta e hover

   A padronização do .chip vive em 03-ui.css §4. O que sobra aqui é o ritmo
   da fileira e a única coisa que passou a ser TERRITÓRIO desta seção depois
   da rodada "mostruário" (agosto de 2026): a faixa deixou de ser um filtro
   clicável (partials/portfolio.js, verticalItems()) e virou uma
   ENUMERAÇÃO. `cursor: default` no `.chip` genérico impede que ele continue
   prometendo clique que não existe mais — mesmo princípio já registrado
   para `.link` em 03-ui.css ("um <span> com essa classe mentiria isto é
   clicável"). Nada nesta folha redeclara raio, padding ou altura do chip
   individual: só o que muda é a AFORDÂNCIA (cursor/hover), não a forma.

   CÁPSULA + CENTRALIZAÇÃO (06/08/2026, pedido do dono, três queixas juntas:
   "conteúdo alinha à esquerda com sobra à direita", "cor diferente demais
   dos outros designs", precisa de hover). As três nascem da MESMA causa: até
   esta rodada `.portfolio__filters` era só uma fileira solta de chips — sem
   contêiner próprio nenhuma pele além da smooth (10-skin-smooth.css, que já
   desenha uma calha com fundo/borda/raio) dava ao mostruário a LEITURA de
   "uma peça só" que o nome "cápsula" pede; nas outras quatro peles cada chip
   flutuava sozinho, então a fileira lia como "alinhada à esquerda com sobra"
   (não tinha contêiner para centralizar OU colorir). A correção é dar à
   faixa, aqui na folha COMPARTILHADA (vale para as peles sem tratamento
   próprio — glass/cult/surface/index sem pele — a smooth continua com a
   própria calha, 10-skin-smooth.css, que já cobria os três pontos), o
   MESMO par superfície/borda que qualquer outra cápsula do site usa e
   centralizar o conteúdo dentro dela — resolve "sobra à direita" (o grupo
   de chips passa a ocupar o centro da própria calha, que por sua vez
   centraliza na largura do toolbar).

   TOKEN NEUTRO, NÃO --surface-2/--border (revisitado 06/08/2026, pedido do
   dono com print: "contorno rosado" num preset de marca saturada — "essa
   cor tem que seguir um padrão indiferente de qual modelo for"). --border/
   --surface-2 são tingidos de propósito pelo matiz do FUNDO do preset em
   TODO o site (derive(), core/theme.js) — correto pra elevação de cartão,
   mas um preset como nubank (matiz 279°) tingia esse contorno fino o
   bastante pra ler como rosado em vez de cinza. `--chip-neutral-bg`/
   `--chip-neutral-border` (01-tokens.css) são a mesma dupla de valores,
   mas NUNCA escritos inline por preset nenhum — ver a nota completa lá.

   CADA ITEM VIRA PÍLULA DE VERDADE (mesma rodada, segunda queixa do dono:
   "tá sem botões, só escritos, tem que dar um jeito"). O `.chip` de
   03-ui.css (§4) usa `--chip-radius` pequeno — raio calibrado pra filtro
   CLICÁVEL noutro lugar do site, que não é o caso aqui (mostruário virou
   ENUMERAÇÃO, ver nota grande acima) — e em pelo menos uma pele
   (08-skin-surface.css, `.chip:not([aria-pressed="true"])`) o item fica
   com preenchimento TRANSPARENTE por design ("a borda é o preenchimento"),
   o que lido dentro da cápsula (sem contexto de "grupo de filtro ativo/
   inativo" que justifica esse tratamento alhures) lê como "texto solto".
   O seletor `.portfolio__filters .chip.portfolio__chip` (3 classes) tem
   especificidade maior que `[data-skin="X"] .chip` (atributo + 1 classe)
   de qualquer pele — vence a cascata independente da ordem de carga dos
   arquivos, sem `!important`. `--radius-full` aqui é seguro (ao contrário
   do que 10-skin-smooth.css documenta pra CALHA de 6+ linhas): cada chip
   é uma caixa baixa e larga por si só, nunca a caixa alta/estreita que
   fazia o raio morder o texto lá. */
.portfolio__toolbar {
  justify-content: center;
}

.portfolio__filters {
  row-gap: var(--space-2);
  justify-content: center;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-lg);
  border: var(--hairline) solid hsl(var(--chip-neutral-border));
  background-color: hsl(var(--chip-neutral-bg) / 0.6);
}

.portfolio__filters .chip {
  cursor: default;
}

.portfolio__filters .chip.portfolio__chip {
  border-radius: var(--radius-full);
  background-color: hsl(var(--chip-neutral-bg));
  border-color: hsl(var(--chip-neutral-border));
}

/* HOVER (mesmo pedido, item 4): item decorativo (não interativo — `<span>`
   sem tabindex, sem clique, ver a nota acima), então o hover pedido é "só
   de tinta/fundo leve", nunca borda/transform/foco — os três continuariam
   prometendo interação que não existe. Um wash sutil de `--primary` no
   fundo é feedback de "o cursor está aqui" sem fingir clicabilidade. */
.portfolio__filters .chip:hover {
  border-color: hsl(var(--chip-neutral-border));
  color: hsl(var(--muted-foreground));
  background-color: hsl(var(--primary) / 0.08);
  transform: none;
}

/* SOME NO MOBILE (06/08/2026, dono ao vivo com print: "os chips novos
   ficaram pesados no mobile... pilha de botões ocupando a tela" — a mesma
   pílula-com-cara-de-botão pedida acima, que resolveu desktop, empilha em
   6+ linhas de botão sólido em telas estreitas, exatamente o "quadrado"
   que o resto da missão trabalhou pra tirar do produto, só que agora em
   massa). Pedido do dono, latitude explícita: "poderia ser removido ou
   feito algo mais assimétrico, menor... a direção é sutileza". Decisão
   registrada: REMOVER no mobile em vez de miniaturizar. Duas opções foram
   comparadas — (1) encolher os chips (fonte/padding menores, ainda
   pílulas) ainda empilha em N linhas, só que mais fino, o "monte de
   botões" continua lendo como monte; (2) virar texto corrido sem
   caixa/borda, separado por interponto — mais sutil, mas 12 itens numa
   fonte legível em 360-414px de largura ainda quebra em 3-4 linhas de
   texto, e um bloco de texto corrido sem propósito de leitura (é
   mostruário decorativo, não um parágrafo) compete com o resto da faixa
   por atenção sem ganhar nada em troca. REMOVER é a única opção que
   resolve "pesado" com certeza, sem trocar um defeito por outro
   defeito-mais-fraco — o desktop, onde a cápsula tem espaço de sobra pra
   ser elegante, fica exatamente como está (nenhuma regra deste bloco muda
   acima do breakpoint). 39,9375rem (639px) é o mesmo território "celular"
   que o resto do site testa (a matriz do projeto vai até 414px de celular
   antes de saltar pra 768px de tablet — a faixa entre os dois nunca tem
   conteúdo próprio, então o corte cai com folga de qualquer lado). */
@media (max-width: 39.9375rem) {
  .portfolio__toolbar {
    display: none;
  }
}

/* "OUTROS" PERDEU A EXCEÇÃO (06/08/2026, pedido do dono: "'Outros' aparece
   sem a pílula que todos os outros têm; quero todos com a mesma marcação,
   sem exceção"). A regra que vivia aqui zerava borda e fundo do último item
   para ele ler como reticência da lista, não como categoria real — leitura
   correta, mas o dono decidiu por uniformidade. A classe
   `portfolio__chip--mais` saiu junto de partials/portfolio.js
   (verticalItems()), então nada aqui precisa de exceção nova: "Outros"
   entra pela mesma regra `.portfolio__filters .chip.portfolio__chip` dos
   outros dezoito.

   ÓRFÃO FORA DESTE TERRITÓRIO: `10-skin-smooth.css:1564` ainda tem uma
   regra para `.chip.portfolio__chip--mais`, que fica inerte; e a regra
   irmã de lá (`:not(.portfolio__chip--mais)`, linha 1554) passa a valer
   para TODOS os chips, inclusive "Outros" — que é exatamente o efeito
   pedido, sem precisar tocar naquele arquivo. */

/* --------------------------------------------------------------------------
   10. Efeitos

   10.1 (REENTRADA APÓS FILTRO) SAIU INTEIRA na rodada "mostruário", agosto
   de 2026. O `@keyframes nx-portfolio-entra` e a regra
   `.portfolio__grid[data-portfolio-entrando]` só existiam para reanimar a
   grade a cada clique de chip (reanimarGrade(), partials/portfolio.js) —
   sem filtro clicável (o dono pediu para tirar a interatividade da faixa de
   verticais, ver §2 acima e verticalItems() no partial), o atributo
   `data-portfolio-entrando` nunca mais é escrito, e a animação nunca mais
   dispara. `--nx-i` (o índice por cartão que alimentava o atraso da
   cascata) saiu do HTML junto (produtoCard(), partial) por não ter mais
   consumidor nenhum depois desta remoção — grep confirmado antes de tirar
   os dois. A entrada dos cartões continua existindo: `data-reveal` em cada
   `<article>` já é o motor GENÉRICO de revelação (core/motion.js), que
   nunca dependeu de filtro.
   -------------------------------------------------------------------------- */

/* 10.2 GALERIA EM CROSSFADE (1 A 3 IMAGENS), E O VÍDEO POR CIMA DELA.

   As imagens do produto já saem empilhadas de 04-sections.css
   (`.media-frame img { position: absolute; inset: 0 }`). O que esta folha faz
   é decidir qual delas está pintada, com que transição, e onde o vídeo entra.
   Só `opacity` e `transform` animam: a alternância roda CONTINUAMENTE, e o
   §6.0 é taxativo sobre esse caso ("os únicos permitidos em efeito que roda
   continuamente"). Nada de filter, nada de blur, nada de background.

   POR QUE A ATIVA SOBE DE CAMADA EM VEZ DE AS DUAS SE CRUZAREM NO MEIO.
   Crossfade ingênuo é a de saída indo de 1 a 0 enquanto a de entrada vai de 0
   a 1, e ele tem um defeito que não é de gosto: a meio caminho as duas estão
   em 0,5 e o que aparece na composição é 25% do fundo da moldura, ou seja, um
   piscar de `--muted` no meio de cada troca, treze vezes na grade (os 13
   produtos com mais de uma imagem; JX-AI e CEB IPes têm só 1, sem alternância).
   Aqui a
   imagem que ENTRA recebe `z-index: 1` e abre por cima; a que SAI só começa a
   apagar depois de `--dur-slow`, quando já está inteiramente coberta. Não há
   instante em que a soma das opacidades seja menor que 1, então não há dip.

   O deslocamento de fase que evita a grade piscar junta é do comportamento
   (partials/portfolio.js), não daqui: em CSS ele viraria um `--nx-i` a mais e
   uma animação por cartão, e animação não pausa fora da viewport.

   Sob `--motion: 0` e sob `prefers-reduced-motion` a duração vira zero por
   este cálculo e o JS para de alternar: fica a imagem 1, parada. */
.portfolio__card-media {
  /* Um só ponto de verdade para o passo de aproximação sob o ponteiro. Ele
     multiplica a escala das duas camadas em vez de existir como uma regra de
     hover à parte, senão a regra de hover e a de estado ativo disputariam a
     mesma propriedade `transform` com a mesma especificidade. */
  --shot-zoom: 1;
}

.portfolio__card:hover .portfolio__card-media,
.portfolio__card:focus-within .portfolio__card-media {
  --shot-zoom: 1.035;
}

/* 1.045 -> 1.02 (06/08/2026, pedido do dono: "um esmaecer moderno e
   discreto... nada chamativo" para a troca de imagem). O multiplicador é
   só o passo de entrada do CROSSFADE (a imagem que ainda não é a ativa
   "encolhe" de 1,02 para o zoom efetivo ao virar ativa); --shot-zoom (o
   zoom de HOVER do cartão, 1 a 1,035) não muda — os dois efeitos
   continuam multiplicados na mesma fórmula, só o passo de troca ficou
   mais sutil. */
.portfolio__shot {
  opacity: 0;
  z-index: 0;
  transform: scale(calc(var(--shot-zoom, 1) * 1.02));
  transition-property: opacity, transform;
  transition-duration: calc(var(--dur-slow) * var(--motion));
  transition-timing-function: var(--ease-io), var(--ease-out);
  transition-delay: calc(var(--dur-slow) * var(--motion)), 0s;
}

.portfolio__shot[data-ativo] {
  opacity: 1;
  z-index: 1;
  transform: scale(var(--shot-zoom, 1));
  transition-delay: 0s, 0s;
}

/* O vídeo cobre a galeria inteira quando toca, e é a única coisa acima dela.
   Em repouso ele é uma caixa transparente de zero byte: o partial não escreve
   <source> nenhum antes do ponteiro chegar, e o `poster` é o que aparece no
   intervalo entre o primeiro hover e o primeiro quadro decodificado.

   `object-position: top` (06/08/2026, pedido do dono: "o vídeo nasce alto,
   cortando o menu"). O arquivo gravado (scripts/gravar-produtos.mjs) já sai
   em 1200×750 (16/10 — VIDEO_DIMS, acima) e o PRIMEIRO QUADRO já é o topo
   real da página (o script rola até 0 antes de capturar o primeiro quadro;
   confirmado por amostragem: ffprobe + extração do quadro 1 de vídeos já
   gravados mostra o cabeçalho do site inteiro visível). O corte acontecia
   DEPOIS, só no CSS: `.portfolio__card-media` força `--media-ratio: 2 / 1`
   (mais baixo que o 16/10 do arquivo), e `object-fit: cover`
   (`.media-frame video`, 04-sections.css) sem `object-position` própria usa
   o padrão `center` — cortando ~75px em cima E embaixo do quadro de 750px
   para caber nos 600px da moldura, exatamente a faixa onde o menu do site
   gravado vive. `top` move o corte inteiro para a base: o topo do quadro
   (menu incluído) nunca é recortado, só o rodapé da rolagem captada. */
.portfolio__shot-video {
  z-index: 2;
  opacity: 0;
  object-position: top;
  pointer-events: none;
  transition: opacity calc(var(--dur-base) * var(--motion)) var(--ease-out);
}

.portfolio__card-media[data-video-ativo] .portfolio__shot-video {
  opacity: 1;
}

/* 10.4 MARCA D'ÁGUA, TESTE — SELO NO RODAPÉ (06/08/2026, pedido do dono, 4
   produtos: firedash/projeto-mel/layers/maia; só 3 ganharam `p.logoMarca`,
   LAYERS ficou fora por falta de asset, ver content.js).

   CORREÇÃO DE POSIÇÃO, MESMA RODADA: a primeira versão sobrepunha a logo em
   cima da mídia (canto inferior-esquerdo da moldura). O dono reprovou por
   captura — "o logo no topo sobre a imagem não é isso" — e a direção
   aprovada é o selo no CORPO do cartão, linha do CTA: "Ver produto" à
   esquerda, logo à direita da MESMA linha. `.card__footer` (03-ui.css) já é
   `display: flex; align-items: center`; só falta empurrar os dois filhos
   para as pontas quando o segundo (a logo) existe — `justify-content:
   space-between` não muda nada quando só há um filho (o botão sozinho já
   fica no início por padrão flex, igual a antes desta rodada).

   MONOCROMÁTICO/DESSATURADO + OPACIDADE REDUZIDA EM REPOUSO, COR PLENA NO
   HOVER DO CARTÃO (pedido literal): `grayscale(1)` remove a cor própria da
   marca (os 3 arquivos são coloridos), `opacity: 0.55` (dentro da faixa
   0,5-0,65 pedida) mantém o selo discreto. O gatilho de hover é o CARTÃO
   INTEIRO (`.portfolio__card:hover`/`:focus-within`), não o selo sozinho —
   mesma superfície de intenção que já rege o zoom da mídia (`--shot-zoom`)
   e a seta do link (§10.3): o cartão inteiro convida ao hover, então tudo
   que reage a ele reage junto, sem um segundo alvo de hover isolado dentro
   do rodapé.

   ALTURA POR SLUG, NÃO UM NÚMERO SÓ (pedido do dono, três ajustes na
   mesma rodada: 20-24px na primeira captura, "tá bem pequeno" na segunda
   — 30-34px —, e por fim "cuidado com a padronização — os logos têm
   proporções e pesos visuais diferentes, o da MAIA sai bem maior que os
   demais na mesma altura"). Uma única `block-size` para as 3 imagens
   iguala a altura do CANVAS (o arquivo PNG, com a margem transparente
   embutida), não a altura do GLIFO — e os 3 arquivos NÃO têm o mesmo
   respiro interno. Medido com `magick <arquivo> -trim` (bounding box real
   do conteúdo opaco, sem a margem transparente):

     produto        canvas      conteúdo real   conteúdo/canvas (altura)
     firedash       414×161     390×114         70,8%
     projeto-mel    756×224     732×176         78,6%
     maia           528×132     504×108         81,8%

   Isso sozinho já pediria alturas de CANVAS diferentes para igualar o
   GLIFO em ~30px (as três regras `--firedash`/`--projeto-mel`/`--maia`
   abaixo, cada uma com a conta de volta ao canvas). Mas a MAIA tem um
   segundo fator: é a proporção mais HORIZONTAL das 3 (4:1, contra 2,57:1
   do firedash e 3,375:1 do projeto-mel) — o mesmo bloco de altura de
   glifo ainda sai ~35-40% mais LARGO nela, pesando mais na composição por
   pura extensão horizontal (dois triângulos sólidos + wordmark, mais
   "tinta" que um contorno fino). Por isso a MAIA usa `inline-size` (não
   `block-size`) como eixo de controle — um TETO DE LARGURA, "logos
   horizontais compridos podem precisar de teto de largura em vez de
   altura" — calibrado para ficar no mesmo território de largura das
   outras duas (~103-125px de conteúdo real), aceitando abrir mão de
   um pouco de altura de glifo (a MAIA sai com traço mais grosso/cor
   sólida, então carrega peso visual equivalente mesmo ~5px mais baixa).

   `flex: none` tira o selo do rateio de `flex: 1` que o botão "Ver
   produto" (`.portfolio__link`) não tem por padrão, mas evita qualquer
   disputa futura de espaço. `align-items: center` (herdado de
   `.card__footer`, 03-ui.css) mantém o selo centralizado na MESMA linha
   do botão — "sem empurrar a linha" quer dizer sem quebrar para uma
   segunda linha, não sem crescer a altura da faixa: o rodapé cresce
   junto com o filho mais alto (comportamento padrão de flex), e nos
   quatro produtos testados sobra folga de sobra para isso em qualquer
   viewport da matriz, inclusive 390px. */
/* FAIXA RESERVADA PELO MAIOR SELO REAL (06/08/2026, pedido do dono: "tem
   que deixar o logo sabendo que pode ter 2 linhas, para ficar o tamanho
   legal para todos"; e a régua geral da mesma leva: "se tiver 3 linhas,
   todos têm que ter o espaço de 3 já setado").

   O RODAPÉ ERA A ÚLTIMA ZONA DESALINHADA, e a medição mostra por quê: a
   altura dele é a do filho mais alto, que é o SELO, e os selos têm alturas
   diferentes por construção (a régua por slug do bloco 10.4 iguala o peso
   do GLIFO, não a caixa do arquivo). Medido em 1440×900, os 14 cartões:
   rodapé de 49,8px (MAIA) a 70,9px (AnteciPay), 13 alturas distintas. Como
   `.portfolio__card-body` distribui a folga com `align-content:
   space-between`, essa diferença subia pelo cartão e deslocava a lista de
   características em até 9,7px entre vizinhos da MESMA linha da grade.

   A FAIXA É O MAIOR CASO REAL, não um número escolhido: `2.95rem` é a
   régua do selo mais alto do catálogo (AnteciPay, bloco 10.4) e
   `var(--space-6)` é o `padding-block-end` que `.card__footer` já aplica
   (03-ui.css; o `padding-block-start` é 0). A soma é exatamente a altura
   que o rodapé mais alto já tinha — nenhum cartão fica mais alto do que o
   mais alto de hoje, e os outros treze sobem para essa mesma faixa, com o
   selo centralizado nela (`align-items: center`, herdado).

   Se um dia um selo passar de 2,95rem de altura, esta conta acompanha
   sozinha? NÃO: é a única linha desta folha que precisa ser conferida junto
   com uma régua nova de selo. Fica dito aqui, ao lado do número, em vez de
   virar surpresa numa captura. */
.portfolio__card-foot {
  justify-content: space-between;
  min-block-size: calc(2.95rem + var(--space-6));
}

/* CHIP REMOVIDO (06/08/2026, pedido do dono na mesma rodada: "selos
   entrando com o quadrado de fundo... todos os 15 flutuando sem
   retângulo"). O chip de fundo neutro (`--muted`) que este bloco tinha
   ganhado minutos antes, pensado para resolver contraste de logo de
   tinta única (ver histórico abaixo), foi revertido: o dono viu o
   resultado (LegalX, Projeto MEL) e não quis nenhum retângulo atrás de
   logo nenhum, nem neutro.

   RISCO RESOLVIDO NUMA RODADA POSTERIOR, MESMO DIA: "tem que ter
   watermark diferente pra cada caso, se for light tem que ser um, se for
   dark tem que ser outro" — não mais um filtro cego (invert()), e sim DOIS
   ARQUIVOS por produto (ver a nota grande de logoMarcaHTML(),
   partials/portfolio.js): `<slug>-claro.png` (tinta escura, calibrado pro
   tema claro) e `<slug>-escuro.png` (tinta clara, pro tema escuro). Os
   dois `<img>` nascem no DOM como irmãos (`data-tema-logo="claro"` /
   `"escuro"`); a troca abaixo é só display:none/flex por `[data-theme]`,
   sem JS e sem media query — o mesmo atributo que já pinta o resto do
   site em claro/escuro, inclusive no modo "sistema" (core/theme.js
   sempre escreve `data-theme` explícito, nunca deixa a decisão pra
   `prefers-color-scheme` sozinha). Nos logos coloridos multicanal (que já
   liam bem nos dois temas) os dois arquivos podem ser quase idênticos —
   só o ajuste fino de luminância (ver process de geração, não repetido
   aqui) muda pixel a pixel, não a composição.

   HISTÓRICO (por que o chip existiu por um instante): Aquanta e Cenix são
   BRANCAS (hero escuro), LegalX é ESCURA (fundo claro) — `--card` é
   branco no tema claro e quase preto no escuro (01-tokens.css), então
   qualquer logo de tinta única SOME contra um dos dois SE FOR UM ARQUIVO
   SÓ. O chip resolvia isso escondendo o problema atrás de um retângulo; o
   par claro/escuro resolve na raiz, sem retângulo nenhum. */
.portfolio__logo-marca {
  flex: none;
  filter: grayscale(1);
  opacity: 0.55;
  transition:
    opacity calc(var(--dur-fast) * var(--motion)) var(--ease-out),
    filter calc(var(--dur-fast) * var(--motion)) var(--ease-out);
}

/* Só um dos dois `<img>` por vez — o outro nem ocupa espaço no layout
   (`display: none`, não `visibility`/`opacity`, pra não disputar o rateio
   de `.card__footer` com um irmão invisível). Regra dupla por atributo
   (não por tema) porque o padrão é "esconde por padrão, mostra quando
   bate" nos dois sentidos — sem isso, um teria que ser o fallback do
   outro e a ordem no DOM passaria a importar. */
.portfolio__logo-marca[data-tema-logo="claro"] { display: none; }
.portfolio__logo-marca[data-tema-logo="escuro"] { display: none; }
[data-theme="light"] .portfolio__logo-marca[data-tema-logo="claro"] { display: inline-block; }
[data-theme="dark"] .portfolio__logo-marca[data-tema-logo="escuro"] { display: inline-block; }

/* TAMANHO POR SLUG — família inteira recalibrada numa única rodada
   (06/08/2026, pedido do dono: "MEL e FireDash são o padrão aprovado",
   LegalX "tá feio" com placa (corrigida, ver nota do arquivo em
   content.js), Cenix com borda visível (limpa, mesmo motivo), Wehrmann
   "pequena demais" e MAIA "muito grande"). Mesma medição de sempre —
   `magick <arquivo> -fuzz 2% -trim` mede o CONTEÚDO real contra o CANVAS
   do arquivo — mirando ~30px de altura de glifo pros wordmarks e um
   pouco menos (~26-28px) pras marcas mais quadradas/tipo-ícone, que pesam
   mais por área a mesma altura. Lockups compridos (ícone + wordmark de
   2 palavras, ou wordmark sem ícone com 10+ caracteres) são controlados
   por `inline-size` (teto de LARGURA) em vez de altura — mesma lógica já
   usada pela MAIA desde a rodada anterior — porque dimensionar pela
   altura os deixaria mais largos que o próprio rodapé do cartão. */

/* 42,4px de canvas × 70,8% de conteúdo real ≈ 30px de glifo. */
.portfolio__logo-marca--firedash {
  block-size: 2.65rem;
  inline-size: auto;
}

/* 38,2px de canvas × 78,6% de conteúdo real ≈ 30px de glifo. */
.portfolio__logo-marca--projeto-mel {
  block-size: 2.4rem;
  inline-size: auto;
}

/* 7,5rem -> 6,5rem (06/08/2026, "muito grande — reduzir um pouco", ~13%
   menor). Eixo de controle continua LARGURA, não altura (lockup mais
   horizontal dos 15, 4:1) — teto de 6,5rem (104px) de canvas ≈ 99,3px de
   conteúdo real. `block-size: auto` deixa a altura cair onde a proporção
   intrínseca do arquivo mandar. */
.portfolio__logo-marca--maia {
  inline-size: 6.5rem;
  block-size: auto;
}

/* 36px de canvas × 83,3% de conteúdo real ≈ 30px de glifo. */
.portfolio__logo-marca--aquanta {
  block-size: 2.25rem;
  inline-size: auto;
}

/* Recalibrado depois da limpeza de borda (era 1,875rem sobre um arquivo
   com ~13% de haze de baixa opacidade nas bordas — o "com borda visível"
   que o dono reportou; arquivo saneado, ver assets/img/logos/cenix-*.png).
   O arquivo limpo ganhou padding interno de verdade (8%, mesmo padrão dos
   outros); 2,86rem de canvas × 65,5% de conteúdo real ≈ 30px de glifo —
   mesmo alvo de sempre, só a MEDIÇÃO mudou porque o arquivo-fonte mudou. */
.portfolio__logo-marca--cenix {
  block-size: 2.86rem;
  inline-size: auto;
}

/* SEGUNDA RECALIBRAGEM (06/08/2026, dono olhando o site ao vivo:
   "faltando no ar" — não estava ausente do DOM, media 41×30px, quase
   imperceptível ao lado do botão). Lockup quase quadrado (1,346:1, ícone +
   "CIRCUIT LAUNCH" em duas linhas), conteúdo real ≈98,6% do canvas (quase
   sem respiro interno) — a essa proporção, 1,9rem de canvas já entregava
   ~30px de glifo, mas 30px de ALTURA numa marca de 1,35:1 ainda é só ~41px
   de LARGURA, um alvo minúsculo perto dos 100-190px do resto da família.
   2,6rem de canvas ≈ 41,6px de glifo, ~56px de largura — mais visível sem
   virar o maior selo do catálogo.
   [HISTÓRICO. A medição de "conteúdo ≈98,6% do canvas" estava ERRADA, e é
   ela que explica por que o dono continuou vendo o selo sumido — ver
   abaixo.] */
/* ARQUIVO REFEITO: O SELO NÃO ESTAVA "SEM SELO", ESTAVA MINÚSCULO DENTRO
   DE UM CANVAS DE PÁGINA INTEIRA (06/08/2026, terceiro relato do dono:
   "circuit-launch está SEM selo no cartão").

   A CAUSA, medida: o arquivo importado era o screenshot de página inteira
   do site do cliente (2400×1783), com a marca ocupando só os 477×131 do
   canto superior esquerdo. A auditoria anterior mediu o conteúdo com
   `magick -trim`, que enxergou 2388×1758 e concluiu "conteúdo = 98,6% do
   canvas" — o trim foi enganado por uma névoa de alfa quase zero espalhada
   pelo screenshot inteiro. Com `block-size: 2.6rem` (41,6px de canvas), a
   marca real renderizava a 41,6 × 131/1783 = 3,1px de altura. Três pixels:
   por isso lia como ausente, e por isso duas recalibragens de tamanho não
   resolveram — o problema nunca foi a régua, era o arquivo.

   A CORREÇÃO: `assets/img/logos/circuit-launch-{claro,escuro}.png` refeitos
   a partir dos próprios arquivos, recortando a região da marca (bbox por
   limiar de alfa em 6%, que separa a marca da névoa) e recompondo com 14px
   de respiro. Canvas novo: 505×159, conteúdo real 477×131 (medido com
   `-trim` DEPOIS da limpeza, agora confiável).

   O TAMANHO: 2,2rem (35,2px) de canvas ≈ 29px de glifo (35,2 × 131/159) e
   ~112px de largura — dentro da faixa da família nos dois eixos, ao
   contrário do "56px de largura" da régua antiga. */
.portfolio__logo-marca--circuit-launch {
  block-size: 2.2rem;
  inline-size: auto;
}

/* TERCEIRA RECALIBRAGEM (06/08/2026, dono ao vivo, dois achados juntos: 1.
   ícone "L" longe do wordmark, gap de 55px contra 41px de largura do
   próprio ícone — herança do espaço que a PLACA removida ocupava, não algo
   que a remoção de fundo por si só causasse; 2. "maior que os demais",
   consequência do mesmo arquivo largo). Corrigido o arquivo primeiro
   (assets/img/logos/legalx-{claro,escuro}.png recompostos: ícone e texto
   recortados separadamente e remontados com um gap proporcional, ~22% da
   altura do ícone, em vez do gap herdado da placa — dono autorizou cair
   pra só o ícone "se não desse pra aproximar com naturalidade", mas deu).
   Arquivo novo: canvas 318×102, conteúdo 274×58 (56,9% de altura). Alvo
   revisado pra 27px (era 30px): 2,95rem de canvas × 56,9% ≈ 27px de
   glifo, 127px de conteúdo largura — dentro do território da família,
   sem teto de largura. [HISTÓRICO: essa era a régua até a rodada abaixo.] */
/* QUARTA RECALIBRAGEM, EIXO TROCADO (06/08/2026, dono ao vivo com nova
   captura: "grande demais, e desceu" — o "desceu" não é desalinhamento de
   `align-items`, é COLAPSO DE LINHA. Medido: `.card__footer` é
   `display:flex; flex-wrap:wrap` (03-ui.css) e o par CTA + selo só cabe
   lado a lado quando a LARGURA dos dois soma menos que o rodapé disponível;
   em LegalX o CTA é "Solicitar demonstração" (184-194px, o mais longo dos
   14 rótulos ativos) e o selo media 147px — a soma estourava o rodapé do
   cartão span 1 em QUASE TODA largura da matriz do §7.1: 390px (100px
   disponíveis), 768px (97px), 1280px (139px, folga real) e 1920px (103px),
   sempre por §7.1 obrigatório, `sem pele`. Só sobrava lado a lado num
   ponto de sorte (1024px, coluna larga o bastante). Onde não cabe, o selo
   cai pra segunda linha do rodapé — e um `<img>` de 47,2px de altura numa
   segunda linha lê exatamente como "desceu" na captura do dono.

   A régua de altura (27px de glifo) media o alvo errado: um lockup deste
   formato (3,12:1) precisa de ORÇAMENTO DE LARGURA, não de altura — a
   régua anterior controlava altura e deixava a largura (147px) livre para
   estourar o rodapé. Troca de eixo: `inline-size` fixo, `block-size: auto`
   (mesmo padrão já usado por MAIA/Atlantis/TerraVision/INCO-LATAM, os
   outros lockups compridos da família).

   TAMANHO: 5,4rem (86,4px) de canvas — cabe nos quatro viewports do §7.1
   citados acima com folga (menor uso é 768px, 86,4 contra 97px
   disponíveis, ~11px de sobra). Conteúdo real: 86,4 × 86,2% (274/318, a
   MESMA proporção de largura do arquivo, medição herdada da rodada
   anterior) ≈ 74,5px; glifo cai para ~15,8px de altura (274:58 = 4,72:1 de
   proporção do conteúdo). É bem menor que o "família padrão" de ~27-30px
   — concessão aceita aqui: o lockup de LegalX é o mais horizontal da
   família depois de Atlantis/TerraVision, e o rótulo do CTA ao lado dele é
   o mais longo do catálogo; não há tamanho de glifo nessa faixa que
   resolva "grande demais" e "desceu" ao mesmo tempo sem control por
   largura. 1440px (75px disponíveis, o mais apertado da matriz) continua
   sem caber ao lado do CTA mesmo reduzido — mas ali 7 outros produtos
   (Projeto MEL, FireDash, AnteciPay, TerraVision, Cenix, Atlantis,
   INCO-LATAM) TAMBÉM quebram linha pelo mesmo motivo estrutural (coluna
   span 1 mais estreita da grade, ~321px, contra o CTA mais rótulo mais
   selo de qualquer produto): LegalX deixa de ser o único cartão "quebrado"
   da tela, que era o defeito relatado — passa a quebrar só onde a maioria
   do catálogo também quebra, não mais sozinho. */
.portfolio__logo-marca--legalx {
  inline-size: 5.4rem;
  block-size: auto;
}

/* SEGUNDA RECALIBRAGEM (06/08/2026, dono ao vivo: "ainda está pequeno —
   aumentar mais", depois do primeiro ajuste 1,875rem -> 2,2rem já não ter
   bastado). 2,2rem -> 2,6rem (mais ~18%, ~38% acima do valor original).
   Arquivo sem respiro interno (conteúdo = 100% do canvas), então o número
   de canvas É o glifo: 41,6px — o maior glifo do catálogo depois desta
   rodada, deliberado (o dono pediu duas vezes seguidas). */
.portfolio__logo-marca--wehrmann {
  block-size: 2.6rem;
  inline-size: auto;
}

/* EXPANSÃO PARA O CATÁLOGO INTEIRO, 7 PRODUTOS QUE FALTAVAM (06/08/2026,
   "watermark aprovada, expanda para os 15" + revisitado no mesmo dia: o
   dono confirmou os 7 nominalmente — antecipay, layers, jx-ai, atlantis,
   terravision, inco-latam, ceb-ipes). Assets, procedência e recorte de
   cada um documentados no comentário de `logoMarca` do produto em
   content.js; aqui só o tamanho. Mesma medição/alvo do bloco acima
   (~30px de glifo pra wordmark, ~26-28px pra marca tipo-ícone, teto de
   LARGURA em vez de altura pros lockups compridos). */

/* RECALIBRADO (06/08/2026, dono ao vivo: "pequeno demais — aumentar").
   9,08rem -> 10,5rem de teto de largura (+16%) — ícone + wordmark de 2
   palavras (hexágono + "ATLANTIS TECHNOLOGIES"), lockup comprido, mesmo
   tratamento de teto de largura da MAIA. 10,5rem de canvas (168px) ≈
   144,7px de conteúdo real — acima do território "família" original
   (100-130px) por pedido explícito, junto com CIRCUIT LAUNCH e WEHRMANN
   os únicos deliberadamente acima da média nesta recalibragem.
   [HISTÓRICO: essa era a régua até a rodada abaixo, e ela media a coisa
   errada — ver o número de glifo lá.] */
/* ARQUIVO RECORTADO E SELO AUMENTADO (06/08/2026, pedido do dono:
   "Atlantis precisa ser RECORTADO para conter só a palavra Atlantis, sem
   Technologies, e depois AUMENTADO para ficar do mesmo peso visual dos
   irmãos").

   POR QUE ELE PARECIA MENOR MESMO COM O MAIOR TETO DE LARGURA DA FAMÍLIA:
   a régua de largura não diz nada sobre o tamanho do GLIFO quando o lockup
   é comprido. Medido no arquivo antigo (847×192 de canvas, conteúdo
   729×74): com `inline-size: 10.5rem` (168px), a altura renderizada era
   168 × 192/847 = 38,1px de CANVAS, e o glifo dentro dela media 74/192
   disso = 14,7px. Os irmãos rodam entre 24 e 30px de glifo. Ou seja, o
   selo mais LARGO do catálogo era, ao mesmo tempo, o de letra mais BAIXA —
   e "Technologies" (a metade que o dono mandou cortar) era justamente o
   que consumia a largura.

   O RECORTE: `assets/img/logos/atlantis-{claro,escuro}.png` refeitos a
   partir dos próprios arquivos (corte em x=452 do canvas de 847, onde o
   perfil de alfa por coluna zera entre "ATLANTIS" e "TECHNOLOGIES"),
   `-trim` e recomposição com 14px de respiro. Canvas novo: 402×102,
   conteúdo real 374×74 — o ícone hexagonal FICA (é metade do lockup e o
   que dá reconhecimento à marca; o pedido nomeia a palavra "Technologies"
   como o que sai).

   O TAMANHO: 9,2rem (147,2px) de canvas ≈ 27,1px de glifo (147,2 × 74/402)
   e 137px de conteúdo real de largura. 27px é o alvo da família para
   wordmark, e a largura fica dentro da faixa dos irmãos (100 a 145px),
   então o rodapé não estoura. Eixo continua sendo LARGURA, pelo mesmo
   motivo de sempre em lockup comprido. */
.portfolio__logo-marca--atlantis {
  inline-size: 9.2rem;
  block-size: auto;
}

/* Ícone de satélite + "TERRAVISION" (11 caracteres) — lockup ainda mais
   comprido que o da ATLANTIS (8,3:1 de conteúdo). Mesmo eixo de largura:
   8,73rem de canvas (139,7px) ≈ 120px de conteúdo real.
   [HISTÓRICO: essa era a régua até a rodada abaixo.] */
/* REDUZIDO (06/08/2026, dono ao vivo: "reduzir um pouco"). 8,73rem ->
   7,65rem (~12,4% menor) — mesmo eixo de largura, sem trocar de eixo (ao
   contrário do LegalX/nota grande acima, aqui não havia queda de linha:
   medido no rodapé real do TerraVision nos cinco viewports do §7.1, o par
   CTA + selo cabia lado a lado em quatro deles (390/768/1280/1920px,
   130-162px disponíveis contra 140px do selo antigo) e só quebrava em
   1440px (125px disponíveis, coluna mais estreita da grade) — o mesmo
   ponto onde outros 7 produtos do catálogo também quebram (ver a nota
   grande do LegalX). Em 7,65rem (122,4px de canvas) o selo passa a caber
   também em 1440px (122,4 < 125), fechando esse último ponto de quebra
   sem precisar de nenhuma outra mudança.
   [HISTÓRICO: régua até a rodada abaixo.] */
/* ARQUIVO APARADO E SELO MAIOR (06/08/2026, relato do dono: "o selo do
   TerraVision está mais baixo que os irmãos, ou pequeno demais").

   MEDIDO ANTES DE MEXER, para saber qual dos dois era. Alinhamento: o selo
   estava centralizado no rodapé como todos os outros (`align-items:
   center`), e a diferença de linha de base entre ele e os irmãos vinha da
   ALTURA DO RODAPÉ de cada cartão, corrigida nesta mesma rodada pela faixa
   reservada do bloco 10.4. Tamanho: aí sim havia defeito — glifo de
   **12,6px** de altura renderizada (caixa de 122,4×29,9 com conteúdo real
   ocupando 42,2% da altura do canvas), contra 27 a 30px do resto da
   família e 20,7px do INCO-LATAM, o vizinho mais próximo em formato.
   Portanto: era tamanho, não alinhamento, e o relatório diz isso.

   DUAS CAUSAS, as duas tratadas. (1) O arquivo carregava 52px de canvas
   vazio acima e abaixo do lockup (736×180 de canvas para 632×76 de
   conteúdo): como este selo é dimensionado por LARGURA, esse respiro morto
   virava altura desperdiçada, e o glifo encolhia junto. Os dois arquivos
   foram aparados e recompostos com o respiro padrão da família, 14px:
   660×104 de canvas para os mesmos 632×76 de conteúdo. (2) A régua subiu de
   7,65rem para 9,4rem (150,4px), que é o teto que ainda cabe ao lado do CTA
   na coluna mais estreita da grade (272px úteis no cartão span 1 de 1440px,
   menos 102px de "Saber mais" e o gap do rodapé).

   RESULTADO: glifo de 17,3px (+37%) e caixa de 150,4×23,7 — a caixa fica
   MENOR que a de antes em altura (23,7 contra 29,9) justamente porque o
   vazio saiu, e é isso que faz o selo ler como assentado na linha em vez de
   flutuando dentro de uma caixa alta demais.

   CONCESSÃO REGISTRADA: 17,3px continua abaixo do alvo de 27 a 30px da
   família. Não há tamanho que resolva isso sem quebrar o rodapé: o lockup
   tem proporção de conteúdo de 8,3:1 (ícone de satélite mais 11
   caracteres), então 27px de glifo pediriam ~224px de largura, quase o
   rodapé inteiro. É a mesma concessão já aceita e documentada para o
   LegalX (15,8px), pelo mesmo motivo estrutural. */
.portfolio__logo-marca--terravision {
  inline-size: 9.4rem;
  block-size: auto;
}

/* RECALIBRADO (06/08/2026, dono ao vivo: "maior que os demais — reduzir").
   3,01rem -> 2,6rem (~14% menor). Losango + "LAYERS" + "by HEMAV",
   proporção mais compacta que ATLANTIS/TERRAVISION (2,64:1 de canvas),
   cabe no alvo de altura sem precisar de teto de largura: 2,6rem de
   canvas × 62,3% de conteúdo real ≈ 25,9px de glifo, 94px de conteúdo
   largura. */
.portfolio__logo-marca--layers {
  block-size: 2.6rem;
  inline-size: auto;
}

/* Ícone globo (CECC/UnB) + "INCO-LATAM" (10 caracteres) — lockup comprido
   o bastante pra também pedir teto de largura: 8,75rem de canvas (140px)
   ≈ 125px de conteúdo real, altura de glifo resultante ~24px (abaixo dos
   30px-padrão, mesma concessão já registrada pra MAIA em rodada
   anterior — lockup comprido troca altura por largura controlada).
   [HISTÓRICO: essa era a régua até a rodada abaixo.] */
/* REDUZIDO (06/08/2026, dono ao vivo: "reduzir um pouco", mesmo pedido e
   mesma rodada do TerraVision, nota grande logo acima — mesma causa
   também: cabia lado a lado em quatro dos cinco viewports do §7.1 e só
   quebrava em 1440px, junto de outros 7 produtos do catálogo). 8,75rem ->
   7,65rem (~12,6% menor), fechando também o ponto de quebra em 1440px
   (122,4px de canvas contra 125px disponíveis). */
.portfolio__logo-marca--inco-latam {
  inline-size: 7.65rem;
  block-size: auto;
}

/* ARQUIVO REFEITO (06/08/2026, dono ao vivo: "feio, mal alinhado e com
   baixa qualidade"). Fonte trocada — a captura webp original (fundo
   escuro, halo de compressão/subpixel nas bordas) foi substituída por um
   frame ChatGPT-Image do mesmo mockup em tinta única sobre fundo claro
   (sem halo de cor), recortado só o "JX" (sem a tagline, ilegível em
   miniatura), com upscale 6x Lanczos + unsharp ANTES do recorte alfa (não
   depois — nitidez sobre o degradê real, não sobre uma máscara binária já
   serrilhada) pra compensar a baixa resolução nativa do mockup (55×42px
   brutos). Arquivo novo: canvas 363×215, conteúdo 302×153 (71,2% de
   altura, proporção 1,69:1 — bem mais perto do território de wordmark
   comum que o arquivo antigo). "JX" continua tratado como ÍCONE (2
   letras, mais compacto que wordmark): 2,45rem de canvas × 71,2% ≈ 27,9px
   de glifo. [HISTÓRICO: régua até a rodada abaixo.] */
/* REDUZIDO (06/08/2026, dono ao vivo: "JX-AI está grande demais, reduza um
   pouco"). 2,45rem -> 2,05rem (-16%): o glifo cai de 27,9px para ~23,4px
   (32,8px de canvas × 71,2% de conteúdo real). Fica abaixo do alvo de
   27-30px da família de propósito, e o motivo é o mesmo que já justificou
   a concessão inversa no CIRCUIT LAUNCH: "JX" são DUAS LETRAS em bloco
   cheio, quase quadradas (1,69:1) — à mesma altura de glifo de um wordmark
   de dez caracteres, elas pesam bem mais por área de tinta. Peso visual
   equivalente, não altura equivalente, é o critério que este bloco usa
   desde a primeira recalibragem por slug. */
.portfolio__logo-marca--jx-ai {
  block-size: 2.05rem;
  inline-size: auto;
}

/* Logo CEB (ícone + wordmark + tagline pequena) — também mais denso que
   um wordmark solto, mira ~27px: 2,25rem de canvas × 75,5% de conteúdo
   real ≈ 27,2px de glifo. */
.portfolio__logo-marca--ceb-ipes {
  block-size: 2.25rem;
  inline-size: auto;
}

/* Ícone (app-icon arredondado, tinta fixa preta nos dois temas — ver nota
   de content.js) + "anteciPay" — proporção compacta (2,71:1 de canvas),
   sem precisar de teto de largura: 2,8rem de canvas × 62,3% de conteúdo
   real ≈ 28px de glifo, 105px de conteúdo largura.
   [HISTÓRICO: essa era a régua até a rodada abaixo.] */
/* AUMENTADO (06/08/2026, dono ao vivo: "está menor que os irmãos —
   aumentar só um pouco"). 2,8rem -> 2,95rem (~5,4% maior): glifo sobe de
   28px para ~29,5px, dentro da faixa ~27-30px que o resto da família usa
   (firedash/projeto-mel ~30px).

   TROCA REGISTRADA, NÃO ESCONDIDA: o CTA "Conhecer o produto" já deixava
   pouca folga ao lado do selo (97-134px disponíveis contra 122px do selo
   antigo, medido nos cinco viewports do §7.1, sem pele) — 768px já
   quebrava linha ANTES desta mudança (97 < 122), e o aumento agora também
   empurra 390px pra quebra (122 disponíveis contra 128px do selo novo,
   por ~6px). Optou-se por manter o aumento mesmo assim: é pedido literal
   e específico do dono, o incremento é pequeno ("só um pouco"), e
   AnteciPay passa a quebrar linha no rodapé em 2 dos 5 viewports da
   matriz (390 e 768) — companhia de Projeto MEL, que já quebra nesses
   dois mesmos pontos por causa própria (nota de LegalX, acima, cataloga
   quem mais quebra em 1440px). Não é regressão silenciosa: o novo ponto
   de quebra (390px) está medido e registrado aqui para quem revisar
   depois. */
.portfolio__logo-marca--antecipay {
  block-size: 2.95rem;
  inline-size: auto;
}

.portfolio__card:hover .portfolio__logo-marca,
.portfolio__card:focus-within .portfolio__logo-marca {
  filter: grayscale(0);
  opacity: 1;
}

/* 10.5 A SETA DO LINK DE PRODUTO SAI JUNTO COM O CARTÃO, não só quando o
   ponteiro encosta no próprio link: o cartão inteiro é a superfície de
   intenção, e o link é a consequência dela. */
.portfolio__link svg {
  transition: transform calc(var(--dur-fast) * var(--motion)) var(--ease-out);
}

.portfolio__card:hover .portfolio__link svg,
.portfolio__card:focus-within .portfolio__link svg {
  transform: translate3d(0.15em, -0.15em, 0);
}

/* 12a rodada: camada da plataforma efetiva (hover sem video). */
.portfolio__shot-tela {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  opacity: 0;
  transition: opacity calc(var(--dur-base) * var(--motion)) var(--ease-out);
  pointer-events: none;
  z-index: 2;
}
.portfolio__shot-tela[data-visivel] { opacity: 1; }

/* 12a rodada, "titulo torto": ZONAS INTERNAS PADRONIZADAS. Cada faixa do
   cartao reserva a mesma altura em todos (nome ate 2 linhas, meta 1, tagline
   ate 2), entao badge, nome, tagline, descricao, claims e CTA alinham entre
   vizinhos de linha, mesmo quando o texto de um quebra e o do outro nao.

   ============================================================================
   FAIXAS COM MÍNIMO E MÁXIMO, AGORA NAS SEIS ZONAS (06/08/2026, pedido do
   dono, com captura: "tem que ter o espaço definido para não ficar título
   mais para cima e outros mais para baixo; e defina um máximo e mínimo de
   cada um deles"; o vão enorme entre descrição e lista no TerraVision era o
   sintoma mais visível).

   O QUE ESTAVA ERRADO, medido antes (1440×900, escuro, pele padrão, os 14
   cartões, `getBoundingClientRect` de cada zona relativo ao topo do cartão):

     zona        altura observada          efeito
     meta        18,1px (1 linha) ou       tudo abaixo dela nascia 18px
                 36,3px (2 linhas)         mais alto ou mais baixo
     tagline     53,9 / 71,4 / 95,3px      três alturas diferentes
     descrição   43,4 ou 65,1px            duas
     claims      69,8 / 87,8 / 105,8 /     quatro, de 3 a 6 linhas
                 123,8px
     atuação     0 / 18,1 / 36,3px         ZERO no LAYERS e no TerraVision
                                           (campo null, elemento nem existia)
     rodapé      49,8 a 107,9px            selo caindo para a 2ª linha

   Só o título já era uniforme (184px de topo nos 13 cartões span 1). Tudo o
   mais dependia do TEXTO de cada produto, e é isso que a grade em
   `align-items: stretch` transforma em desalinhamento visível: as zonas
   escorregam umas sobre as outras e a folga que sobra vai parar em lugares
   diferentes de cada cartão.

   A REGRA, e ela é uma só para as seis zonas: MÍNIMO = MÁXIMO = o teto de
   linhas que aquela zona já tinha por `line-clamp`. Com o piso igual ao
   teto, a zona ocupa a MESMA altura com uma linha ou com o máximo, e o
   cartão inteiro vira uma pilha de faixas fixas — título, lista e rodapé
   caem no mesmo y em qualquer cartão da grade, por construção e não por
   coincidência de comprimento de texto.

   NADA DE ALTURA MÁGICA EM PIXEL (pedido literal do dono). Cada faixa é
   `<n> linhas` da PRÓPRIA zona, e a linha vem da escala tipográfica: a
   unidade `lh` é, por definição, o `line-height` computado do elemento, o
   que faz a faixa acompanhar sozinha qualquer mudança de `--scale`, de
   `--density`, de fonte ou de token de entrelinha. A declaração em `em`
   logo acima é a mesma conta escrita à mão (o multiplicador é a razão de
   entrelinha daquela zona) e existe como piso para motor sem `lh`: se `lh`
   não for entendido, a declaração é descartada e vale a de `em`, que já
   entrega a mesma altura na escala de fábrica. Progressive enhancement
   idêntico ao par `-webkit-line-clamp`/`line-clamp` usado no resto desta
   folha.

   POR QUE `min-block-size` E NÃO `block-size`: com altura fixa, texto que
   estourasse a faixa vazaria por fora da caixa (o §7.1 chama isso de texto
   cortado e reprova). Com piso mais teto de linhas, quem estoura é cortado
   pelo `line-clamp` DENTRO da caixa, que é o comportamento que esta folha
   já documenta desde a rodada 3. */

/* Nome do produto: UMA linha reservada, teto de duas.

   A FAIXA É O MAIOR CASO REAL, MEDIDO, e aqui ele é 1 (06/08/2026, dois
   pedidos do dono na mesma leva: "o vão entre o nome do produto e o par de
   crachás está grande demais" e "se tiver 3 linhas, todos têm que ter o
   espaço de 3 já setado; se tiver 2, espaço de 2"). A régua antiga
   reservava 2 linhas por precaução, não por medição. Medido em 390, 768,
   1280, 1440 e 1920px, com cada `<h3>` clonado sem clamp e sem piso para
   ler a altura NATURAL: **os 14 nomes cabem em 1 linha em todos os cinco
   viewports**, inclusive os mais longos ("Atlantis Technologies", "Agrícola
   Wehrmann", "MAIA Investimentos"). Ou seja, a segunda linha reservada era
   ar em todos os cartões, e era ela — não o `gap` — o vão que o dono viu
   entre o nome e os crachás: 32,2px de linha vazia contra 11,88px de gap.

   O TETO DE 2 (bloco 4, `-webkit-line-clamp`) FICA, e é a rede: se um dia
   entrar um nome que quebre, ele quebra dentro da caixa em vez de vazar, e
   o cartão daquele produto cresce uma linha. A troca é consciente: um
   catálogo inteiro com 32px de ar em cada cartão é um defeito que o dono
   enxergou; um cartão futuro desalinhado por um nome mais longo é um caso
   que ainda não existe e que a medição acima detecta na hora. */
.portfolio__card .card__title {
  min-block-size: 1.15em;
  min-block-size: 1lh;
}

/* Linha de metadados ("Governo · Plataforma pública"): 2 LINHAS SEMPRE,
   pedido explícito do dono ("deve caber em ATÉ 2 LINHAS e ocupar sempre
   esse espaço mesmo quando tiver uma só"). Era `1.6em`, que reservava uma
   linha e deixava a segunda empurrar o cartão. A entrelinha aqui é a de
   corpo (1,65 na escala de fábrica) porque a zona é um `<p>` comum, só com
   fonte mono e caixa alta. O teto de 2 linhas é o `line-clamp` abaixo: a
   taxonomia nova tem rótulos longos ("Administrativo e Gestão · Sistemas
   corporativos") e sem teto uma terceira linha voltaria a desalinhar. */
.portfolio__card .portfolio__meta {
  min-block-size: calc(2 * 1.65em);
  min-block-size: 2lh;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* Tagline: 3 linhas, o mesmo teto que o bloco 4 declara.

   DEFEITO DE CASCATA CORRIGIDO AQUI, e ele estava vivo há três rodadas: a
   regra antiga escrevia `display: block` neste seletor de 2 classes
   (0,2,0), que vence o `display: -webkit-box` do bloco 4 (0,1,0) — e sem
   `-webkit-box` o `-webkit-line-clamp` não faz nada. Ou seja, o teto de 3
   linhas da tagline nunca valeu: medido, FireDash e MAIA renderizavam 4
   linhas (95,3px) em 1440px. Repor o `display` correto no seletor que
   vence é o que faz o teto voltar a existir, e é também o que fecha o
   máximo desta faixa. */
.portfolio__card .portfolio__tagline {
  min-block-size: calc(3 * 1.15em);
  min-block-size: 3lh;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

/* `.card__desc > strong:first-child` continua com o tratamento antigo: é
   um destaque em linha dentro da descrição, não uma zona do cartão. */
.portfolio__card .card__desc > strong:first-child {
  min-block-size: calc(2 * 1.3em);
  display: block;
}

/* Descrição: 3 linhas, o teto que o bloco 4 já declarava. Entrelinha de
   corpo. */
.portfolio__card .portfolio__desc {
  min-block-size: calc(3 * 1.55em);
  min-block-size: 3lh;
}

/* Lista de características: a faixa é POR MARCADOR, não pela lista inteira,
   e isso é de propósito. Reservar altura no `<ul>` daria a altura certa mas
   deixaria os marcadores flutuando dentro dela (o `gap` do grid distribui
   diferente conforme quantos itens de fato ocupam duas linhas); reservando
   2 linhas em CADA marcador — o mesmo teto que `.portfolio__claim > span`
   já impunha no bloco 4 — a lista inteira fica com 3 faixas iguais mais 2
   `gap`s, e o rodapé abaixo dela cai no mesmo y nos 14 cartões. A
   entrelinha do marcador é a declarada no bloco 4 (1,5). */
.portfolio__card .portfolio__claim {
  min-block-size: calc(2 * 1.5em);
  min-block-size: 2lh;
}

/* Linha de papel ("Produto · Arquitetura · Engenharia"): 2 linhas, com o
   MESMO espaço reservado quando o campo é null. É o item 5 do pedido do
   dono: LAYERS e TerraVision não têm `atuacao` documentada (ver a nota em
   partials/portfolio.js e a pendência em scratchpad/pendencias-content-js.md)
   e, até esta rodada, o partial nem escrevia o elemento — o cartão deles
   ficava 18 a 36px mais curto que o vizinho e desalinhava a linha inteira
   da grade. Agora o `<p>` existe sempre, vazio quando não há dado, e esta
   faixa garante que ele ocupe o mesmo espaço com ou sem texto. Um `<p>`
   vazio não é anunciado por leitor de tela e não afirma nada: ele reserva
   geometria, não inventa papel. */
.portfolio__card .portfolio__atuacao {
  min-block-size: calc(2 * 1.65em);
  min-block-size: 2lh;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* 12b. FAIXAS PRÓPRIAS DO CARTÃO DE FECHAMENTO LARGO (docking), 06/08/2026.

   Pedido do dono, com captura do cartão do Aquanta (o único fechador hoje,
   `.portfolio__card--resumo-docking` em FECHAMENTO_SLUGS, partials/
   portfolio.js): "se for o caso diminui um pouco a altura, para não
   precisar de tanto espaço entre as coisas à toa".

   A CAUSA: até esta rodada o fechador herdava as MESMAS seis faixas do
   bloco 12a acima, calibradas para o cartão ESTREITO (1 trilha, ~320px).
   Mas o fechador span 2 ou 3 (container query, bloco 5 desta folha) é
   2,5 a 3,5x mais largo — 792/1009/1122px medidos em 1280/1440/1920px de
   viewport —, e o MESMO texto que quebra em 2 ou 3 linhas na coluna
   estreita cabe em bem menos linhas na coluna larga. A faixa continuava
   reservando o teto pensado pra coluna estreita, e a diferença virava vão
   morto entre os blocos.

   MEDIDO, sem piso e sem `line-clamp` (altura natural do texto real do
   Aquanta — único produto que ocupa este slot hoje — dividida pelo
   `line-height` computado de cada zona), nos três viewports de prova:

     zona        1280px (792px)   1440px (1009px)   1920px (1122px)
     meta        1 linha          1 linha            1 linha
     tagline     1 linha          1 linha            1 linha
     descrição   2 linhas         2 linhas           2 linhas
     claim       1 linha (cada)   1 linha (cada)     1 linha (cada)
     atuação     1 linha          1 linha            1 linha

   (`card__title` fica de fora: já reservava 1 linha no bloco 12a, e o
   nome "Aquanta" também só usa 1 — nenhum vão ali.)

   As faixas abaixo reservam exatamente isso — o MAIOR CASO REAL do
   cartão largo, não um palpite, mesma disciplina do bloco 12a (piso =
   teto DENTRO desta faixa recalibrada). O `-webkit-line-clamp` de cada
   zona (bloco 12a, herdado sem mudança nenhuma aqui) continua sendo o
   TETO de segurança: se um produto futuro assumir este slot com texto
   mais longo, ele quebra dentro da própria caixa (o cartão cresce além
   deste piso menor) em vez de vazar ou cortar informação — mesmo
   precedente já registrado para `card__title` no bloco 12a.

   NÃO MEXE NO CARTÃO ESTREITO: os seletores abaixo (`.portfolio__card--
   resumo-docking .portfolio__meta` etc.) são específicos ao fechador — os
   14 cartões regulares continuam com a padronização do bloco 12a, medida
   e aprovada em 1280/1440/1920px, dispersão zero. E não precisa
   realinhar o fechador com o vizinho de linha: `align-self: start`
   (bloco 5 acima) já desacopla a altura dele da linha do cartão regular
   ao lado, então encolher as faixas aqui não desalinha ninguém — o
   rodapé do PRÓPRIO fechador é quem passa a colar no conteúdo acima,
   sem o vão que sobrava até o fim da faixa antiga. */
.portfolio__card--resumo-docking .portfolio__meta {
  min-block-size: calc(1 * 1.65em);
  min-block-size: 1lh;
}

.portfolio__card--resumo-docking .portfolio__tagline {
  min-block-size: calc(1 * 1.15em);
  min-block-size: 1lh;
}

.portfolio__card--resumo-docking .portfolio__desc {
  min-block-size: calc(2 * 1.55em);
  min-block-size: 2lh;
}

.portfolio__card--resumo-docking .portfolio__claim {
  min-block-size: calc(1 * 1.5em);
  min-block-size: 1lh;
}

.portfolio__card--resumo-docking .portfolio__atuacao {
  min-block-size: calc(1 * 1.65em);
  min-block-size: 1lh;
}

/* --------------------------------------------------------------------------
   13. Modal de produto (rodada "artes novas + modal", 05/08/2026)

   Estrutura em partials/portfolio.js (modalShell() no NX.register, conteúdo
   populado em JS por popularDoCartao()/montarCarrossel() no inicializador
   do fim do arquivo). Mesmo PADRÃO do diálogo de contato
   (sections/contato.css §6): `position: fixed` promovido a filho de <body>,
   painel opaco (nunca translúcido — mesma nota de "vidro sobre fundo liso"
   do §12.1), transição só em opacity/transform, tudo multiplicado por
   `var(--motion)` (reduced-motion sem regra própria, a mesma variável já
   zera a duração). Território deste agente: nenhum hex, nenhum !important,
   nenhum vh — só tokens.
   -------------------------------------------------------------------------- */

/* 13.1 O CARTÃO INTEIRO VIRA GATILHO. O rodapé deixou de ser o único alvo
   clicável (ver a nota grande de `acao`, produtoCard()): o clique em
   qualquer parte do cartão abre o modal (inicializador de portfolio.js), e
   o cursor precisa dizer isso antes do clique. `.portfolio__link` (o botão
   do rodapé) já herda `cursor: pointer` do reset de botão (02-base.css); o
   resto do cartão, que não é controle nenhum, precisa da declaração
   explícita. */
.portfolio__card {
  cursor: pointer;
}

/* O botão do rodapé é o único descendente com a PRÓPRIA intenção de clique
   (abre o modal, mas também é o alvo de foco por teclado — Enter/Espaço).
   `cursor: pointer` nele já vem do reset; nada extra necessário aqui. */

.portfolio-modal[hidden] {
  display: none;
}

.portfolio-modal {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--space-6) var(--gutter);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Trava de rolagem do documento enquanto o modal está aberto — mesmo nome
   de classe do diálogo de contato trocado só pelo dono (nx-portfolio-…),
   porque os dois podem existir na mesma página e precisam de travas
   independentes (o modal de produto nunca abre por cima do diálogo de
   contato nem vice-versa, mas cada um solta a própria trava ao fechar). */
.nx-portfolio-modal-aberto {
  overflow: hidden;
}

.portfolio-modal__veu {
  position: fixed;
  inset: 0;
  background-color: hsl(var(--scrim) / 0.72);
  opacity: 0;
  transition: opacity calc(var(--dur-base) * var(--motion)) var(--ease-out);
}

.portfolio-modal.is-open .portfolio-modal__veu {
  opacity: 1;
}

.portfolio-modal__painel {
  position: relative;
  inline-size: 100%;
  max-inline-size: 44rem;
  margin-block: auto;
  border-radius: var(--radius-lg);
  border: var(--hairline) solid hsl(var(--border));
  background-color: hsl(var(--surface-2));
  color: hsl(var(--foreground));
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  opacity: 0;
  transform: translate3d(0, 12px, 0) scale(0.985);
  transition:
    opacity calc(var(--dur-base) * var(--motion)) var(--ease-out),
    transform calc(var(--dur-base) * var(--motion)) var(--ease-out);
}

.portfolio-modal.is-open .portfolio-modal__painel {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
}

.portfolio-modal__fechar {
  position: absolute;
  inset-block-start: var(--space-3);
  inset-inline-end: var(--space-3);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap-min);
  min-block-size: var(--tap-min);
  border-radius: var(--radius-sm);
  border: var(--hairline) solid transparent;
  /* Sobre a própria imagem do carrossel (o botão pousa em cima da mídia, não
     do painel) — precisa de fundo próprio para não se perder contra
     qualquer imagem clara. */
  background-color: hsl(var(--scrim) / 0.55);
  color: hsl(0 0% 100%);
  cursor: pointer;
  font-size: var(--fs-lead);
  transition:
    background-color calc(var(--dur-fast) * var(--motion)) var(--ease-out),
    transform calc(var(--dur-fast) * var(--motion)) var(--ease-out);
}

.portfolio-modal__fechar:hover {
  background-color: hsl(var(--scrim) / 0.75);
  transform: rotate(90deg);
}

.portfolio-modal__fechar:focus-visible {
  outline: 2px solid hsl(var(--ring));
  outline-offset: 2px;
}

/* 13.2 Carrossel. `--media-ratio` local (2/1, a mesma proporção de
   .media-frame no resto do arquivo) mantém o quadro estável entre imagens —
   nenhuma camada empurra layout ao trocar de slide. */
.portfolio-modal__carrossel {
  position: relative;
  aspect-ratio: 2 / 1;
  overflow: hidden;
  background-color: hsl(var(--muted));
}

/* JANELA BAIXA (17ª rodada, 06/08/2026). Medido em 1440x650: o painel tinha
   903px e 276px ficavam fora da tela, com o carrossel de 351px sozinho comendo
   mais da metade do que sobrava. O painel agora cabe na janela pelo primitivo
   .dialog-fit (03-ui.css) e o carrossel cede altura, porque entre ver a arte
   inteira e ver o texto do produto, quem manda é o texto. */
@media (max-height: 46rem) {
  .portfolio-modal__carrossel {
    aspect-ratio: auto;
    block-size: min(38dvh, 20rem);
  }
}

/* ARRASTO (06/08/2026, pedido do dono): `cursor: grab` avisa que o
   carrossel se arrasta antes do primeiro toque; `.is-dragging`
   (partials/portfolio.js, onArrastoInicio()/onArrastoFim()) troca para
   `grabbing` durante o gesto e desliga a transição (o JS já zera
   `transition-property` inline nesse momento — aqui só a troca de cursor
   e o `user-select: none`, que impede o gesto de virar seleção de texto
   da página por baixo do carrossel, comum quando o pointerdown começa
   sobre uma imagem). `touch-action: pan-y` deixa o toque vertical (rolar a
   página) atravessar livre; só o eixo horizontal é capturado pelo JS —
   sem isso, um swipe vertical dentro do carrossel, em toque, brigaria com
   o scroll da página inteira. */
.portfolio-modal__track {
  display: flex;
  block-size: 100%;
  cursor: grab;
  touch-action: pan-y;
  transition: transform calc(var(--dur-base) * var(--motion)) var(--ease-out);
}

.portfolio-modal__track.is-dragging {
  cursor: grabbing;
  user-select: none;
}

.portfolio-modal__slide {
  flex: none;
  inline-size: 100%;
  block-size: 100%;
}

.portfolio-modal__slide img,
.portfolio-modal__slide video {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* SLIDE DE VÍDEO (06/08/2026, pedido do dono: uma das peças do carrossel é
   o vídeo do site em movimento). `object-position: top` pelo MESMO motivo
   já documentado no vídeo do cartão (bloco 10.2): o arquivo é 16/10, a
   moldura do carrossel é 2/1, e `cover` centralizado comeria a mesma faixa
   de topo onde vive o cabeçalho do site gravado. Fundo próprio para o
   intervalo entre a abertura do modal e o primeiro quadro decodificado
   (com `preload="none"` e as fontes criadas só na ativação do slide, esse
   intervalo existe de propósito) — o pôster cobre esse tempo, e o `--muted`
   por baixo cobre a hipótese de o pôster falhar. */
.portfolio-modal__video {
  object-position: top;
  background-color: hsl(var(--muted));
}

.portfolio-modal__seta {
  position: absolute;
  inset-block: 0;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: clamp(2.75rem, 4vw + 1rem, 3.5rem);
  border: 0;
  background: linear-gradient(90deg, hsl(var(--scrim) / 0.35), hsl(var(--scrim) / 0));
  color: hsl(0 0% 100%);
  cursor: pointer;
  transition: background-color calc(var(--dur-fast) * var(--motion)) var(--ease-out);
}

.portfolio-modal__seta--prev {
  inset-inline-start: 0;
}

.portfolio-modal__seta--prev svg {
  transform: rotate(180deg);
}

.portfolio-modal__seta--next {
  inset-inline-end: 0;
  background-image: linear-gradient(270deg, hsl(var(--scrim) / 0.35), hsl(var(--scrim) / 0));
}

.portfolio-modal__seta:hover {
  background-color: hsl(var(--scrim) / 0.25);
}

.portfolio-modal__seta:focus-visible {
  outline: 2px solid hsl(var(--ring));
  outline-offset: -2px;
}

.portfolio-modal__seta[hidden] {
  display: none;
}

.portfolio-modal__dots {
  position: absolute;
  inset-block-end: var(--space-3);
  inset-inline: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

.portfolio-modal__dots[hidden] {
  display: none;
}

/* ALVO DE TOQUE SEM INFLAR O PONTO (achado do dono, captura do modal em
   390px: os dots viravam círculos de ~44px cobrindo a imagem). A causa não
   era esta regra: é `<button>` de verdade (montarCarrossel(), acima), e
   02-base.css §4 força TODO `button` a `min-inline-size`/`min-block-size:
   var(--tap-min)` sob `(pointer: coarse)` — regra correta para botão comum,
   mas `min-*` sempre vence `inline-size`/`block-size` quando é maior, então
   ela inflava o dot de 0.5rem para 44px em qualquer tela de toque, raio 50%
   incluído (daí os círculos gigantes). A classe aqui é mais específica que
   o seletor `button` de 02-base.css (0,1,0 contra 0,0,1) e por isso já
   venceria em qualquer ordem de carga, mas o reset abaixo fica EXPLÍCITO em
   vez de depender de especificidade para não repetir o susto se alguém
   um dia mover a regra de 02-base.css para depois desta folha.

   O alvo de toque de 44px continua existindo, só não desenha mais: mesmo
   padrão de .link::before em 03-ui.css (`grep` confirma o precedente) — um
   pseudo-elemento invisível, centralizado, fora do fluxo, position:
   relative no dot pai para ancorar. */
/* REDUZIDO E SEM BORDA (06/08/2026, pedido do dono, captura do modal:
   "pontinhos ENORMES embaixo da imagem" — a régua de 0.5rem + filete branco
   da rodada anterior (nota grande acima) já resolvia a inflação por toque,
   mas o próprio dot, com anel de contorno, ainda lia como o elemento mais
   "pesado" da moldura, na família errada perto do traço fino que
   `.case-block__dash` (sections/cases.css) usa para o mesmo papel — "siga a
   mesma família visual", pedido literal. Três ajustes, mesmo mecanismo de
   alvo de toque de sempre (::before invisível, abaixo, intocado):

     1. 0.5rem -> 0.375rem (8px -> 6px): mais discreto, ainda alvo real de
        clique com folga de sobra (o alvo de TOQUE nunca foi o círculo
        visível, é o ::before de --tap-min).
     2. `border` sai: o filete de 0,7 de alfa somava contorno + miolo, dois
        elementos de tinta por ponto. Só `background-color` (como o traço
        fino dos cases, que também não tem borda) enxuga a silhueta sem
        perder contraste sobre a foto — o ativo continua branco 100% opaco.
     3. `transform: scale(1.2)` no ativo SAI: um "pop" de tamanho é o oposto
        de "nunca engordando a bolinha visual" (mesma frase do pedido). O
        estado ativo passa a se diferenciar só por cor (branco cheio contra
        translúcido), igual ao case-block__dash-fill ao lado do traço vazio
        — nenhum dos dois pontos muda de RAIO, só de opacidade/cor. */
/* `appearance: none` (06/08/2026, terceira volta do dono no mesmo ponto:
   "no celular as bolinhas do carrossel aparecem enormes; já foi mexido uma
   vez e não resolveu"). Medido em ponteiro grosso REAL (390×844 com
   `Emulation.setDeviceMetricsOverride` + `setTouchEmulationEnabled`, e
   também em WebKit com o perfil de iPhone), o ponto do MODAL já media
   6,0×6,0px antes desta rodada, nos dois motores — a régua da rodada
   anterior tinha resolvido o caso dele. A causa real do relato estava na
   OUTRA barra de pontos da página, a da Evidência, que media 44×44px
   arredondada a 9999px: quatro bolas cheias sob a imagem do case, em
   qualquer telefone (a correção mora em sections/cases.css, bloco 3, e o
   diagnóstico completo está lá).

   Este reset entra mesmo assim, e não é zelo: `.case-block__dash` e
   `.case-block__seta` já precisaram exatamente dele para o Safari (nota de
   06/08/2026 naquela folha, com print do dono) — sem `appearance: none` o
   motor mantém o cromo nativo de `<button>`, que tem métrica mínima
   própria. Os dois pontos são o mesmo componente em dois lugares; deixar
   um blindado e o outro não é esperar o mesmo defeito de novo. */
.portfolio-modal__dot {
  position: relative;
  appearance: none;
  -webkit-appearance: none;
  inline-size: 0.375rem;
  block-size: 0.375rem;
  min-inline-size: 0.375rem;
  min-block-size: 0.375rem;
  border-radius: 50%;
  background-color: hsl(0 0% 100% / 0.35);
  cursor: pointer;
  padding: 0;
  transition: background-color calc(var(--dur-fast) * var(--motion)) var(--ease-out);
}

/* `--tap-min` mais 2px, e os 2px têm motivo escrito: o harness de auditoria
   (`scripts/shots.mjs`, areaDeToqueOk()) sonda os quatro pontos cardeais a
   EXATAMENTE 22px do centro, e com a caixa em 44px cravados esses pontos
   caem na fronteira — `elementFromPoint` resolve para o que estiver atrás
   ou para o vizinho, por arredondamento, e o alvo reprova mesmo existindo.
   Medido antes da folga: o ponto de baixo resolvia para
   `.portfolio-modal__body`. */
.portfolio-modal__dot::before {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  inline-size: calc(var(--tap-min) + 0.125rem);
  block-size: calc(var(--tap-min) + 0.125rem);
  transform: translate(-50%, -50%);
}

/* ALVOS DE TOQUE QUE NÃO SE ATROPELAM (06/08/2026, achado desta rodada).
   O pseudo-elemento acima dá 44px de área a cada ponto, como o §7.5 pede,
   mas com `gap: var(--space-2)` os centros ficam a 14px um do outro: as
   áreas de 44px se sobrepõem em 30px, e quem ganha o toque é o último no
   documento, não o ponto que o dedo mirou. Ou seja, o alvo existia mas
   pertencia ao vizinho.

   Sob ponteiro grosso, o `gap` passa a ser o que falta para o passo entre
   pontos fechar exatamente `--tap-min` — as áreas ladrilham sem sobrar nem
   sobrepor. O PONTO VISÍVEL não muda de tamanho (continua 0,375rem, que é
   o pedido de sempre: "ponto discreto, área de toque por pseudo-elemento");
   o que muda é a distância entre eles, e só no dedo. Em ponteiro fino nada
   disso se aplica. */
@media (pointer: coarse) {
  .portfolio-modal__dots {
    gap: calc(var(--tap-min) + 0.125rem - 0.375rem);
    /* A fileira sobe um degrau (de `--space-3` para `--space-6`) porque o
       carrossel é `overflow: hidden`: com os pontos a 12px da base, a
       metade de baixo da caixa de toque caía fora da moldura e era
       RECORTADA junto — medido, o ponto a 22px abaixo do centro resolvia
       para `.portfolio-modal__body`, não para o ponto. A 24px da base a
       caixa inteira cabe dentro da moldura. */
    inset-block-end: var(--space-6);
  }
}

.portfolio-modal__dot[aria-selected="true"] {
  background-color: hsl(0 0% 100%);
}

.portfolio-modal__dot:focus-visible {
  outline: 2px solid hsl(0 0% 100%);
  outline-offset: 2px;
}

/* 13.3 Corpo: título, tagline, descrição, claims, ação. Reaproveita
   .portfolio__badge/.portfolio__claims/.portfolio__claim como vieram do
   cartão (cloneNode em popularDoCartao()) — zero regra nova para essas três
   classes, a pintura já existe acima (bloco 11) e nas peles. */
.portfolio-modal__body {
  display: grid;
  gap: var(--space-5);
  padding: clamp(var(--space-6), 1.6vw + var(--space-4), var(--space-8));
}

.portfolio-modal__head {
  display: grid;
  gap: var(--space-3);
}

/* PRIMEIRA LINHA DO CABEÇALHO: CRACHÁS À ESQUERDA, MARCA D'ÁGUA À DIREITA
   (06/08/2026, pedido do dono: "a marca d'água também dentro do modal, num
   lugar que não brigue com o botão nem com o texto").

   Os dois candidatos descartados, e por quê: sobre a imagem do carrossel
   ela disputaria espaço com as setas e com os pontos (e mudaria de fundo a
   cada slide, que é o defeito que já reprovou a primeira tentativa de selo
   no cartão); na linha de ação ela disputaria com o botão, que é
   exatamente o que o dono excluiu. A linha de crachás é a única faixa do
   painel que já existe, tem sobra à direita nos 14 produtos e não carrega
   nem texto corrido nem controle.

   `align-items: center` alinha o selo pelo meio da fileira de crachás, e o
   `gap` impede que um lockup comprido encoste neles; abaixo de 30rem a
   fileira quebra e o selo cai para a linha de baixo, ainda à direita. */
.portfolio-modal__topo {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.portfolio-modal__badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.portfolio-modal__badges:empty {
  display: none;
}

/* MARCA D'ÁGUA DO MODAL. Reaproveita o par claro/escuro do cartão (os dois
   `<img>` clonados chegam com `data-tema-logo`, e é o `[data-theme]` do
   `<html>` que decide qual aparece — as regras estão no bloco 10.4, valem
   para o clone sem nenhuma linha nova).

   O QUE MUDA EM RELAÇÃO AO CARTÃO: aqui o selo NÃO é dessaturado nem
   translúcido em repouso. No cartão a discrição existe porque o selo divide
   a linha com o CTA e reage ao hover do cartão inteiro; no modal não há
   hover de superfície nem competição — o painel é o lugar onde a pessoa já
   escolheu olhar aquele produto, e uma marca a 55% de opacidade ali lê como
   falha de carregamento, não como sutileza. O teto de altura mantém o selo
   no mesmo porte dos crachás ao lado, seja qual for a régua por slug do
   bloco 10.4 (que é calibrada para o rodapé do cartão, mais largo). */
.portfolio-modal__selo {
  display: flex;
  align-items: center;
  flex: none;
}

.portfolio-modal__selo[hidden] {
  display: none;
}

.portfolio-modal__logo-marca {
  filter: none;
  opacity: 1;
  max-block-size: 2rem;
  max-inline-size: 8rem;
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
  object-position: right center;
}

.portfolio-modal__titulo {
  font-size: var(--fs-h3);
  font-weight: 680;
  letter-spacing: calc(var(--tracking-display) - 0.01em);
  text-wrap: balance;
}

.portfolio-modal__tagline {
  color: hsl(var(--muted-foreground));
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
}

.portfolio-modal__tagline[hidden] {
  display: none;
}

.portfolio-modal__desc {
  color: hsl(var(--foreground));
  font-size: var(--fs-base);
  line-height: var(--lh-body);
}

/* O case da Evidência não tem parágrafo de descrição (o que ele tem são os
   FATOS estruturados, abaixo), então esta zona some em vez de abrir um vão
   morto. Mesmo mecanismo já usado pela tagline. */
.portfolio-modal__desc[hidden] {
  display: none;
}

/* FATOS ESTRUTURADOS, CONTÊINER GENÉRICO (06/08/2026, modal reaproveitado
   pela Evidência). Recebe nós CLONADOS pelo consumidor — hoje a `<dl>` de
   Contexto/Problema/Restrições/Decisão e a régua de números do case, que
   chegam com as classes `.case-block__*` e, com elas, a pintura que
   sections/cases.css e as peles já dão. Por isso aqui não há tipografia
   nenhuma: só o ritmo vertical e o divisor, iguais aos do bloco de claims
   logo acima, para o painel ler como um documento só.

   `> :first-child` zera o `border-block-start` que `.case-block__facts` traz
   de origem: dentro do modal quem desenha o divisor é este contêiner, e dois
   filetes colados leriam como erro de renderização. */
.portfolio-modal__fatos {
  display: grid;
  gap: var(--space-5);
  padding-block-start: var(--space-5);
  border-block-start: var(--hairline) solid hsl(var(--border));
}

.portfolio-modal__fatos[hidden] {
  display: none;
}

.portfolio-modal__fatos > :first-child {
  border-block-start: 0;
  padding-block-start: 0;
}

/* TAGS (06/08/2026, classificação do dono). Fileira de `.chip`, o mesmo
   primitivo do mostruário de verticais — nenhuma tinta nova, nenhum token
   novo. `cursor: default` porque isto não é clicável, pelo mesmo princípio
   que rege o mostruário: um elemento não pode prometer clique que não
   existe. */
.portfolio-modal__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.portfolio-modal__tags[hidden] {
  display: none;
}

.portfolio-modal__tag {
  cursor: default;
  font-size: var(--fs-xs);
}

/* RESPIRO ACIMA DA LISTA (06/08/2026, pedido do dono, captura em anexo: "a
   lista dos 3 claims está colada na linha divisória acima"). O `gap` do
   grid pai (`.portfolio-modal__body`, --space-5) já separa o BLOCO de
   claims do bloco anterior (descrição) — o problema era DENTRO deste
   bloco: a borda divisória (`border-block-start`) e o primeiro item da
   lista ficavam a só --space-2 um do outro, bem menos que o ritmo entre os
   OUTROS blocos do modal. `padding-block-start` sobe para --space-5, o
   MESMO valor do gap externo — o resultado é um modal com ritmo vertical
   consistente ponta a ponta (cabeçalho, descrição, divisor+claims, ações
   todos com o mesmo respiro), não um valor pinçado só para "parecer
   melhor" aqui. */
/* ESPECIFICIDADE (06/08/2026, "o respiro ainda não pegou" — achado real,
   não falso positivo). A causa NÃO era pele nem ordem de arquivo: é
   `ul[role="list"] { padding: 0 }` (02-base.css §... — reset genérico do
   padrão "lista sem estilo de lista", usado no site inteiro). Este
   elemento É um `<ul role="list">` (partials/portfolio.js, modalShell()),
   e `ul[role="list"]` mede (0,1,1) — tag + atributo — contra `.portfolio-
   modal__claims` sozinho, (0,1,0), um só de classe. O reset ganha por
   PESO, não por ordem: mesmo com 02-base.css carregando antes desta
   folha, (0,1,1) > (0,1,0) vence riqualquer posição no arquivo. Medido:
   `padding-block-start` computava 0px, não `var(--space-5)`, e o traço
   entre a borda e o primeiro claim ficava a 1px — a "colada" relatada.
   `[role="list"]` aqui sobe a conta para (0,2,0), que vence o reset sem
   depender de ordem — mesmo padrão de `:where()`/seletor extra já usado
   noutras guerras de especificidade desta folha (ver a nota do crachá
   "ativo", bloco 4). */
.portfolio-modal__claims[role="list"] {
  display: grid;
  gap: var(--space-2);
  padding-block-start: var(--space-5);
  border-block-start: var(--hairline) solid hsl(var(--border));
}

.portfolio-modal__claims[hidden] {
  display: none;
}

/* O teto de duas linhas (§ bloco 11, `.portfolio__claim > span`) não faz
   sentido aqui: o modal tem largura de sobra e é exatamente o lugar onde o
   claim cortado no cartão pode terminar de aparecer por extenso. */
.portfolio-modal__claims .portfolio__claim > span {
  -webkit-line-clamp: unset;
  line-clamp: unset;
}

.portfolio-modal__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding-block-start: var(--space-2);
}

/* Case sem link externo (os três de hoje) não desenha ação nenhuma: sem
   isto, o `gap` do corpo do modal mais o `padding-block-start` daqui
   somariam um vão morto no pé do painel. */
.portfolio-modal__actions:empty {
  display: none;
}

.portfolio-modal__visitar {
  white-space: normal;
  text-align: center;
}

.portfolio-modal__privado {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: hsl(var(--muted-foreground));
  font-size: var(--fs-sm);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   14. Modal em tela pequena
   -------------------------------------------------------------------------- */

@media (max-width: 30rem) {
  .portfolio-modal {
    padding: var(--space-3);
  }

  .portfolio-modal__painel {
    max-inline-size: none;
  }

  .portfolio-modal__actions {
    align-items: stretch;
    flex-direction: column;
  }
}

@media (max-height: 40rem) {
  .portfolio-modal {
    align-items: flex-start;
  }

  .portfolio-modal__painel {
    margin-block: 0;
  }
}
