/* components.css — componentes pAtom GLOBAIS e reutilizaveis (graduados da bancada).
   Token-driven, BEM, estados via pseudo-classe. Consumido por base_auth (login/signup)
   e por quem mais precisar. Hover/active/placeholder = color-mix derivando de tokens
   (sem hex cru). O desenho dos icones vem da font Material Symbols (carregada no shell). */

/* Tipografia dos componentes abaixo: papeis (title-* / body / action / label / caption / subtext)
   vivem em atoms.css (ADR-0001), carregado ANTES de components.css em base.html. */

/* ═════════ .btn ═════════ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-8);   /* ícone+texto respiram (paridade legado core.css:763) */
  height: var(--btn-h);
  padding-inline: var(--sp-20);
  border-width: var(--bw-thick) !important;   /* 2px */
  border-style: solid !important;             /* literal; !important p/ vencer reset button{border:none} + qualquer copia duplicada (diagnostico @import) */
  border-color: var(--c-navy) !important;     /* navy #052450, visivel; herdado por TODAS as variantes (ADR-0002) */
  border-radius: var(--r-control);
  background: var(--c-white);
  color: var(--c-info);
  font-family: var(--font-sans);
  line-height: var(--lh-tight);
  cursor: pointer;   /* tipografia: fs+fw via role action; color/family/lh proprios */
  transition: background var(--dur-fast) var(--ease-base), color var(--dur-fast) var(--ease-base), border-color var(--dur-fast) var(--ease-base), opacity var(--dur-fast) var(--ease-base);
}
/* estados PADRONIZADOS (todos os botões; via token, 0 hex) */
.btn:hover  { background: var(--c-info); color: var(--c-white); }                               /* inverte, mantém família azul */
.btn:active { background: color-mix(in srgb, var(--c-info) 85%, black); color: var(--c-white); }
.btn:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: var(--ring-offset); }
.btn:disabled, .btn[disabled] { opacity: var(--op-disabled); cursor: not-allowed; }
/* btn-estilo-control: normal = visual do field__wrap em repouso (components.css:100-114),
   valores computados idênticos por token EXATO — bg var(--c-white), border 1px solid
   var(--c-border-strong) (medido: color(srgb .8488 .8664 .894) = mesmo valor), texto
   var(--c-ink) (medido: rgb(14,42,86) = mesmo valor). Radius já era --r-control (8px, igual
   ao control — sem mudança). Tamanho: min-height 44 (--btn-h) mantido, NÃO 52 do control
   (decreto explícito — só cor/borda/fundo igualam, geometria de botão permanece). */
.btn--primary, .btn--secondary {
  background: var(--c-white);
  color: var(--c-ink);
  border-width: var(--bw-thin) !important;   /* !important: vence o border-width 2px !important do .btn base — largura idêntica ao control (1px) */
  border-style: var(--bs-solid) !important;
  border-color: var(--c-border-strong) !important;   /* !important: vence o border-color !important do .btn base */
  min-height: var(--btn-h);
  font-weight: var(--fw-bold);
  font-size: var(--fs-14);
  box-shadow: var(--shadow-control);   /* L3: era literal 0 1px 1px rgba(6,27,66,.06) — tokenizado (mesmo valor, familia navy) */
}
.btn--primary:hover, .btn--secondary:hover {
  /* btn-estilo-control: fundo/cor NÃO mudam no hover (substitui o swap azul do .btn:hover base) —
     mesmo comportamento do control real, que mantém bg branco/texto ink ao ganhar foco; só
     border-color+box-shadow reproduzem o estado :focus-visible do control (idêntico a ccacf7b,
     agora coerente porque o normal também é branco). */
  background: var(--c-white);
  color: var(--c-ink);
  border-color: color-mix(in srgb, var(--c-accent) 38%, transparent) !important;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent);
}
.btn--block { width: 100%; }
/* icon-only QUIET (DESIGN-SPEC B2/R1): ghost 44 SEM borda, r-pill, cinza; hover/aberto = wash de
   marca (accent-soft). No legado 100% dos 113 icon-only eram borderless — INCLUSIVE a ação
   primária do page-head (evidência do dono, /global/accounts/ legado: add-membro quiet puro,
   correção do S2-fix-b2; a leitura anterior "--outline/--primary composto mantém borda" estava
   ERRADA e foi removida — icon-only NUNCA tem borda; borda/outline é exclusiva de botão COM
   texto). Destaque de primária icon-only, se o design pedir, é por ícone/cor — não por borda.
   FUNDE os idiomas ghost paralelos: .icon-btn (lápide adiante), o kebab-trigger
   (.ui-actions-menu__trigger, mesma anatomia) e a lupa da busca (B1: lupa virou ícone interno
   via .field--icon). .field__action (olho de senha) NÃO funde: affordance intra-campo 36px.
   O composto .btn--primary.btn--icon herda o quiet naturalmente (botão único ADR-0002: variantes
   não têm regras de aparência); o guarda explícito abaixo trava isso contra regressão. */
.btn--icon,
.btn--icon.btn--primary, .btn--icon.btn--secondary {
  width: var(--btn-h); padding-inline: 0; flex: none;
  border: none !important;   /* vence o border !important do .btn base */
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--c-gray);
}
.btn--icon:hover { background: var(--c-accent-soft); color: var(--c-accent); }
.btn--icon:active { background: color-mix(in srgb, var(--c-accent) 16%, var(--c-white)); color: var(--c-accent); }
.btn--icon.btn--danger { color: var(--c-danger); }
.btn--icon.btn--danger:hover, .btn--icon.btn--danger:active { background: var(--c-danger-bg); color: var(--c-danger); }
/* ícone dentro de QUALQUER .btn = --icon-md (a folha do Google traz 24px; sem isto o átomo variava
   20/24 conforme a classe do span — .icon vs .material-symbols-outlined). */
.btn .icon, .btn .material-symbols-outlined { font-size: var(--icon-md); }
/* BOTAO UNICO (ADR-0002, ciclo "muda 1 muda todos"): o .btn base acima e a UNICA definicao de
   aparencia (fundo/cor/borda + estados). As variantes --primary / --secondary / --danger NAO
   sobrescrevem fundo/cor/borda — herdam 100% do base (fundo var(--c-white), texto var(--c-info),
   borda var(--c-navy)) e os estados do base. Sobrescritas removidas neste ciclo; a semantica por
   variante (ex.: --danger vermelho) fica para ciclo futuro. Mudar o base muda TODOS os botoes. */

/* ═════════ .field (input / textarea / select) ═════════ */
/* B6 (DESIGN-SPEC): morreu o `max-width: var(--sidebar-w)` — token de SIDEBAR travando todo campo
   em 264px era semântica errada e encolhia forms inteiros (acidente do refac). Largura do campo =
   100% da célula do grid; largura é responsabilidade do layout (form-grid--2/--3), não do átomo. */
/* C1 [detail-labels-tipografia]: variante estática read-only (.field__static, valor equivalente
   ao .field__control editável) NÃO mora aqui — vive em atoms.css (camada tipográfica, role:
   control), junto de .field__label. Este bloco cobre só a estrutura dos controles editáveis
   (input/textarea/select); .field__static não tem propriedade estrutural própria (herda
   posicionamento de .field__wrap), por isso não aparece aqui — arquitetura intencional
   (atoms=tipografia, components=estrutura), não taxonomia perdida. */
.field { display: flex; flex-direction: column; gap: var(--sp-6); }
/* PASSO PB [detail-labels-tipografia]: regra .field__label movida por completo pra
   atoms.css (fs+fw+color juntos agora, extraído do grupo role:label compartilhado —
   não fazia mais sentido ficar split entre 2 arquivos). */
.field__wrap { position: relative; display: flex; align-items: center; }
.field__control,
.field__wrap > input,
.field__wrap > select,
.field__wrap > textarea {
  width: 100%;
  height: var(--control-h);
  padding: var(--sp-14);   /* label-placeholder-owner: 12→14 — legado core.css:948 (indent do wrapper, não text-indent) */
  border: var(--bw-thin) var(--bs-solid) var(--c-border-strong);   /* S4b/A18: controle = borda-que-aparece; --c-gray e papel de TEXTO (A4); legado usava borda clara */
  border-radius: var(--r-control);
  background: var(--c-white);
  font-family: var(--font-sans);
  font-size: var(--fs-14);   /* label-placeholder-owner-P2: B6 REVOGADO por decisão do dono — 14px por
    fidelidade ao legado (core.css:2683, people-scope), aceitando de volta o auto-zoom do iOS que B6
    corrigia (fonte <16px foca-zoom no Safari iOS). Trade-off consciente, não acidente. */
  line-height: 1.45;   /* label-placeholder-owner: legado core.css:159 (body, herdado) — literal, sem --lh-* que bata (mesmo idioma de .kpi__ring-value/.grades__chip-x, que já usam line-height cru) */
  transition: border-color var(--dur-fast) var(--ease-base);
}
.field__control::placeholder,
.field__wrap > input::placeholder,
.field__wrap > select::placeholder,
.field__wrap > textarea::placeholder {
  color: var(--c-text-placeholder);
  font-weight: var(--fw-medium);   /* label-placeholder-owner: legado core.css:999 (peso 500) */
}
.field__control[data-has-value="1"],
.field__wrap > input[data-has-value="1"],
.field__wrap > select[data-has-value="1"],
.field__wrap > textarea[data-has-value="1"] {
  color: var(--c-ink);   /* label-placeholder-owner: legado core.css:1024 (--c-text:#0e2a56 — idêntico a --c-ink aqui) */
  font-weight: var(--fw-bold);   /* legado core.css:1025 (peso 700) */
}
/* efeito de foco/estado dos controles = PORTADO do legado Contatos (/people/novo/, decreto
   Alli 2026-07-22, kit-focus-legado): borda + glow suave em accent, medido em
   core.css:2601-2608 (.people-scope form .input:focus-within — border-color rgba(accent,.38)
   + box-shadow 0 0 0 1px rgba(accent,.18), 0 0 16px rgba(accent,.10)). --c-accent é IDÊNTICO
   nos dois repos (#007a1a) — porta via token+color-mix, não literal. SUPERSEDE o decreto
   anterior "sem ring, só borda" (form-controls-roadmap-A-owner, que espelhava o catalog-scope
   do legado, não o people-scope — ver retorno da tarefa p/ a divergência entre os dois). A
   animação neon (.is-attn, legado) NÃO foi portada — é estado de atenção-inicial específico,
   não o efeito de foco genérico pedido. */
.field__control:focus-visible,
.field__wrap > input:focus-visible,
.field__wrap > select:focus-visible,
.field__wrap > textarea:focus-visible {
  outline: none;   /* produtos-focus-fix: suprime o ring nativo do browser (competia com o glow) — espelha .field--icon:170, que já fazia isso; o glow (box-shadow) é o indicador de foco visível, a11y preservada */
  border-color: color-mix(in srgb, var(--c-accent) 38%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent);
}
.field__control:disabled,
.field__wrap > input:disabled,
.field__wrap > select:disabled,
.field__wrap > textarea:disabled { opacity: var(--op-disabled); cursor: not-allowed; }
.field__control--textarea,
.field__wrap > textarea { height: auto; min-height: calc(var(--control-h) + var(--sp-20)); resize: vertical;   /* S4/F4: 72px por degraus proprios, nao token de sidebar */ line-height: var(--lh-base); }
.field__control--select,
.field__wrap > select { cursor: pointer; }
/* .catalog-notes: textarea de Observações do produto — sem resize (drag handle some) + scroll
   interno no overflow. Escopado pela classe do widget (catalog/forms.py, ProductForm.Meta.widgets
   notes), não por [name="notes"] global — people/forms.py:100 também tem campo "notes" (Contact),
   colisão evitada. Demais textareas do kit seguem resize:vertical (components.css:115). */
.field__wrap > textarea.catalog-notes { resize: none; overflow-y: auto; }
/* icone a esquerda */
.field__icon {
  position: absolute; left: var(--sp-12); top: 50%; transform: translateY(-50%);
  color: var(--c-gray); font-size: var(--icon-sm); pointer-events: none;   /* S4/T1: icone por --icon-* */
}
/* acao a direita (ex.: olho de senha) */
.field__action {
  position: absolute; right: var(--sp-8); top: 50%; transform: translateY(-50%);
  height: var(--btn-h-sm); padding-inline: var(--sp-8);
  color: var(--c-gray); font-size: var(--icon-sm);
  border-radius: var(--r-sm); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-base);
}
.field__action:hover { color: var(--c-ink); }
/* fix-icone-mecanica-flex: mecânica FLEX do legado (crontex_from_prod_clean
   core.css:942-984 .input/.input .icon — ícone É flex-irmão do input dentro de um
   wrapper com border+padding+gap; auth.css:61-69 .input input É borderless, flex:1).
   A "caixa" (borda/bg/altura) muda de dono pro .field__wrap quando há ícone; o
   .field__control interno some (sem borda/bg própria), igual ao legado. Corrige 2
   bugs: (1) `.field--icon .field__control{padding-left:40px}` (removida acima) NUNCA
   casava — BoundField real não tem class="field__control" (só _atoms_ da bancada
   tinham); a base `.field__wrap > input` (elemento) sempre vencia, por isso 14px em
   vez de 40. (2) mesmo casando, padding-sobre-absolute não é a mecânica do legado —
   o legado usa flex+gap, não overlay. */
.field--icon .field__wrap {
  gap: var(--sp-10);
  padding-inline: var(--sp-14);
  border: var(--bw-thin) var(--bs-solid) var(--c-border-strong);
  border-radius: var(--r-control);
  background: var(--c-white);
  height: var(--control-h);
  transition: border-color var(--dur-fast) var(--ease-base);
}
.field--icon .field__icon { position: static; flex: none; top: auto; transform: none; }   /* fix-icone-alinhamento-vertical: zera o top/translateY(-50%) da regra base (linha acima) — resíduo do mecanismo absolute antigo; centraliza só por align-items:center da wrapper, igual ao legado */
.field--icon .field__control,
.field--icon .field__wrap > input,
.field--icon .field__wrap > select,
.field--icon .field__wrap > textarea {
  flex: 1; width: auto; min-width: 0;   /* legado auth.css:64-66 .input input — sem isto, width:100% da base soma ao ícone e estoura o wrapper */
  padding-left: 0;
  border: none;
  background: transparent;
  height: 100%;
}
.field--icon .field__control:focus-visible,
.field--icon .field__wrap > input:focus-visible,
.field--icon .field__wrap > select:focus-visible,
.field--icon .field__wrap > textarea:focus-visible { outline: none; }
.field--icon .field__wrap:focus-within {
  border-color: color-mix(in srgb, var(--c-accent) 38%, transparent);   /* kit-focus-legado: mesma família do glow — ver components.css:104 */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent);
}
.field__control[type="search"],
.field__wrap > input[type="search"] { appearance: none; -webkit-appearance: none; }   /* remover-input-duplo-bancada: input[type=search] tem appearance:auto por padrão — o browser desenha chrome nativo de campo de busca (decoração própria, independente do border:none já correto em .field--icon .field__control, linha 203-212) por cima da moldura do .field__wrap, produzindo a dupla moldura. Fix padrão/universal, sem downside conhecido. Vive na classe canônica (não em #ks-*): _search_form.html é partial compartilhado com ~22+ listas de produção, mesmo bug lá — autorização humana explícita para aplicar aqui, não só na bancada. */
.field--icon.field--error .field__wrap { border-color: color-mix(in srgb, var(--c-warn) 55%, transparent); }   /* L3: era rgba(138,75,0,.55) — mesmo computado, derivado de --c-warn (#8a4b00) */
/* fix-action-padding-seletor-morto: mesmo bug do .field--icon (7259e58) — .field__control
   é classe morta em BoundField real; troca pro padrão de seletor de elemento. Valor 40px
   LITERAL do legado (auth.css:71-73 .input.has-pw-toggle input — sem token lá, sem --sp-40
   aqui; porta literal, não aproxima pro --sp-32 existente). */
.field--action .field__control,
.field--action .field__wrap > input,
.field--action .field__wrap > select,
.field--action .field__wrap > textarea { padding-right: 40px; }
/* toggle de visibilidade de senha: o HTML traz os 2 ícones; o CSS alterna qual aparece.
   default = olho (esconde o cortado); .is-visible (posta pelo JS no clique) = inverte. */
.field__action__eye { display: block; }
.field__action__eye-off { display: none; }
.field__action.is-visible .field__action__eye { display: none; }
.field__action.is-visible .field__action__eye-off { display: block; }
/* erro */
.field--error .field__control,
.field--error .field__wrap > input,
.field--error .field__wrap > select,
.field--error .field__wrap > textarea { border-color: color-mix(in srgb, var(--c-warn) 55%, transparent); }   /* âmbar fiel ao legado — escopado a .field--error, --c-danger (botão/badge/alert) intocado; L3: rgba(138,75,0,.55)→color-mix(--c-warn), mesmo computado */
.field__error { font-size: var(--fs-12); color: color-mix(in srgb, var(--c-warn) 55%, transparent); }   /* L3: idem */
/* .field__help — tipografia integralmente via role caption (12/gray) */
.field__req { color: var(--c-danger); }
.field--check { flex-direction: row; align-items: center; gap: var(--sp-8); }

/* ═════════ .check / .radio (appearance none, desenhados com tokens) ═════════ */
.check, .radio { display: inline-flex; align-items: center; gap: var(--sp-8); cursor: pointer; }   /* tipografia: role body */
.check__box, .radio__box {
  appearance: none; -webkit-appearance: none;
  width: var(--sp-20); height: var(--sp-20);
  margin: var(--sp-0);
  border: var(--bw-thin) var(--bs-solid) var(--c-gray);
  background: var(--c-white);
  display: inline-grid; place-content: center;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-base), border-color var(--dur-fast) var(--ease-base);
}
.check__box { border-radius: var(--r-sm); }
.radio__box { border-radius: var(--r-circle); }
.check__box::after {
  content: ""; width: var(--sp-4); height: var(--sp-8);
  border-style: var(--bs-solid); border-color: var(--c-white);
  border-width: 0 var(--bw-thick) var(--bw-thick) 0;
  transform: rotate(45deg);
  opacity: 0; transition: opacity var(--dur-fast) var(--ease-base);
}
.radio__box::after {
  content: ""; width: var(--sp-8); height: var(--sp-8);
  border-radius: var(--r-circle); background: var(--c-white);
  opacity: 0; transition: opacity var(--dur-fast) var(--ease-base);
}
.check__box:checked, .radio__box:checked { background: var(--c-cta); border-color: var(--c-cta); }
.check__box:checked::after, .radio__box:checked::after { opacity: 1; }
.check__box:focus-visible, .radio__box:focus-visible,
.check__box:hover:not(:disabled), .radio__box:hover:not(:disabled) {
  border-color: color-mix(in srgb, var(--c-accent) 38%, transparent);   /* kit-focus-legado: glow — components.css:104; card-lista-f5: hover entra na MESMA regra (família única, zero sombra nova) — :not(:disabled) p/ o desabilitado não sinalizar interatividade */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent);
}
.check__box:disabled, .radio__box:disabled { opacity: var(--op-disabled); cursor: not-allowed; }

/* ═════════ .link ═════════ */
.link {
  color: var(--c-accent);
  font-family: var(--font-sans); font-size: var(--fs-14);
  border-radius: var(--r-sm);
  transition: color var(--dur-fast) var(--ease-base);
}
.link:hover { color: color-mix(in srgb, var(--c-accent) 80%, black); text-decoration: underline; }
.link:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: var(--ring-offset); }
.link--muted { color: var(--c-gray); }
.link--muted:hover { color: color-mix(in srgb, var(--c-gray) 80%, black); }

/* ═════════ .icon (Material Symbols Outlined; --icon-* = tamanho, --c-* = cor) ═════════ */
.icon {
  font-family: 'Material Symbols Outlined';
  font-weight: var(--fw-regular);
  font-style: normal;
  font-size: var(--icon-md);
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  vertical-align: middle;
  -webkit-font-smoothing: antialiased;
  font-feature-settings: 'liga';
  -webkit-font-feature-settings: 'liga';
  font-variation-settings: 'opsz' 24, 'wght' 400, 'FILL' 0, 'GRAD' 0;
}
.icon--sm { font-size: var(--icon-sm); }
.icon--lg { font-size: var(--icon-lg); }
.icon--xl { font-size: var(--icon-xl); }

/* ═════════ .card (superfície) ═════════ */
.card {
  background: var(--c-white);
  border: var(--bw-thin) var(--bs-solid) var(--c-border-subtle);   /* S4/A17: hairline navy do legado (mata o gray-mix literal) */
  border-radius: var(--r-container);   /* F1-fix: container RETO (medido: .card legado = 0px) */
  padding: var(--sp-16);
  box-shadow: var(--shadow-container);   /* F1-fix: sombra MEDIDA do section-card legado (.card.pop 0 10px 30px navy .10) — era shadow-sm */
}
.card--subtle { box-shadow: none; }   /* sutil = sem elevação (= .card.subtle do legado) */

/* ═════════ .alert (mensagens — primitivo global; messages do Django) ═════════ */
.alert { padding: var(--sp-12) var(--sp-14); border-radius: var(--r-container); font-size: var(--fs-14); border: var(--bw-thin) var(--bs-solid) transparent; }
.alert--success { background: var(--c-success-bg); color: var(--c-success); border-color: color-mix(in srgb, var(--c-success) 30%, var(--c-white)); }
.alert--error, .alert--danger { background: var(--c-danger-bg); color: var(--c-danger); border-color: color-mix(in srgb, var(--c-danger) 30%, var(--c-white)); }
.alert--warning { background: var(--c-warn-bg); color: var(--c-warn); border-color: color-mix(in srgb, var(--c-warn) 30%, var(--c-white)); }   /* S4/A8: borda na propria familia */
.alert--info { background: var(--c-info-bg); color: var(--c-ink); border-color: color-mix(in srgb, var(--c-ink) 20%, var(--c-white)); }

/* ═════════ .kpi (métrica) + .kpi-grid — 1º componente de dados ═════════ */
/* C5: minmax por token próprio --kpi-min (o calc(--sidebar-collapsed-w*2) antigo acoplava
   largura de KPI a sidebar — coincidência numérica em 144, semântica falsa). Sem margem
   externa: o ritmo vem SEMPRE do gap do pai (stack/card-grid). */
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--kpi-min), 1fr)); gap: var(--sp-12); }

/* ═════════ .card-grid (grid emergente de cards — dashboards/coleções; DESIGN-SPEC C1/C5) ═════════ */
/* Colunas 3/2/1 EMERGENTES por auto-fit — ZERO media query (o legado precisava de 2: 900/640);
   grid-auto-rows:1fr = altura uniforme por linha (decisão do legado, core.css:1805-1811).
   Densidade do Painel: card de módulo pad sp-12 (C6 nível denso); num card de ~350px o
   kpi-grid interno auto-fit já produz o 2×2 do legado sem CSS dedicado. */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--card-min), 1fr)); gap: var(--sp-12); align-items: stretch; grid-auto-rows: 1fr; }
.card-grid > .card { padding: var(--sp-12); }
.kpi { display: flex; flex-direction: column; gap: var(--sp-4); padding: var(--sp-16); background: var(--c-white); border: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); border-radius: var(--r-container); }   /* S4/A17: hairline navy (legado kpi .06) */
.kpi__copy { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }   /* kpi-donut-produto: wrapper do rótulo+valor — libera .kpi p/ virar row quando tem donut (.kpi--chart), sem mudar o visual do kpi simples */
.kpi__label { color: var(--c-gray); }   /* fs+fw: role label */
.kpi__value { line-height: var(--lh-tight); }   /* tipografia: role metric (24/700 tabular-nums — B4/R4) */
.kpi--sm { padding: var(--sp-12); }
.kpi--sm .kpi__value { font-size: var(--fs-20); }
.kpi__value--success { color: var(--c-success); }
.kpi__value--warning { color: var(--c-warn); }   /* S4b/A9: yellow NUNCA em texto (2.2:1); warn e o ambar-texto AA (A8) */
.kpi__value--danger  { color: var(--c-danger); }
.kpi__value--muted   { color: var(--c-gray); }

/* kpi-donut-produto: donut de progresso nos KPIs de card de módulo (Contatos/Membros/Estoque/...
   — os que já computam chart_pct na view). Pixel-fiel ao legado (core.css:1855-1921
   .kpi-chart-ring), tokenizado: cores via color-mix(--c-ink), tamanho/duração em tokens
   dedicados (--kpi-ring-size/--dur-kpi-ring, literais de fidelidade). */
.kpi--chart { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--sp-12); }
.kpi__ring {
  --kpi-progress: 0;
  position: relative;
  flex: 0 0 auto;
  width: var(--kpi-ring-size);
  height: var(--kpi-ring-size);
  border-radius: var(--r-circle);
  display: grid;
  place-items: center;
  background:
    radial-gradient(circle at center, var(--c-white) 0 55%, transparent 57%),
    conic-gradient(color-mix(in srgb, var(--c-ink) 45%, transparent) calc(var(--kpi-progress) * 1%), color-mix(in srgb, var(--c-ink) 6%, transparent) 0);
  animation: kpi-ring-fill var(--dur-kpi-ring) var(--ease-out) forwards;
}
.kpi__ring-value { color: var(--c-ink); font-size: var(--fs-11); font-weight: var(--fw-bold); line-height: 1; }
.kpi__sub { color: var(--c-gray); font-size: var(--fs-11); font-weight: var(--fw-semibold); }   /* dash-headline-remove-produto: subtexto do chart_label (ex. "62%"), fora do anel — pixel-fiel ao legado .kpi-chart-sub (core.css:1912-1915) */
@keyframes kpi-ring-fill { to { --kpi-progress: var(--kpi-pct); } }

/* ═════════ .status-icon (átomo — caixa tonal de estado 44×44; DESIGN-SPEC B5) ═════════ */
/* Destilado do .bling-state-icon do legado: a caixa tonal carrega o estado num canal além do
   glifo. Uso canônico: faixa de status do workspace (ADR-0006) no lugar do ícone solto. */
.status-icon { display: inline-grid; place-items: center; flex: 0 0 auto; width: var(--btn-h); height: var(--btn-h); border-radius: var(--r-control); background: var(--c-surface-sunken); color: var(--c-gray); }
.status-icon .icon, .status-icon .material-symbols-outlined { font-size: var(--icon-md); }
.status-icon--success { background: var(--c-success-bg); color: var(--c-success); }
.status-icon--warning { background: var(--c-warn-bg); color: var(--c-warn); }
.status-icon--danger  { background: var(--c-danger-bg); color: var(--c-danger); }
.status-icon--info    { background: var(--c-info-bg); color: var(--c-info); }

/* lapide: .icon-btn morto no S2 (DESIGN-SPEC B2 — idioma ghost paralelo ao .btn--icon quiet;
   consumidores chat/producao/formsets migrados p/ .btn.btn--icon). Nome nao reusar. */

/* ═════════ utilitários de página (reutilizáveis pós-login) ═════════ */
.page-title { margin: 0; }   /* tipografia: role title-page */
.stack { display: flex; flex-direction: column; gap: var(--sp-16); }
.card__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-12); margin-bottom: var(--sp-12); }
.card__title { display: flex; align-items: center; gap: var(--sp-8); margin: 0; }   /* tipografia: role title-card */
.card__title a { color: inherit; }

/* ═════════ .table (dados) — léxico de lista ═════════ */
.table-wrapper { overflow-x: auto; background: var(--c-white); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-container); will-change: transform; }   /* hairline-scroll-x-fix: .table usa table-layout:auto (linha 394/407 — coluna Nome sem width própria), colunas nascem em larguras fracionárias de sub-pixel; bordas verticais de 1px (--bw-thin, linha 477-482) nessas posições somem/piscam durante o scroll-x por re-rasterização por frame. will-change:transform promove .table-wrapper a uma camada de composição GPU estável (rasteriza 1x, só translada) — mitiga o sintoma sem mexer no layout de colunas. Global: todas as ~22 lists com .table-wrapper (Produtos/Estoque/Contas/Contatos/Pedidos/Studio) herdam. Se persistir, próximo passo é table-layout:fixed com larguras explícitas (mudança maior, gate próprio). */
.table { width: 100%; border-collapse: collapse; font-size: var(--fs-14); }
/* FAIXA do thead (B3): tint de marca + hairlines navy top/bottom — a assinatura consistente do
   S01 legado por 3 declarações; th 12/600 up ls-wide, td pad-12 fs14 → linha-alvo 44-48px. */
.table thead th { background: var(--c-surface-tint); text-align: left; white-space: nowrap; padding: var(--sp-10) var(--sp-12); color: var(--c-gray); text-transform: uppercase; letter-spacing: var(--ls-wide); border-top: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }   /* fs+fw: role label */
.table tbody td { padding: var(--sp-12); color: var(--c-ink); vertical-align: middle; border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border); }   /* produtos-lista-valign: centraliza conteúdo (thumb/texto/ações) verticalmente na linha — owner compartilhado (.table), 23 consumidores; melhoria neutra */
.table tbody tr:last-child td { border-bottom: none; }
/* produtos-lista-col-sku: coluna SKU (4ª: check·Cod·Imagem·SKU·Nome) limitada a 13 dígitos
   (EAN-13) + folga pequena. Medido (render real, td:nth-child(4)): fonte 16px Inter, 13
   dígitos = 130px = 13ch exatos (numerais tabulares); padding da célula = 14px×2 = 28px;
   box-sizing:border-box (reset.css:1) → width precisa incluir o padding. 17ch = 170px
   border-box → ~142px de texto útil (130px + ~12px de folga). Escopado por
   [data-bulk-table="catalog-products"] (wrapper único da lista de produtos, produto_list.html:31
   — KI-0018 já documenta este data-attr como valor fiel, não classe fantasma) — NÃO toca
   .table de outros 22 consumidores. Nome (5ª coluna) sem width própria: table-layout:auto
   (medido) já distribui a sobra pra ela. */
/* card-lista-f0-17ch: guarda min-width — a regra era calibrada pro layout de TABELA (desktop);
   sem a guarda ela vazava pro mobile, onde o td vira grid de 2 colunas (.table--stack) e o
   width:170px border-box espremia a coluna do VALOR pra 11px (SKU "2462" quebrava dígito a
   dígito — medido). >520px: comportamento idêntico ao anterior. ≤520px: largura natural. */
@media (min-width: 521px) {
  [data-bulk-table="catalog-products"] .table th:nth-child(4),
  [data-bulk-table="catalog-products"] .table td:nth-child(4) { width: 17ch; }
}
/* [escopo:colunas-limpas-truncamento-listas] table-layout:fixed escopado a catalog-products
   (não ao .table global — mudaria a distribuição de largura das outras 21 listas sem
   necessidade). Motivo: .cell--truncate (max-width:0 + overflow:hidden) sinaliza ao algoritmo
   de auto-layout "esta coluna quer o mínimo possível" — sem outras colunas com largura
   explícita, o auto-layout redistribuía a sobra pras colunas SEM restrição (Variantes chegou a
   196px, Nome caiu a 76px — medido, screenshot mostrando "Prod…" truncado quase no primeiro
   token). Com table-layout:fixed + largura explícita em TODAS as colunas menos Nome, a única
   coluna sem width fica com 100% do restante — o inverso do problema medido. Larguras
   calibradas para ficar próximas do auto-layout original (Cod/Imagem/Variantes/Preço/Estoque),
   olho por medição, não arbitrárias. ks-crud-list-demo (bancada, #ks-crud-list) inclusa no
   MESMO seletor — runtime e bancada compartilham a mesma correção, mesma calibração.
   min-width:861px (NÃO 641px, o genérico do .cell--truncate/contatos): achado por medição,
   contrariando a premissa inicial (assumi 640px pra TODAS as listas, por analogia a contatos).
   O mecanismo .table--stack GENÉRICO (linha ~692, data-label/overline) tem breakpoint próprio
   861px — MAIS LARGO que os 640px de contatos (@media max-width:640px, linha ~851, bloco
   independente) — e o card rico de produtos (grid-template-areas, "card-lista-f1-grid-areas")
   fica ANINHADO ainda mais fundo, dentro de @media(max-width:520px) dentro desse bloco de
   860px. Ou seja: 521-860px = stacking genérico (label:value); ≤520px = card rico; >860px =
   desktop. width=700 (dentro de 521-860) renderizava stacking genérico, não table-row — as
   regras de desktop abaixo, escopadas antes a 641px, brigavam com esse modo (nome_td_width
   caía a 0/28px). Corrigido para 861px, batendo com o breakpoint real. Abaixo, uma segunda
   regra neutraliza .cell--truncate especificamente na janela 641-860px para catalog-products/
   ks-crud-list-demo, deixando o stacking genérico (que já resolve Nome via grid label:value,
   sem truncamento) undisputed. */
@media (min-width: 861px) {
  [data-bulk-table="catalog-products"] .table,
  [data-bulk-table="ks-crud-list-demo"] .table { table-layout: fixed; }
  [data-bulk-table="catalog-products"] .table th:nth-child(2),
  [data-bulk-table="catalog-products"] .table td:nth-child(2),
  [data-bulk-table="ks-crud-list-demo"] .table th:nth-child(2),
  [data-bulk-table="ks-crud-list-demo"] .table td:nth-child(2) { width: 5ch; }
  [data-bulk-table="catalog-products"] .table th:nth-child(3),
  [data-bulk-table="catalog-products"] .table td:nth-child(3),
  [data-bulk-table="ks-crud-list-demo"] .table th:nth-child(3),
  [data-bulk-table="ks-crud-list-demo"] .table td:nth-child(3) { width: calc(56px + var(--sp-14) * 2); }
  [data-bulk-table="catalog-products"] .table th:nth-child(6),
  [data-bulk-table="catalog-products"] .table td:nth-child(6),
  [data-bulk-table="ks-crud-list-demo"] .table th:nth-child(6),
  [data-bulk-table="ks-crud-list-demo"] .table td:nth-child(6) { width: 16ch; }
  [data-bulk-table="catalog-products"] .table th.cell--num,
  [data-bulk-table="catalog-products"] .table td.cell--num,
  [data-bulk-table="ks-crud-list-demo"] .table th.cell--num,
  [data-bulk-table="ks-crud-list-demo"] .table td.cell--num { width: 10ch; }
  /* guarda contra colapso: medido em 1024px — a soma das larguras fixas das colunas vizinhas
     zerava a coluna Nome (0px, nome invisível) em telas mais estreitas que essa soma. Testado
     min-width no <td>: SEM efeito — table-layout:fixed usa só os valores de width das colunas
     pra distribuir espaço, min/max-width por célula não participam do algoritmo (comportamento
     de spec, confirmado por medição, não suposição). 1ª tentativa de min-width na <table> ainda
     zerava Nome — faltava contar o border-spacing horizontal de .table--cards (--sp-16,
     components.css:552): com 9 colunas (10 gaps × 16px = 160px) essa folga não entra nas
     larguras de coluna, mas CONSOME espaço do total antes da distribuição — sem somá-la ao
     min-width, o "resto" calculado pro Nome ficava consistentemente 160px menor que o
     previsto (medido: 18-28px em vez de ~108px). Fix: min-width na <table> incluindo os 10
     gaps de --sp-16. Abaixo desse total, o .table-wrapper (overflow-x:auto, components.css:403,
     já é o mecanismo padrão de todas as 22 listas) rola horizontalmente; Nome nunca fica menor
     que o mínimo somado aqui. Soma: check(--btn-h) + Cod(5ch) + Imagem(56px+padding) + SKU(17ch)
     + Variantes(16ch) + Preço(10ch) + Estoque(10ch) + Ações(--btn-h) + Nome-mínimo(12ch) +
     10×border-spacing(--sp-16). */
  [data-bulk-table="catalog-products"] .table,
  [data-bulk-table="ks-crud-list-demo"] .table {
    min-width: calc(var(--btn-h) + 5ch + 56px + var(--sp-14) * 2 + 17ch + 16ch + 10ch + 10ch + var(--btn-h) + 12ch + var(--sp-16) * 10);
  }
}
/* janela 641-860px: catalog-products/ks-crud-list-demo ainda estão no card mobile PRÓPRIO
   (breakpoint 860px, acima) — .cell--truncate genérico (ativo desde 641px pras outras listas)
   precisa ficar inerte aqui, ou brigaria com o -webkit-line-clamp:2 que o card mobile já aplica
   no mesmo .link (max-width:0 no <td> teria efeito zero de qualquer forma dentro do grid do
   card, mas o filho `> *` force um white-space:nowrap que cortaria o clamp de 2 linhas). */
@media (min-width: 641px) and (max-width: 860px) {
  [data-bulk-table="catalog-products"] .table .cell--truncate,
  [data-bulk-table="ks-crud-list-demo"] .table .cell--truncate { max-width: none; overflow: visible; }
  [data-bulk-table="catalog-products"] .table .cell--truncate > *,
  [data-bulk-table="ks-crud-list-demo"] .table .cell--truncate > * { white-space: normal; overflow: visible; text-overflow: clip; }
}
.table tbody tr[data-href] { cursor: pointer; transition: background var(--dur-fast) var(--ease-base); }
.table tbody tr[data-href]:hover { background: var(--c-surface-subtle); }
.table .col-check { width: var(--btn-h); }   /* B3: célula de seleção 44 (checkbox 16-20 centrado) */
.table code { font-family: var(--font-mono); font-size: var(--fs-12); }

/* list-flutuante-kebab: linha flutuante do legado (core.css:1102-1144) — REABRE a decisão
   KI-0014 item 2 (row tiras×hairline), por decreto do dono. Escopo estrito a .list-frame
   .table (só telas de lista real) — as demais tabelas (detail/form/kanban/grade) seguem
   hairline flat, intocadas. Mantém o kebab de linha do refactoring (não reintroduz
   Editar/Excluir soltos). O clipping do popover (razão 3 da KI-0014) já tem mitigação
   existente (.table-wrapper:has(.ui-actions-menu[open]) abaixo) — testado explicitamente. */
.list-frame .table { border-collapse: separate; border-spacing: 0 var(--sp-8); }
/* lista-linhas-hairline: perímetro completo 1px em vez de sombra — idioma único com o header
   (que já usa border-bottom 1px --c-border-subtle, pós lista-header-toldo-off). A base global
   (.table tbody td, components.css:397) usa --c-border (#e5e7eb, flat gray/legado-line) —
   DIVERGE do --c-border-subtle (navy-wash 8%) que os divisores verticais desta lista já usam
   (list-td-divisorias, abaixo). Override explícito para --c-border-subtle: as 4 bordas da
   célula (topo/direita/baixo/esquerda) ficam no MESMO tom, coerente com o header. td:last-child
   perde o override antigo — cada linha agora é um retângulo fino completo (não só a base). */
.list-frame .table tbody td { border-top: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
/* SUBSTITUÍDO (lista-sombra-cards-monitor, decreto Alli 2026-07-23c): a sombra da row deixa
   de ser --shadow-sm/--shadow-md (lista-sombras-restore) e passa a ser IDÊNTICA à do .card
   do módulo Monitor — medida em /monitor/, computed real: rgba(6,27,66,.1) 0 10px 30px =
   var(--shadow-container) (mesmo token do .card, components.css:264/tokens.css:188). O .card
   NÃO tem :hover próprio (medido — box-shadow constante), então a row também fica com 1 valor
   único, sem escalar no hover (hover de background global, components.css:411, segue ativo). */
.list-frame .table tbody tr { background: var(--c-white); box-shadow: var(--shadow-container); }
/* list-row-altura-legado: reabre KI-0014 item 6 (densidade/fs14 compacto) por decisão do
   dono — fs16 + lh do legado (core.css: body fs16 herdado/lh 1.45; --lh-base 1.5 é o token
   mais próximo já existente) + padding 12×14 (legado: --sp-12 --sp-14). Escopo estrito a
   .list-frame .table (só listas reais); demais tabelas (detail/form/kanban/grade) mantêm
   fs14 compacto (KI-0014 item 6 intocada fora de listas). */
.list-frame .table { font-size: var(--fs-16); line-height: var(--lh-base); }
/* SUBSTITUÍDO (lista-sombra-cards-monitor, decreto Alli 2026-07-23c): as 2 sombras
   específicas de lista (--shadow-list-header no tr + inset navy no th, RE-APLICADAS em
   lista-sombras-restore/2026-07-23b — histórico: nasceram como fidelidade ao legado
   core.css:1108-1117/1118, KI-0014 item 3) foram trocadas por UMA sombra única e igual à do
   .card do Monitor — var(--shadow-container), mesma da row (linha acima). --shadow-list-header
   (tokens.css:189) volta a ficar ÓRFÃ (0 consumidor); não removida do tokens.css sem decreto
   próprio (anotado de novo no KI-0014). */
.list-frame .table thead tr { box-shadow: var(--shadow-container); position: relative; z-index: 1; }
/* list-borda-match: a "borda" percebida no legado não é na tr/td — é no elemento INTERNO da
   célula. Diff visual confirmou: .avatar já tinha o mesmo border do legado (1px --c-border);
   o que faltava era o contorno do .tag/.badge de subcategoria (legado: border:1px solid
   rgba(6,27,66,.1)). Escopado só a .list-frame .table (não o .badge GLOBAL, usado em status
   em toda a app) — reusa color-mix(--c-ink) para o mesmo tom navy-wash do legado. */
.list-frame .table .badge { background: var(--c-white); border: var(--bw-thin) var(--bs-solid) color-mix(in srgb, var(--c-ink) 10%, transparent); }   /* chips-grade-kitchen-branco: fundo branco (era herdado do .badge global --c-surface-sunken); borda existente garante legibilidade sobre linha branca */
/* produtos-lista-nome-navy: link de navegação de linha (NOME/título) vira navy+semibold —
   escopado a .list-frame .table (mesmo idioma do chips-grade acima), NÃO ao .link GLOBAL
   (components.css:273-280, --c-accent verde), que também serve links textuais inline em
   auth/signup ("Criar conta", "Voltar ao login") — lá o verde É a affordance de clique;
   navy+semibold ali se confundiria com texto em negrito comum. Consumidores confirmados
   deste padrão (row-nav dentro de .list-frame .table): produtos, account_list, contact_list,
   order_list, studio_list — mudança aplicada a todos coerentemente. */
.list-frame .table .link { color: var(--c-ink); font-weight: var(--fw-semibold); }
.list-frame .table .link:hover { color: color-mix(in srgb, var(--c-ink) 80%, black); text-decoration: underline; }
/* list-td-divisorias: divisórias verticais entre colunas — legado (core.css:1121-1150)
   aplica border-left em td+td/th+th, 1px navy-wash 8% — --c-border-subtle já é EXATAMENTE
   esse tom (tokens.css: "legado navy-wash .06-.08"), reusado sem token novo. Escopado a
   .list-frame .table. Os extremos (:first-child/:last-child) tinham border-left/right
   próprios até o PASSO 8F — removidos quando a moldura de .table-wrapper passou a ser a
   única fonte de borda externa (comentário abaixo). */
.list-frame .table thead th + th,
.list-frame .table tbody td + td { border-left: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
/* col-check-edge-removido (PASSO 8F): border-left/right de :first-child/:last-child removidos —
   a moldura de .list-frame > .table-wrapper (restaurada acima) é agora a ÚNICA fonte de borda
   externa esquerda/direita. Mantê-los aqui também duplicaria a linha nas pontas do scroll
   (wrapper fixo + célula rolada ficando adjacentes em scrollLeft=0/max). Cada row segue com
   borda top/bottom própria (.list-frame .table tbody td, linha 436 — intocada) e divisórias
   internas entre colunas (td+td/th+th, linhas 478-479 — intocadas); só a lateral mais externa
   passou a ser responsabilidade do wrapper. */
.list-frame .table tbody td, .list-frame .table thead th { padding: var(--sp-12) var(--sp-14); }

/* ═════════ .table--cards (kitchen-sink ks-crud-list, aplica-lista-cards-kitchen-sink) — rows como cards flutuantes: sem borda, elevação via shadow, divisor vertical entre colunas ═════════ */
/* corrigir-cobertura-lista-cards: achado via Playwright (computed style) — mesma especificidade
   das regras .list-frame .table* acima (linhas 447/455/462/476/492-493) resolve por ORDEM NO
   ARQUIVO, não só por especificidade — por isso este bloco fica DEPOIS de todas elas. Escopo
   estrito ao demo: as ~22 lists reais que herdam .list-frame .table (sem .table--cards) continuam
   com hairline+shadow-container, intocadas. */
.list-frame .table-wrapper:has(.table--cards) { border: none; background: transparent; padding-top: 0; padding-inline: var(--sp-32); margin-inline: calc(-1 * (var(--sp-32) + var(--sp-16))); }   /* :has() já é padrão aceito no projeto (auth.css:91/110, kitchen_sink.css:772, components.css:510). padding-top:0 (equalizar-espacamento-toolbar-tabs-row): neutraliza .list-frame > .table-wrapper (linha 611, padding-block:--sp-4). padding-inline:32 (corrigir-sombra-scroll-tablet-lista-cards): --shadow-container (tokens.css:193) = "0 10px 30px" — blur-radius 30px, --sp-32 é o token existente mais próximo ≥30 (sem precisar criar token novo). Esse padding vira o "respiro" DENTRO da área com overflow-x:auto do wrapper — em qualquer posição de scroll, a sombra sempre tem 32px de zona segura antes do clip do próprio wrapper, não só quando a tabela cabe inteira. margin-inline:-(32+16): width:auto (wrapper não tem width própria) absorve margem negativa alargando o border-box automaticamente — sem isso a 1ª célula desalinharia do toolbar/page-head. Matemática: contentLeft = containerLeft + margin-left + padding-left; para contentLeft + border-spacing(16, 1ª célula) = containerLeft (312), margin-left = -(padding-left + 16) = -(32+16); alinhamento óptico (first_td_x=312) preservado por construção, não por coincidência. */
#ks-crud-list .toolbar.tabs { border-bottom: none; margin-bottom: 0; }   /* remover-border-bottom-toolbar-tabs: .tabs (components.css:893) é compartilhado com produção (accounts/catalog/ecommerce/monitor/base_detail/base_workspace) — .toolbar.tabs só existe em kitchen_sink.html:17109, escopo por ID de seção evita tocar o .tabs canônico (#ks-workspace, fiel à produção). .tab.is-active (linha 899) não é afetado: são elementos .tab diferentes, ausentes deste toolbar (só filter+search). margin-bottom:0 (equalizar-espacamento-toolbar-tabs-row): neutraliza .list-frame > .toolbar (linha 597, margin:--sp-12 0) — deixa o border-spacing (--sp-16) como única origem do gap toolbar→tabs, igual ao gap tabs→row1. */
.list-frame .table--cards { border-spacing: var(--sp-16) var(--sp-16); }   /* auditar-superficie-e-espacar-rows-cards: precisa do prefixo .list-frame — a regra decretada .list-frame .table (linha 436, border-spacing: 0 var(--sp-8)) tem especificidade maior que .table--cards sozinho (2 classes vs 1) e vencia silenciosamente, mesmo bug já corrigido nas outras regras deste bloco. --sp-16 = mesmo valor do gap de .stack em #ks-workspace (Playwright computed). Horizontal também precisa de inset — .table-wrapper tem overflow-x/y:auto (clip) e a row ocupava 100% da largura do wrapper sem margem, cortando a sombra nas laterais; border-spacing horizontal insere a mesma folga usada no espaçamento vertical entre rows. */
.list-frame .table--cards thead th { background: none; border-top: none; border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); font-size: var(--fs-14); font-weight: var(--fw-semibold); text-transform: none; letter-spacing: normal; }   /* refinar-lista-cards-elevacao-tabs: remove o bloco de cor (planilha), acrescenta hairline só embaixo; tipografia realinhada ao role:action de .tab (atoms.css:62-66, mesmos tokens --fs-14/--fw-semibold) em vez do role:label herdado (atoms.css:73) — cor já era --c-gray em ambos, sem mudança */
.list-frame .table--cards thead tr { box-shadow: none; }   /* neutraliza a sombra decretada em .list-frame .table thead tr — thead sem bloco de cor nem sombra própria */
.list-frame .table--cards tbody tr { box-shadow: var(--shadow-container); }   /* corrigir-sombra-lista-cards-referencia-card: paridade EXATA com .card (components.css:317-323) — mesma custom property, não valor paralelo */
.list-frame .table--cards tbody td { border-top: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); border-left: none; border-right: none; background: var(--c-white); }   /* mesma tripla border-width/style/color do .card (components.css:319); perímetro fechado abaixo (first/last-child) em vez de hairline por célula */
.list-frame .table--cards tbody td:first-child { border-left: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
.list-frame .table--cards tbody td:last-child { border-right: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
.list-frame .table--cards thead th + th,
.list-frame .table--cards tbody td + td { border-left: none; }   /* neutraliza o divisor full-height decretado (list-td-divisorias) — só o divisor centralizado abaixo permanece */
.table--cards tbody td:not(:last-child) { position: relative; }
.table--cards tbody td:not(:last-child)::after { content: ""; position: absolute; top: 25%; right: 0; height: 50%; width: var(--bw-thin); background: var(--c-border); }   /* divisor vertical centralizado, 50% da altura da row — estilo hr vertical, sem style inline */

/* ═════════ .toolbar (catálogo ADR-0020: T1 lista = __actions+__search, owner block do
   base_list · T2 status-strip = owner _toolbar_status · uso cru = util T5 documentado) ═════════ */
.toolbar { display: flex; align-items: center; gap: var(--sp-12); flex-wrap: wrap; }
.toolbar:has(.thumb) { display: grid; grid-template-columns: repeat(auto-fit, minmax(56px, 1fr)); gap: var(--sp-12); align-items: center; }   /* minithumb-gallery: grid auto-fit preenche espaço; minmax 56px = .thumb width (linha 982) */
.toolbar__spacer { flex: 1; }
.toolbar__actions, .toolbar__search { display: flex; align-items: center; gap: var(--sp-8); }
/* B1 (DESIGN-SPEC/R2): a busca OCUPA todo o vão da toolbar (legado: flex:1 1 260px min-200 em
   100% das lists com q) — 260/200px são medidas estruturais da molécula, não tokens de cor.
   O campo interno estica junto; lupa é ícone DENTRO do campo (.field--icon), submit por Enter,
   sem botão. Lists sem busca: o slot vazio vira o próprio spacer (flex:1). */
.toolbar__search { flex: 1 1 260px; min-width: 200px; justify-content: flex-end; }   /* slot sem busca (só links/botões): alinha à direita como antes */
.toolbar__search > form[role="search"], .toolbar__search .field { flex: 1 1 auto; min-width: 0; }

/* ═════════ .badge (rótulo de status — ÚNICO chip do kit; DESIGN-SPEC B7a absorve o .tag legado) ═════════ */
.badge { display: inline-flex; align-items: center; padding: var(--sp-4) var(--sp-10); border-radius: var(--r-pill); font-size: var(--fs-12); font-weight: var(--fw-bold); background: var(--c-surface-sunken); color: var(--c-ink); }   /* pad 4x10 do legado (2x8 apertava a pill fs12) */
.badge--danger { background: var(--c-danger-bg); color: var(--c-danger); }
.badge--warning { background: var(--c-warn-bg); color: var(--c-warn); }
.badge--success { background: var(--c-success-bg); color: var(--c-success); }
.badge--info { background: var(--c-info-bg); color: var(--c-info); }

/* ═════════ .pagination ═════════ */
/* PASSO C [pagination-listas]: pager numerado (chevrons + janela ±1 c/ ellipsis) promovido de
   mobile-only a padrão UNIVERSAL — era .pagination__pager display:none fora de ≤520px, com o
   trio Anterior/info/Próxima (texto) dono da barra no desktop/tablet; esse trio foi removido do
   HTML (_pagination.html), então a regra que só existia pra escondê-lo no mobile
   (.pagination__links > .btn/.pagination__info { display:none }) não faz mais sentido e saiu
   também — .pagination__info da classe global (atoms.css) segue intocada, ainda usada por
   integrations_bling.html/shop_list.html (markup próprio, fora do include _pagination.html;
   confirmado que nunca usaram a relação .pagination__links > .pagination__info removida aqui,
   então nenhum efeito colateral neles). Ativo = idioma do kit p/ estado ativo
   discreto (--c-accent + semibold, o MESMO par de .sidebar__sublink--active) — sem cor nova, sem
   borda/caixa. Select de itens/página no canto DIREITO em toda resolução (order swap; .pagination
   já é space-between) e sem rótulo VISUAL via clip-pattern (segue no a11y tree — display:none o
   tiraria do leitor de tela; CSS-only, template do form intocado). */
.pagination { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-12); flex-wrap: wrap; }
.pagination__links { display: flex; align-items: center; gap: var(--sp-8); order: 1; min-height: var(--control-h); }   /* PASSO C3: altura implícita do pager (32px, herdada de .pagination__num) centralizava contra a altura EXPLÍCITA do select (--control-h, 52px) via align-items — funciona em teoria, mas depender de comparação implícita×explícita é frágil entre browsers. min-height iguala as duas linhas explicitamente; align-items:center passa a centralizar os 32px dentro de uma caixa de 52px garantida dos dois lados. */
.pagination__pager { display: flex; align-items: center; gap: var(--sp-4); }
.pagination__num { display: inline-grid; place-items: center; min-width: var(--sp-32); height: var(--sp-32); border-radius: var(--r-control); color: var(--c-gray); font-size: var(--fs-13); text-decoration: none; }
.pagination__num--active { color: var(--c-accent); font-weight: var(--fw-semibold); }
.pagination__ellipsis { color: var(--c-gray); font-size: var(--fs-13); }
.pagination__pager .material-symbols-outlined { font-size: var(--icon-md); }
.pagination > .field { order: 2; margin-left: auto; }   /* margin-left:auto: com página única a barra só tem o select — space-between não empurra item único pro canto direito; o auto-margin cobre os dois casos */
.pagination .field__label { position: absolute; width: var(--bw-thin); height: var(--bw-thin); overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 520px) {
  .pagination { padding-right: var(--sp-12); }   /* mesmo eixo direito da toolbar/trigger (X=363 em 375px, decretos topo-alinha-*): sem isto o select encostava na borda da tela (list-frame tem lateral 0 no mobile). Desktop/tablet não precisam — list-frame já tem 24px de padding lateral próprio (Passo 8F). */
}
@media (min-width: 521px) {
  /* PASSO C4: pager centralizado no eixo horizontal do envelope (opção b — select permanece à
     direita). Escopado a desktop/tablet — mobile mantém o flex+wrap original (linha 524) intacto,
     risco de overflow numa tela estreita com pager+select juntos não vale a pena pra um ganho
     puramente estético que só importa a partir daqui. Grid 3 colunas (1fr auto 1fr): a coluna do
     meio (auto) abraça o pager e fica geometricamente centralizada porque as duas colunas 1fr ao
     redor dividem o espaço restante em partes IGUAIS — funciona tanto com pager+select juntos
     quanto só um dos dois presente (_pagination.html tem guard próprio; se só o select renderiza,
     ele ainda cai na coluna 3 via grid-column explícito, preservando o alinhamento à direita do
     caso "só select" que o margin-left:auto tratava no flex). */
  .pagination { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }   /* align-items explícito: grid default é stretch, não center — sem isto perderia a centralização vertical do Passo C3 */
  .pagination__links { grid-column: 2; justify-self: center; }
  .pagination > .field { grid-column: 3; justify-self: end; margin-left: 0; }
}

/* ═════════ .list-frame (B8 — invólucro das lists; DESIGN-SPEC adenda S5 + fix-b8) ═════════ */
/* Destilado do section.card.page-card do legado (app.css:498-505 + assentamento medido em
   /produtos/): a DECISÃO do legado é o AGRUPAMENTO com ritmo próprio — toolbar+tabela+paginação
   DENTRO de um container ESTRUTURAL INVISÍVEL (flat: sem borda/raio/sombra/bg destacado); a
   list view FLUTUA no fundo e a única delimitação visual é a faixa do thead (B3).
   [S5-fix-b8, evidência do dono 2026-07-03: a versão original deste bloco vestia o container
   com a pele do card B7c ("consumação da intenção flat→card") — interpretação MINHA contra a
   fonte; F1 ratificou raio em COMPONENTES, não licenciou encaixotar a página. Revertida.]
   Ritmo destilado preservado: page-head(16) + toolbar mt 12 = 28 topo→toolbar (legado 29);
   toolbar→tabela 12; tabela→paginação 14; fundo 24. Conteúdo alinha com o page-head (o legado
   alinhava breadcrumb e conteúdo no mesmo inset de 24 do card — geometria equivalente).
   Escopo herdado do S01: .list-frame delimita onde a transformação mobile da tabela atua
   (core.css:1101 legado — "escopado p/ não atingir tabelas de form/detail/dashboard");
   no MOBILE os tr viram cards COM pele (C7b) — a pele é dos cards, não do container. */
.list-frame { display: flow-root; padding: 0 var(--sp-24) var(--sp-24); }   /* flow-root: BFC invisível (sem ele a margem do toolbar colapsa e o topo→toolbar cai de 28 p/ 16); inset lateral 24 = o do page-card legado (conteúdo 1052 @1100 — o inset, não a caixa, era o grosso do ganho de paridade) */
/* o legado alinhava o breadcrumb ao conteúdo do card com padding no PRÓPRIO header
   (.page-header--card, app.css:465-469) — mesmo mecanismo, portado: */
.page-head:has(~ .page-content .list-frame) { padding-inline: var(--sp-24); }
.list-frame > .toolbar { margin: var(--sp-12) 0; }          /* head(16)+12 = 28 topo→toolbar (legado) */
.list-frame > .bulk-bar { margin-bottom: var(--sp-12); }
.list-frame > .pagination { margin-top: var(--sp-14); }      /* tabela→paginação = 14 (legado) */
.list-frame > .alert { margin-top: var(--sp-12); }           /* empty-state respira dentro do container */
/* moldura-list-frame-restaurada (PASSO 8F, decreto Alli): "tabela flutua" revogada dentro de
   list-frame — .table-wrapper volta a herdar a regra base (linha 393: border+radius+background
   branco, mesmo idioma do wrapper-card de detail/form). Borda no wrapper (não na table, não em
   célula) é imune ao scroll-x por construção — não participa do conteúdo rolado. */
/* list-row-sombra-flutua: overflow-x:auto (base .table-wrapper) SEM overflow-y explícito faz
   o computed overflow-y virar 'auto' também (regra do spec: eixo "visible" some quando o outro
   não é visible) — o wrapper vira clip-container nos DOIS eixos. Com padding:0, a 1ª/última row
   ficam coladas na borda do wrapper e a sombra (offset 2 + blur 8 ≈ 10px) é cortada nas pontas.
   overflow-y:visible explícito NÃO resolve (o spec força auto de volta) — a saída é dar
   respiro vertical dentro do próprio wrapper pra sombra caber sem tocar a borda que clipa. */
.list-frame > .table-wrapper { padding-block: var(--sp-4); }

/* C7b — MOBILE ≤860 (breakpoint canônico C3; legado usava 900/820 — absorvido): a moldura
   zera (legado page-card mobile: borda/raio/sombra 0, lateral 0 — gutter único da página) e a
   tabela vira CARDS por linha: thead some, tr = card, td = grid [rótulo | valor], rótulo via
   data-label (padrão GENÉRICO do legado catalog.css:1295-1368 td::before attr(data-label),
   overline fs-11; components.js preenche data-label a partir do thead — template pode fixar o
   seu). O grid-card específico do produto_list legado (catalog_product_cards.css) foi portado
   na F1 (card-lista-f1-grid-areas, bloco ≤520px abaixo) — escopado a produtos, sem tocar o
   mecanismo genérico das demais listas. */
@media (max-width: 860px) {
  .list-frame { padding: 0 0 var(--sp-10); }   /* fix-b8: container já é flat; lateral 0 no mobile (gutter único, legado) */
  .page-head:has(~ .page-content .list-frame) { padding-inline: 0; }
  .stack[data-inventory-root] > .tabs { padding-inline: 0; }   /* estoque-tabs-alinhamento-list-frame: espelha o zero de .page-head acima — list-frame perde o inset 24 neste breakpoint, tabs acompanha */
  /* .table--stack (S2/#4): mecanismo ÚNICO de tabela→cards. As lists pegam via a classe no
     gabarito base_list; qualquer tabela embutida (detail/form/workspace/wizard) adota opt-in.
     data-label preenchido pelo components.js a partir do thead (ou fixo no template). */
  .table--stack { display: block; }
  .table--stack thead { display: none; }
  .table--stack tbody { display: flex; flex-direction: column; gap: var(--sp-12); }
  .table--stack tbody tr { display: block; padding: var(--sp-14); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-container); background: var(--c-white); box-shadow: var(--shadow-md); }
  .table--stack tbody td { display: grid; grid-template-columns: minmax(96px, 120px) minmax(0, 1fr); gap: var(--sp-10); align-items: start; padding: var(--sp-6) 0; border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
  .table--stack tbody tr > td:last-child { border-bottom: 0; }
  .table--stack tbody td::before { content: attr(data-label); font-size: var(--fs-11); font-weight: var(--fw-bold); letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--c-gray); }   /* rótulo = overline (T1) */
  .table--stack tbody td:not([data-label]) { grid-template-columns: minmax(0, 1fr); }
  .table--stack tbody td:not([data-label])::before, .table--stack tbody td[colspan]::before { content: none; }
  .table--stack tbody td.col-check { padding-top: 0; }
  .table--stack tbody td.ui-actions-col { display: flex; justify-content: flex-end; padding: 0; }
  .table--stack tbody td.ui-actions-col::before { content: none; }
  /* corrigir-sombra-scroll-tablet-lista-cards: compatibilizar table--cards com o colapso ≤860px —
     NÃO redesenhar o stack, só devolver o que .table--cards (especificidade maior) sobrescrevia. */
  .table--stack.table--cards tbody td { border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }   /* restaura o divisor rótulo:valor que .list-frame .table--cards tbody td (border-bottom:none) matava */
  .table--stack.table--cards tbody td:not(:last-child)::after { display: none; }   /* divisor vertical centralizado não faz sentido em td:display:grid empilhado */
  .list-frame .table-wrapper:has(.table--stack.table--cards) { padding-inline: 0; margin-inline: 0; }   /* respiro de sombra do scroll horizontal é irrelevante em modo empilhado (sem overflow-x); list-frame já vai a padding-lateral 0 aqui (linha 622) */
  /* Produtos mantém a seleção e a ação nos mesmos eixos também no tablet
     empilhado; ≤520 refina a mesma geometria para o card rico abaixo. */
  [data-bulk-table="catalog-products"] .table--stack thead { display: block; margin: 0 var(--sp-14) var(--sp-4); }
  [data-bulk-table="catalog-products"] .table--stack thead tr { display: flex; align-items: center; box-shadow: none; }
  [data-bulk-table="catalog-products"] .table--stack thead th { display: none; }
  [data-bulk-table="catalog-products"] .table--stack thead th.col-check { --product-card-inset: var(--sp-14); flex: 1; display: flex; align-items: center; gap: var(--sp-8); width: auto; min-height: calc(var(--btn-h) + var(--sp-4) * 2); padding: var(--sp-4) calc(var(--product-card-inset) + var(--bw-thin)); border: 0; background: transparent; }
  [data-bulk-table="catalog-products"] .table--stack thead th.col-check::after { content: "Selecionar todos"; order: 1; font-size: var(--fs-12); font-weight: var(--fw-regular); letter-spacing: normal; text-transform: none; color: var(--c-gray); white-space: nowrap; }
  [data-bulk-table="catalog-products"] .table--stack thead th.col-check .bulk-kebab { order: 2; margin-left: auto; }
  [data-bulk-table="catalog-products"] .table--stack thead th.col-check .bulk-kebab.is-active { display: inline-block; }
  [data-bulk-table="catalog-products"] .table--stack tbody td.ui-actions-col { width: 100%; }
  [data-bulk-table="catalog-products"] .bulk-bar, [data-bulk-table="catalog-products"] .bulk-bar.is-active { display: none; }
  @media (max-width: 520px) {
    .list-frame > .toolbar { margin-top: 0; }   /* decreto Alli (mobile-gap-pos-breadcrumb-uniforme): MEDIDO — o topo→toolbar calibrado do legado é 28 (page-head 16 + este mt 12, comentário acima), mas o critério desta rodada exige 16 uniforme em TODAS as rotas ≤520px; zera só o mt (mantém mb 12, toolbar→tabela inalterado). Tablet (521-860px) e desktop continuam com os 28 originais — só ≤520 muda. */
    .list-frame > .toolbar { padding-right: var(--sp-12); }   /* decreto Alli (toolbar-simetria-v3): REVERSÃO de e8f31ea (padding-inline simétrico, que deslocava o filtro) — o filtro fica intocado (caixa do botão em left:0, como sempre foi). X MEDIDO = 12px, mas não é padding nenhum: é o próprio ícone (--icon-md, 20px) centralizado dentro do botão quadrado (--btn-h, 44px) — inset natural (44-20)/2=12, que já coincide com var(--sp-12) (o gap do flex .toolbar, também intocado). Only padding-right acrescentado, igualando border-direita→busca a esse mesmo X — filtro→busca (gap do flex) e borda-esquerda→ícone (inerente ao botão) já eram 12 antes de qualquer mudança. */
    .toolbar__actions { margin-right: calc(-1 * var(--sp-12)); }   /* decreto Alli (toolbar-simetria-v4): MEDIDO tinta-a-tinta — glifo do filtro termina em 32 (caixa do botão até 44, inset próprio de 12), borda pintada do campo de busca (.field__wrap) começava em 56: gap real glifo→busca = 24 (os 12 do inset do ícone SOMADOS aos 12 do gap do flex .toolbar). Tentativa 1 (margin-left negativo no .toolbar__search) foi revertida: .toolbar__search tem justify-content:flex-end + filho flex:1 próprio (form/.field) — a margem negativa lá interagiu com esse flex aninhado e deslocou o campo 24px em vez de 12 (medido). margin-right negativo em .toolbar__actions evita essa armadilha: o filtro (primeiro filho, flex:0 0 auto, alinhado à esquerda por padrão) não se move — só o LIMITE DIREITO do container de ações avança pro espaço do gap, e o gap do flex some daí em diante. Resultado: glifo(32)→busca(44)=12, sem tocar no botão nem no search. */

    /* ── card-lista-f1-grid-areas: card rico da lista de produtos, porte do legado
       (crontex_from_prod_clean/catalog/static/catalog/css/catalog_product_cards.css:89-195,
       grid de áreas "check thumb head/sku/stats") via CSS-only sobre o HTML atual — escopado a
       [data-bulk-table="catalog-products"] (wrapper único de produto_list.html:31), zero efeito
       nas demais 22 listas que usam o .table--stack genérico acima. Mapeamento nth-child das
       colunas do refactor: 1=check · 2=Cod (oculto no card, como o legado ocultava id/cat/status)
       · 3=Imagem · 4=SKU · 5=Nome(+chips grade) · Preço/Estoque ancorados por nth-last-child
       (Preço é CONDICIONAL a can_view_financials no template — nth-child fixo quebraria; Ações é
       sempre a última, então nth-last-child(2)=Estoque e nth-last-child(3)=Preço valem nos dois
       casos) · última=.ui-actions-col (kebab sobreposto no canto do head, mesma técnica do
       legado: grid-area compartilhada + justify-self:end). Chips de grade: intocados nesta fase
       (F2 decide). "Selecionar todos": F3. ── */
    [data-bulk-table="catalog-products"] .table--stack tbody tr {
      --product-card-inset: var(--sp-12);
      --product-action-rail: var(--btn-h);
      display: grid;
      grid-template-columns: auto 80px max-content minmax(0, 1fr) var(--product-action-rail);   /* 80px = --pc-thumb-w do legado (literal lá também) */
      grid-template-rows: auto auto 1fr;
      grid-template-areas:
        "check thumb head  head   actions"
        "check thumb sku   sku    actions"
        "check thumb stats stats2 actions";
      column-gap: var(--sp-8);
      row-gap: var(--sp-4);
      align-items: start;
      padding-inline: var(--product-card-inset);
    }
    [data-bulk-table="catalog-products"] .table--stack tbody td { display: block; padding: 0; border: 0; min-width: 0; }
    [data-bulk-table="catalog-products"] .table--stack tbody td::before { content: none; }   /* mata os rótulos data-label (COD/IMAGEM/SKU/NOME) — o card do legado não rotula, só prefixa preço/estoque (abaixo) */
    [data-bulk-table="catalog-products"] .table--stack tbody td:nth-child(2) { display: none; }   /* Cod fora do card (legado: id/cat/status display:none) */
    [data-bulk-table="catalog-products"] .table--stack tbody td.col-check { grid-area: check; align-self: start; }
    [data-bulk-table="catalog-products"] .table--stack tbody td:nth-child(3) { grid-area: thumb; }
    [data-bulk-table="catalog-products"] .table--stack tbody td:nth-child(3) .thumb { width: 80px; height: auto; aspect-ratio: 3 / 4; object-fit: cover; border-radius: var(--r-sm); display: block; }   /* legado: thumb 80, 3/4, raio sm */
    [data-bulk-table="catalog-products"] .table--stack tbody td:nth-child(4) { grid-area: sku; font-size: var(--fs-12); color: var(--c-gray); }
    [data-bulk-table="catalog-products"] .table--stack tbody td:nth-child(5) { grid-area: head; }
    [data-bulk-table="catalog-products"] .table--stack tbody td:nth-child(5) .link { font-weight: var(--fw-semibold); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }   /* nome 2 linhas + ellipsis (legado --pc-name-lines) */
    [data-bulk-table="catalog-products"] .table--stack tbody td:nth-child(6) { display: none; }   /* [escopo:colunas-limpas-truncamento-listas] nova coluna Variantes (chips de grade, saíram do td Nome) — card mobile de produtos é um grid de áreas fixo (check/thumb/head/sku/stats/stats2/actions, decreto card-lista-f1-grid-areas acima), sem área livre pros chips; ocultada aqui em vez de forçada num grid já calibrado — mobile de produtos é divergência PENDENTE e separada (reportada em átomo anterior), não redesenhada por este átomo. Nome(5)/Preço·Estoque(nth-last-child)/Ações continuam intocados: Variantes entra DEPOIS de Nome, nada desloca. */
    [data-bulk-table="catalog-products"] .table--stack tbody td.cell--num:nth-last-child(3) { grid-area: stats; align-self: end; justify-self: start; white-space: nowrap; font-weight: var(--fw-semibold); }
    [data-bulk-table="catalog-products"] .table--stack tbody td.cell--num:nth-last-child(3)::before { content: "R$ "; font: inherit; color: inherit; letter-spacing: normal; text-transform: none; }   /* resets: o ::before genérico do stack (overline fs-11 uppercase gray) ainda casa este ::before — sem eles "R$" viraria overline */
    [data-bulk-table="catalog-products"] .table--stack tbody td.cell--num:nth-last-child(2) { grid-area: stats2; align-self: end; justify-self: start; white-space: nowrap; font-size: var(--fs-12); color: var(--c-gray); }
    [data-bulk-table="catalog-products"] .table--stack tbody td.cell--num:nth-last-child(2)::before { content: "Estq. "; font: inherit; color: inherit; letter-spacing: normal; text-transform: none; }
    [data-bulk-table="catalog-products"] .table--stack tbody td.ui-actions-col { display: block; grid-area: actions; width: var(--product-action-rail); justify-self: end; align-self: start; position: relative; z-index: 1; padding: 0; }

    /* ── card-lista-f3-selecionar-todos: thead volta como BARRA compacta (legado
       catalog_product_cards.css:48-78 — ths somem, só o col-check com rótulo via ::after) +
       kebab de bulk na extremidade direita (eixo X=363: padding-right sp-12 + trigger 44).
       Rótulo: par --fs-12/--c-gray (o mesmo do SKU do card, papel meta) com resets contra o
       idioma overline do th base (uppercase/ls-wide). Kebab entra SÓ com seleção (.is-active,
       ligado pelo motor bulk de components.js via [data-bulk-bar]); a _bulk_bar clássica sai
       de cena neste breakpoint pra não duplicar as mesmas 3 ações. ── */
    [data-bulk-table="catalog-products"] .table--stack thead { display: block; margin: 0 0 var(--sp-4); }
    [data-bulk-table="catalog-products"] .table--stack thead tr { display: flex; align-items: center; box-shadow: none; }
    [data-bulk-table="catalog-products"] .table--stack thead th { display: none; }
    [data-bulk-table="catalog-products"] .table--stack thead th.col-check { --product-card-inset: var(--sp-12); flex: 1; display: flex; align-items: center; gap: var(--sp-8); width: auto; min-height: calc(var(--btn-h) + var(--sp-4) * 2); padding: var(--sp-4) calc(var(--product-card-inset) + var(--bw-thin)); border: 0; background: transparent; }   /* mesmo inset do card + sua hairline: check-all e check de linha compartilham um único eixo; o rail de ações termina no mesmo eixo direito */
    [data-bulk-table="catalog-products"] .table--stack thead th.col-check::after { content: "Selecionar todos"; order: 1; font-size: var(--fs-12); font-weight: var(--fw-regular); letter-spacing: normal; text-transform: none; color: var(--c-gray); white-space: nowrap; }
    [data-bulk-table="catalog-products"] .table--stack thead th.col-check .bulk-kebab { order: 2; margin-left: auto; }
    [data-bulk-table="catalog-products"] .table--stack thead th.col-check .bulk-kebab.is-active { display: inline-block; }
    [data-bulk-table="catalog-products"] .bulk-bar, [data-bulk-table="catalog-products"] .bulk-bar.is-active { display: none; }

    /* Estoque (inventory) tabs — mesmo tratamento mobile-card de Produtos (linhas 639-646), escopo dinâmico por aba */
    [data-bulk-table^="inventory-"] .table--stack thead { display: block; margin-bottom: var(--sp-4); }
    [data-bulk-table^="inventory-"] .table--stack thead tr { display: flex; align-items: center; box-shadow: none; }
    [data-bulk-table^="inventory-"] .table--stack thead th { display: none; }
    [data-bulk-table^="inventory-"] .table--stack thead th.col-check { flex: 1; display: flex; align-items: center; gap: var(--sp-8); width: auto; min-height: calc(var(--btn-h) + var(--sp-4) * 2); padding: var(--sp-4) var(--sp-12) var(--sp-4) var(--sp-16); border: 0; background: transparent; }
    [data-bulk-table^="inventory-"] .table--stack thead th.col-check::after { content: "Selecionar todos"; order: 1; font-size: var(--fs-12); font-weight: var(--fw-regular); letter-spacing: normal; text-transform: none; color: var(--c-gray); white-space: nowrap; }
    [data-bulk-table^="inventory-"] .table--stack thead th.col-check .bulk-kebab { order: 2; margin-left: auto; }
    [data-bulk-table^="inventory-"] .table--stack thead th.col-check .bulk-kebab.is-active { display: inline-block; }
    [data-bulk-table^="inventory-"] .bulk-bar, [data-bulk-table^="inventory-"] .bulk-bar.is-active { display: none; }
  }
}

/* ═════════ .stepper (indicador de passos — wizard; global/genérico) ═════════ */
.stepper { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-8); }
.stepper__step { display: inline-flex; align-items: center; justify-content: center; min-width: var(--btn-h); height: var(--btn-h); padding: 0 var(--sp-10); border-radius: var(--r-pill); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); background: var(--c-white); color: var(--c-gray); text-decoration: none; transition: background var(--dur-fast) var(--ease-base); }   /* fs+fw: role action */
.stepper__step:hover { background: var(--c-surface-hover); }   /* S4/A14 */
.stepper__step:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: var(--ring-offset); }
.stepper__step--active { background: var(--c-accent); color: var(--c-white); border-color: var(--c-accent); }
/* estados do wizard (ADR-0006 — o stepper canônico é server-rendered; estes modificadores
   espelham steps_meta[].state: done|error|warning|running; pending = o default acima) */
.stepper__step--done { background: var(--c-success-bg); color: var(--c-success); border-color: var(--c-success); }
.stepper__step--error { background: color-mix(in srgb, var(--c-danger) 10%, var(--c-white)); color: var(--c-danger); border-color: var(--c-danger); }
.stepper__step--warning { background: var(--c-warn-bg); color: var(--c-warn); border-color: var(--c-warn); }   /* S4/A8: familia warn completa (yellow = so dot/icone) */
.stepper__step--running { background: color-mix(in srgb, var(--c-info) 10%, var(--c-white)); color: var(--c-info); border-color: var(--c-info); }
.stepper__item { display: inline-flex; align-items: center; gap: var(--sp-6); }
.stepper__label { color: var(--c-gray); }   /* fs+fw: role subtext */
.stepper__connector { width: var(--sp-16); height: var(--bw-thin); background: var(--c-border); flex: none; }

/* ═════════ .ref-tree (árvore de seleção remota — grupos tri-estado + itens; ADR-0006) ═════════ */
/* Molécula do contrato "seleção de referências": grupo = <details data-*-group> com checkbox
   tri-estado (data-indeterminate="1" reaplicado por JS pós-inject — indeterminate não serializa);
   item = <label> com checkbox + badge de método + título + path. Consumidor: integrations/bling. */
.ref-tree { display: flex; flex-direction: column; gap: var(--sp-4); }
.ref-tree__group { border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-container); background: var(--c-white); }
.ref-tree__group > summary { display: flex; align-items: center; gap: var(--sp-8); padding: var(--sp-8) var(--sp-10); list-style: none; cursor: pointer; }
.ref-tree__group > summary::-webkit-details-marker { display: none; }
.ref-tree__group > summary::marker { content: ""; }
.ref-tree__chevron { font-size: var(--icon-sm); color: var(--c-gray); transition: transform var(--dur-fast) var(--ease-base); flex: none; }
.ref-tree__group[open] > .ref-tree__chevron, .ref-tree__group[open] > summary .ref-tree__chevron { transform: rotate(90deg); }
.ref-tree__group-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--fw-semibold); }
.ref-tree__check { display: inline-flex; align-items: center; gap: var(--sp-4); flex: none; }
.ref-tree__body { padding: var(--sp-4) var(--sp-10) var(--sp-8) calc(var(--sp-10) + var(--icon-sm) + var(--sp-8)); display: flex; flex-direction: column; gap: var(--sp-4); }
.ref-tree__group--nested { border: none; background: transparent; }
.ref-tree__item { display: flex; align-items: center; gap: var(--sp-8); padding: var(--sp-4) var(--sp-6); border-radius: var(--r-sm); cursor: pointer; }
.ref-tree__item:hover { background: var(--c-surface-hover); }   /* S4b/A14: hover neutro usa o token de hover */
.ref-tree__item.is-saving { opacity: var(--op-disabled); pointer-events: none; }
.ref-tree__path { color: var(--c-gray); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }   /* fs: role subtext */

/* ═════════ .formset (inline-formset genérico — catalog + people) ═════════ */
.formset { display: flex; flex-direction: column; gap: var(--sp-12); min-width: 0; }   /* min-width:0 (BOM-tabela-v2): como filho de card flex, sem isto o formset cresce com a tabela e o .table-wrapper nunca contém o scroll-x (estouro do card) */
.formset__rows { display: flex; flex-direction: column; gap: var(--sp-8); }
.formset__row { display: flex; align-items: flex-start; gap: var(--sp-12); padding: var(--sp-12); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-container); background: var(--c-white); }
.formset__row--deleted { opacity: var(--op-disabled); }
.formset__fields { flex: 1; min-width: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(192px, 1fr)); gap: var(--sp-12); }   /* C5: era 12rem — px é a convenção do projeto */
.formset__cell { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.formset__row-actions { flex: none; display: flex; align-items: center; gap: var(--sp-8); }
.formset__del { display: inline-flex; align-items: center; gap: var(--sp-6); }   /* tipografia: role caption */
.formset__foot { display: flex; }
.formset__foot--table { justify-content: flex-end; }   /* posição do legado: core.css:241 .section-actions flex-end (add de material, canto direito do rodapé) */
.contact-list__type-badge { display: none; }   /* card-mobile-contatos-design-humano: badge só existe no card mobile (linha do nome); desktop mantém .contact-list__kind como coluna própria, intocada */
.contact-list__kind-icon { flex: 0 0 auto; margin-right: var(--sp-4); color: var(--c-gray); font-size: var(--icon-sm); vertical-align: middle; }
.contact-list__thumb-head { white-space: nowrap; text-align: center; }
@media (max-width: 640px) {
  /* card-mobile-contatos-design-humano: substitui por completo o card mobile anterior
     (a7ac6c3, "anatomia thumbnail+conteúdo+action rail") — aquele bloco usava
     .contact-list__avatar/grid-area thumb/head/meta/email/phone/actions, mas o markup real
     (contact_list.html + #ks-contacts-real) usa .contact-list__thumb/__thumb-image e nunca
     teve elemento com classe .contact-list__avatar — fiação morta desde o commit, nunca
     aplicou. people.css:43-140 tinha um 3º sistema paralelo (classes corretas, arquivo nunca
     carregado nesta lista) — também removido, consolidado aqui como owner único. Design
     aprovado (Rota C): checkbox+kebab na mesma linha do topo; avatar centralizado (único
     elemento centralizado do card — decisão humana explícita em
     alinhar-coluna-icones-card-contatos: "justifica tudo a esquerda, exceto o avatar");
     nome+badge de tipo na mesma linha, alinhados à esquerda (linha "Tipo" separada não existe
     mais no mobile, plain-text preservado no desktop); sem hairlines internas (elevação do
     card já delimita); ícones de TODAS as linhas de valor (nome, email, telefone,
     subcategorias) à ESQUERDA, antes do texto, ordem natural do DOM, mesma coluna x=13px —
     correção de ajustar-icones-valores-esquerda-card-contatos (a direção "à direita" do
     fe1c91f foi especificação mal expressa) seguida de alinhar-coluna-icones-card-contatos
     (a linha do nome era centralizada como grupo — .contact-list__identity{justify-
     content:center} —, então seu ícone deslocava por contato conforme o tamanho do nome;
     descentralizada para bater na mesma coluna dos demais). Só o kebab do topo fica à
     direita. Polimento óptico (polimento-optico-card-contatos): os 4 ícones (nome/email/
     telefone/categoria) tinham caixas distintas (nome e categoria em 24px, herdado sem
     font-size próprio; email/telefone em 18px/--icon-sm) — normalizados todos para
     --icon-sm (18px, mesmo padrão de contact_detail.css), fechando o alinhamento óptico
     além do geométrico. Gap ícone→texto das linhas de valor subiu de --sp-4 para --sp-6
     (próximo token da escala), igualando o gap que a linha do nome já usava. Respiro
     vertical entre email/telefone/subcategorias: +var(--sp-2) de margin-top em phone/
     categories, então o gap efetivo entre linhas de valor sobe de 8px (--sp-8, gap do grid,
     inalterado para check/avatar/identity) para 10px (--sp-8 + --sp-2 = --sp-10) só nesse
     trecho — hairlines continuam zero. Cor dos valores (email/telefone/categorias) já era
     --c-gray desde antes deste átomo (confirmado por medição, nenhuma mudança necessária);
     nome mantém a cor de destaque (--c-ink), não é "valor". Simetria vertical
     (simetria-vertical-card-contatos): medido topo→avatar=65px vs último-elemento→bottom=13px
     (diferença de 52px = altura da linha check/kebab, 44px do --btn-h do kebab, + gap de
     --sp-8) — a linha "check kebab" ocupava uma row própria no grid, empurrando o resto do
     conteúdo para baixo. Fix: check/kebab saem do fluxo do grid (position:absolute, ancorados
     em top/left/right:var(--sp-12) sobre .contact-list__row{position:relative}) e o
     grid-template-areas passa a começar direto em "avatar" — topo→avatar agora usa só o
     padding do card (13px = --sp-12 + borda), igual ao último-elemento→bottom (13px). Visual:
     checkbox e kebab passam a ficar na mesma linha horizontal entre si (antes o checkbox
     ficava centralizado numa row 44px mais alta que ele, agora ambos no mesmo top). Sem
     colisão com o avatar (checkbox/kebab nas bordas, avatar centralizado). REGRESSÃO
     encontrada em restaurar-checkbox-e-respiro-card-contatos: o checkbox ficava
     completamente oculto (invisível, mas presente/clicável só no centro exato por coincidência
     de stacking) — causa raiz: .list-frame .table--cards tbody td (components.css:512) dá
     background:var(--c-white) OPACO a todo <td>, inclusive .contact-list__thumb, que como
     grid-item de largura 1fr cobre a linha inteira (não só o círculo visível); grid items
     participam do stacking por z-index mesmo com position:static (quirk do spec de Grid), e
     como .contact-list__thumb vem DEPOIS de .col-check no DOM, pinta por cima — .ui-actions-col
     (kebab) escapou por vir DEPOIS de .contact-list__thumb no DOM. Fix: reset explícito
     background:none no `> td` (below) neutraliza o opaco herdado; z-index:1 em .col-check e
     .ui-actions-col garante a prioridade de pintura independente de ordem no DOM. Respiro:
     padding do card sobe de --sp-12 (12px) para --sp-16 (16px, próximo degrau da escala,
     candidato indicado pelo humano) nos 4 lados — topo→avatar e último→bottom sobem juntos de
     13px para 17px (13+4), mantendo a igualdade exata; âncoras de .col-check/.ui-actions-col
     acompanham para var(--sp-16), coerentes com o novo padding. Alinhamentos finais
     (alinhar-kebab-checkbox-card-contatos): (1) desnível vertical checkbox×kebab — ambos
     ancorados no mesmo top:var(--sp-16), mas .col-check (TD) tinha height:24px por um gap
     fantasma de line-height/baseline ao redor do <input> inline-grid de 20px (quirk clássico
     de inline-block em contexto de linha), enquanto .ui-actions-col tinha 44px (var(--btn-h)
     do trigger) — centros diferiam ~8-12px. Fix: .col-check ganha width/height:var(--btn-h) +
     display:flex;align-items:center;justify-content:center — mesma caixa 44×44 do kebab,
     mesma âncora top, centros batem por construção (não por cálculo manual de offset); o
     <input> em si continua 20×20 (hit-area do checkbox intocada), só a caixa-invólucro
     (não-interativa) passa a espelhar a do kebab. (2) lateral do kebab do card × kebab do
     page-head ("Contatos ⋮", shell.css:207-227): medido offset-right do page-head = 12px;
     right:var(--sp-16) do card dava 18px — trocado para right:var(--sp-10), fecha em 12px
     exato (medido, não calculado a priori — a relação entre o token CSS e o offset visual
     final inclui a borda do card, por isso a verificação empírica). Cria a coluna óptica
     vertical entre os dois kebabs da tela. (3) tipografia: medida e confirmada IDÊNTICA ao
     aprovado em 83a10f9 (nome --c-ink/--fs-14/600, valores --c-gray/--fs-12/400, ícones
     18px) — nenhuma divergência encontrada, nenhuma mudança de código necessária.
     CORREÇÃO pós-759a612 (regressão pega pelo humano): o width/height:var(--btn-h) +
     justify-content:center do .col-check (para igualar altura do kebab) empurrou o checkbox
     12.5px à direita do eixo dos ícones (centralizado numa caixa 44px em vez de flush-left).
     Troca de abordagem: em vez de flex-centering, o top é calculado diretamente
     (calc(var(--sp-16) + var(--sp-12)) — kebab center relativo é --sp-16+22(metade de 44); 20px
     de checkbox centra nesse eixo com top=--sp-16+12) e left fica só var(--sp-16), sem largura
     nem flex — o checkbox aterrissa exatamente na mesma coluna dos ícones (--sp-16, igual ao
     padding do card) e no mesmo centro vertical do kebab, os dois por construção geométrica,
     não por centering automático. Duas causas de resíduo de 1px encontradas e neutralizadas:
     (a) line-height:0 no .col-check mata o gap fantasma de baseline ao redor do <input>
     inline-grid (mesmo quirk do dde3518, reintroduzido pela mudança de abordagem); (b)
     border-left:0 explícito no .col-check neutraliza .list-frame .table--cards tbody
     td:first-child (components.css:513, regra do padrão desktop) que empatava em
     especificidade com o reset genérico do `> td` e vencia por ordem de código, dando 1px de
     borda esquerda ao checkbox (1º td da linha) que os ícones não tinham. */
  .table--stack tbody tr.contact-list__row {
    position: relative;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-areas: "avatar" "identity" "email" "phone" "categories";
    gap: var(--sp-8);
    padding: var(--sp-16);
    padding-left: calc(var(--sp-16) - 5px);
    border: var(--bw-thin) var(--bs-solid) var(--c-border);
    border-radius: var(--r-container);
    background: var(--c-white);
    box-shadow: none;
  }
  .table--stack tbody tr.contact-list__row > td { display: block; min-width: 0; padding: 0; border: 0; background: none; grid-template-columns: none; }
  .table--stack tbody tr.contact-list__row > td::before { content: none; }
  .table--stack tbody tr.contact-list__row .col-check { position: absolute; top: calc(var(--sp-16) + var(--sp-12)); left: calc(var(--sp-16) - 5px); z-index: 1; line-height: 0; border-left: 0; }
  .table--stack tbody tr.contact-list__row .ui-actions-col { position: absolute; top: var(--sp-16); right: var(--sp-10); width: auto; display: block; z-index: 1; }
  .table--stack tbody tr.contact-list__row .ui-actions-col .ui-actions-menu__trigger .material-symbols-outlined { position: relative; left: 14.5px; }
  .table--stack tbody tr.contact-list__row .ui-actions-menu__trigger { width: var(--btn-h); height: var(--btn-h); }
  .table--stack tbody tr.contact-list__row .contact-list__thumb { grid-area: avatar; display: flex; justify-content: center; }
  .table--stack tbody tr.contact-list__row .contact-list__thumb-image { width: 5rem; height: 5rem; border-radius: var(--r-circle); object-fit: cover; background: var(--c-surface-sunken); }
  .table--stack tbody tr.contact-list__row .contact-list__identity { grid-area: identity; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: var(--sp-6); text-align: left; }
  .table--stack tbody tr.contact-list__row .contact-list__identity .link { font-weight: var(--fw-semibold); }
  .table--stack tbody tr.contact-list__row .contact-list__identity-icon { font-size: var(--icon-sm); }
  .table--stack tbody tr.contact-list__row .contact-list__type-badge { display: inline-flex; }
  .table--stack tbody tr.contact-list__row .contact-list__kind { display: none; }   /* substituída por .contact-list__type-badge na linha do nome — coluna de desktop intocada (>640px) */
  .table--stack tbody tr.contact-list__row .contact-list__email,
  .table--stack tbody tr.contact-list__row .contact-list__phone { display: flex; align-items: center; gap: var(--sp-6); min-width: 0; color: var(--c-gray); font-size: var(--fs-12); }
  .table--stack tbody tr.contact-list__row .contact-list__email { grid-area: email; }
  .table--stack tbody tr.contact-list__row .contact-list__phone { grid-area: phone; margin-top: var(--sp-2); }
  .table--stack tbody tr.contact-list__row .contact-list__email .material-symbols-outlined,
  .table--stack tbody tr.contact-list__row .contact-list__phone .material-symbols-outlined { flex: 0 0 auto; font-size: var(--icon-sm); }
  .table--stack tbody tr.contact-list__row .contact-list__categories { grid-area: categories; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-6); color: var(--c-gray); font-size: var(--fs-12); margin-top: var(--sp-2); }
  .table--stack tbody tr.contact-list__row .contact-list__categories-icon { font-size: var(--icon-sm); }
  .table--stack tbody tr.contact-list__row .contact-list__status { display: none; }
  .formset__row { flex-direction: column; }
  .formset__fields { width: 100%; grid-template-columns: minmax(0, 1fr); }
  .formset__row-actions { align-self: flex-end; }
}

/* ═════════ Projeção compacta de variantes (Produto + Produção) ═════════ */
.variant-swatch { --variant-color: var(--c-surface-sunken); display: inline-block; width: var(--sp-14); height: var(--sp-14); flex: 0 0 var(--sp-14); border: var(--bw-thin) var(--bs-solid) color-mix(in srgb, var(--c-ink) 22%, var(--c-white)); border-radius: var(--r-sm); background: var(--variant-color); box-shadow: inset 0 0 0 var(--bw-thin) color-mix(in srgb, var(--c-white) 38%, transparent); }
.variant-swatch--fallback { background: var(--c-surface-sunken); }
.variant-size-badge { display: inline-flex; align-items: center; justify-content: center; min-width: var(--sp-20); height: var(--sp-14); padding-inline: var(--sp-4); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-sm); background: var(--c-surface-subtle); color: var(--c-gray); font-size: var(--fs-11); font-weight: var(--fw-medium); line-height: 1; }
.variant-inline { display: inline-flex; align-items: center; gap: var(--sp-6); }

/* ═════════ .selectlist (combobox: filtro client-side + create inline; catalog + people) ═════════ */
.selectlist-native-hidden { display: none !important; } /* important-ok: esconde o <select> nativo enquanto o JS decora */
details.selectlist { position: relative; min-width: 14rem; }
details.selectlist > summary { list-style: none; }
.selectlist-summary { display: flex; align-items: center; gap: var(--sp-10); height: var(--control-h); padding: 0 var(--sp-14); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-control); background: var(--c-white); cursor: pointer; user-select: none; }
.selectlist-summary::-webkit-details-marker { display: none; }
.selectlist-summary::marker { content: ""; }
.selectlist-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--c-ink); }
.selectlist-value.is-placeholder { color: var(--c-text-placeholder); }   /* S4b/A24: placeholder tem token proprio */
.selectlist-icon { flex: none; color: var(--c-gray); font-size: var(--icon-md); }
.selectlist-chevron { color: var(--c-gray); font-size: var(--icon-md); }   /* S4/T1 */
details[open].selectlist .selectlist-summary,
.selectlist-summary:focus-visible { border-color: color-mix(in srgb, var(--c-accent) 38%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent); }   /* kit-focus-legado: glow (:focus-visible cobre o tab sem abrir) */
.selectlist-summary:focus-visible { outline: none; }   /* produtos-focus-fix-v2: suprime o ring nativo do <summary> — glow acima é o indicador */
.selectlist-pop { position: absolute; top: calc(100% + var(--sp-8)); left: 0; right: 0; z-index: var(--z-popover); padding: var(--sp-10); background: var(--c-white); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-pop); box-shadow: var(--shadow-lg); }
.selectlist-top { display: flex; flex-direction: column; gap: var(--sp-8); padding-bottom: var(--sp-10); margin-bottom: var(--sp-10); border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
.selectlist-search { width: 100%; height: var(--btn-h); padding: 0 var(--sp-12); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-control); }   /* S4b/A18: input = borda-que-aparece · tipografia: role control */
.selectlist-search:focus-visible { border-color: var(--c-accent); outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: 0; }   /* S4b/A26: foco de teclado visivel */
.selectlist-add { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-8); height: var(--btn-h); border: var(--bw-thin) var(--bs-dashed) var(--c-border-strong); border-radius: var(--r-control); background: transparent; color: var(--c-ink); font-weight: var(--fw-bold); cursor: pointer; }
.selectlist-add[disabled], .selectlist-add[aria-disabled="true"] { opacity: var(--op-disabled); cursor: not-allowed; }
.selectlist-options { max-height: 16rem; overflow: auto; display: flex; flex-direction: column; gap: var(--sp-6); }
.selectlist-option { display: flex; align-items: center; gap: var(--sp-10); padding: var(--sp-8) var(--sp-10); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-control); cursor: pointer; }
.selectlist-option:hover { background: var(--c-surface-hover); }   /* S4/A14 */
.selectlist-option.is-hidden { display: none; }
.selectlist-option input[type="checkbox"] { width: var(--sp-16); height: var(--sp-16); accent-color: var(--c-cta); }
.selectlist-option span { font-size: var(--fs-14); }

/* ═════════ .combo (people-picker: typeahead servidor + create-link; catalog + people) ═════════ */
/* Família própria (markup .combo-* gerado pelo people_combo.js) — irmã de .selectlist; mesma linguagem/tokens. */
.combo { position: relative; display: flex; align-items: stretch; width: 100%; }
/* v7 (re-canonização): o combo veste o MESMO chrome do controle canônico (.field__control:
   altura --control-h, padding sp-14, borda única --r-control) — morre o dialeto split-button
   (input fino r-sm + célula de seta destacada). A seta vira interna, como a do select nativo. */
/* v8: casa TAMBÉM o input server-rendered (class="input") — no modo 1 do people_combo.js
   (wrapper .combo já no markup, produto_form) a classe .combo-input NUNCA é adicionada
   (people_combo.js:95 busca input.input; :103 só roda no modo 2) — era a causa da caixa curta. */
.combo > .combo-input, .combo > input.input { flex: 1 1 auto; width: 100%; min-width: 0; height: var(--control-h); padding: var(--sp-14); padding-right: calc(var(--btn-h) + var(--sp-4)); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-control); background: var(--c-white); color: var(--c-ink); font-size: var(--fs-14); transition: border-color var(--dur-fast) var(--ease-base); }
.combo-toggle { position: absolute; right: 0; top: 0; height: 100%; width: var(--btn-h); border: 0; background: transparent; cursor: pointer; }
/* seta = MESMO chevron material do selectlist (.selectlist-chevron) — pele única de "select do
   kit" p/ os dois motores; o motor de pessoa (people_combo.js, busca-servidor + hidden _id)
   permanece — selectlist.js não cobre esse contrato (client-side, sem _id). */
.combo-toggle::after { content: "expand_more"; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: 'Material Symbols Outlined'; font-size: var(--icon-md); color: var(--c-gray); }
.combo:focus-within > .combo-input, .combo:focus-within > input.input { border-color: color-mix(in srgb, var(--c-accent) 38%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent); }   /* kit-focus-legado: glow */
.combo-input:focus-visible, .combo > input.input:focus-visible { outline: none; }   /* produtos-focus-fix-v2: suprime o ring nativo do input real — glow da regra acima é o indicador */
.combo-panel { position: absolute; z-index: var(--z-popover); top: calc(100% + var(--sp-4)); left: 0; right: 0; padding: var(--sp-6); background: var(--c-white); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-pop); box-shadow: var(--shadow-lg); opacity: 0; transform: translateY(calc(var(--sp-4) * -1)); pointer-events: none; transition: opacity var(--dur-fast) var(--ease-base), transform var(--dur-fast) var(--ease-base); }
.combo.open .combo-panel { opacity: 1; transform: translateY(0); pointer-events: auto; }
.combo-panel[hidden] { display: none; }
.combo-search { width: 100%; margin-bottom: var(--sp-6); padding: var(--sp-8) var(--sp-10); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-sm); }   /* tipografia: role control */
.combo-search:focus-visible { border-color: var(--c-accent); outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: 0; }   /* S4b/A26 */
.combo-list { max-height: 14rem; overflow: auto; margin: 0; padding: 0; list-style: none; border-top: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
.combo-option { padding: var(--sp-6) var(--sp-8); border-radius: var(--r-sm); cursor: pointer; }   /* tipografia: role body */
.combo-option[aria-selected="true"], .combo-option:hover { background: var(--c-accent-soft); }   /* S4/A7: selecao/hover = interacao (accent), nao estado */
.combo-empty { padding: var(--sp-8); }   /* tipografia: role subtext */
.combo-create { margin-top: var(--sp-6); padding-top: var(--sp-6); border-top: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); }
.combo-create-link { display: block; padding: var(--sp-6) var(--sp-8); border-radius: var(--r-sm); font-size: var(--fs-13); color: var(--c-ink); text-decoration: none; }
.combo-create-link:hover { background: var(--c-surface-hover); }

/* ═════════ .grades (editor das duas dimensões de Variantes do Produto) ═════════ */
.grades { min-width: 0; display: flex; flex-direction: column; gap: var(--sp-16); }
.grades__dimension { display: flex; flex-direction: column; gap: var(--sp-10); padding: var(--sp-14); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-container); background: var(--c-white); }
.grades__dimension.is-invalid { border-color: var(--c-danger); }
.grades__dimension-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-12); }
.grades__dimension-head h3 { margin: 0; font-size: var(--fs-14); }
.grades__required { flex: none; font-size: var(--fs-11); color: var(--c-gray); }
.grades__presets, .grades__selected { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-6); }
.grades__selected { min-height: var(--sp-20); }
.grades__preset, .grades__preset--color { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-6); min-height: var(--btn-h-sm); padding-inline: var(--sp-10); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-control); background: var(--c-white); color: var(--c-ink); cursor: pointer; }
.grades__preset:hover { background: var(--c-surface-hover); }
.grades__preset.is-active { border-color: var(--c-accent); background: var(--c-accent-soft); color: var(--c-accent); }
.grades__preset:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: var(--ring-offset); }
.grades__preset-swatch, .grades__chip-swatch { --grade-color: var(--c-surface-sunken); width: var(--sp-14); height: var(--sp-14); flex: 0 0 var(--sp-14); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-sm); background: var(--grade-color); }
.grades__color--fallback { background: var(--c-surface-sunken); }
.grades__chip { display: inline-flex; align-items: center; gap: var(--sp-6); min-height: var(--sp-24); padding: 0 var(--sp-6) 0 var(--sp-8); border-radius: var(--r-sm); font-size: var(--fs-12); line-height: var(--lh-tight); color: var(--c-ink); background: var(--c-surface-sunken); border: var(--bw-thin) var(--bs-solid) var(--c-border); }
.grades__chip-x { appearance: none; display: inline-grid; place-items: center; width: var(--sp-20); height: var(--sp-20); border: 0; background: transparent; cursor: pointer; color: var(--c-gray); font-size: var(--fs-14); line-height: 1; padding: 0; }
.grades__chip-x:hover { color: var(--c-ink); }
.grades__chip-x:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: 0; border-radius: var(--r-sm); }
.grades__custom { display: grid; grid-template-columns: minmax(12rem, 1fr) auto; gap: var(--sp-8); align-items: center; }
.grades__custom--color { grid-template-columns: minmax(12rem, 1fr) var(--btn-h) auto; }
.grades__color-input { width: var(--btn-h); height: var(--btn-h); padding: var(--sp-4); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-control); background: var(--c-white); cursor: pointer; }
.grades__preview { color: var(--c-gray); }
.grades__preview .table { color: var(--c-ink); }
.grades__rows { display: flex; flex-direction: column; gap: var(--sp-8); }
.grades__row { display: grid; grid-template-columns: minmax(10rem, 14rem) minmax(0, 1fr) auto; align-items: flex-start; gap: var(--sp-10); }
.grades__key { height: var(--btn-h); padding: 0 var(--sp-10); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-sm); min-width: 0; }
.grades__tokens { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-6); min-height: var(--btn-h); padding: var(--sp-6) var(--sp-10); border: var(--bw-thin) var(--bs-solid) var(--c-border-strong); border-radius: var(--r-sm); background: var(--c-white); }
.grades__editor { flex: 1 1 5rem; min-width: 0; height: var(--btn-h-sm); padding: 0 var(--sp-4); border: 0; background: transparent; color: var(--c-ink); }
.grades__editor:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: 0; border-radius: var(--r-sm); }
@media (max-width: 640px) {
  .grades__custom, .grades__custom--color { grid-template-columns: minmax(0, 1fr); }
  .grades__row { grid-template-columns: minmax(0, 1fr); }
  .grades__color-input { width: 100%; }
  .grades__custom .btn { width: 100%; }
}

/* célula numérica (saldo/ledger) — alinhamento à direita com numerais tabulares */
.table .cell--num, th.cell--num { text-align: right; font-variant-numeric: tabular-nums; }
/* [escopo:colunas-limpas-truncamento-listas] célula com dado único e truncamento com
   reticências — mecanismo canônico reaproveitável por qualquer coluna de texto de qualquer
   lista (não amarrado a nth-child/posição, aplicado via classe no <td> do template). Escopado
   a min-width:641px (fora do .table--stack) — nowrap/ellipsis não deve vazar pro card mobile
   empilhado, onde o mesmo <td> vira grid label:value e cada padrão de card (ex.: contatos Rota
   C, produtos card rico) já resolve sua própria tipografia. max-width:0 + overflow:hidden no
   <td>, combinado a width:100% no filho direto: sinaliza ao auto-layout "esta coluna quer o
   mínimo" — sozinho, isso só garante que o texto NÃO estoura pro lado; para de fato truncar
   (em vez do auto-layout simplesmente dar 0px à coluna) a lista PRECISA de largura explícita
   nas colunas vizinhas (ver table-layout:fixed escopado a catalog-products/ks-crud-list-demo,
   componentes.css:437-447 — cada lista que adotar .cell--truncate calibra suas próprias
   colunas vizinhas; catalog é a referência de validação deste átomo, as outras 21 ficam
   pendentes por decisão humana, item 4 do retorno). */
@media (min-width: 641px) {
  .table .cell--truncate { max-width: 0; overflow: hidden; }
  .table .cell--truncate > * { display: block; width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
/* key-value (detail): th de linha = rótulo */
.table tbody th { text-align: left; white-space: nowrap; vertical-align: top; padding: var(--sp-12); font-weight: var(--fw-semibold); color: var(--c-gray); border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border); }
.table tbody tr:last-child th { border-bottom: none; }

/* ═════════ .tabs / .tab-panel (detail) — léxico de detail ═════════ */
.tabs { display: flex; flex-wrap: nowrap; gap: var(--sp-4); border-bottom: var(--bw-thin) var(--bs-solid) var(--c-border); overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; white-space: nowrap; scrollbar-width: none; min-width: 0; background: var(--c-white); }   /* S4b/A16: trilho de tabs = divisor interno (peso padrao); unificado: scroll horizontal em todas as resoluções (sem quebra linha) */
.tabs::-webkit-scrollbar { display: none; }
.stack[data-inventory-root] > .tabs { padding-inline: var(--sp-24); }   /* estoque-tabs-alinhamento-list-frame: nav.tabs não tem padding próprio (x=0), mas o conteúdo de .list-frame nasce inset 24px (linha 543) — mesmo problema que motivou a regra de .page-head (linha 546), replicado aqui só p/ o workspace de estoque (escopo data-inventory-root, zero risco a Produtos/outros consumidores de .tabs) */
.stack[data-inventory-root] > .tabs { border-bottom: none; background-image: linear-gradient(var(--c-border), var(--c-border)); background-repeat: no-repeat; background-position: left bottom; background-size: 100% var(--bw-thin); background-origin: content-box; background-clip: content-box; }   /* estoque-tabs-border-content-box: border-bottom normal desenha na borda EXTERNA da caixa (x=0↔100%), ignorando o padding-inline acima — a linha "vazava" além da tabela inset. Trocada por background em content-box: a linha nasce/morre exatamente onde o padding começa/termina, em qualquer largura (inclusive 0 no breakpoint ≤860px, onde volta a ir de ponta a ponta sozinha, sem regra extra). background-color:white da .tabs base (shorthand, linha 793) preservado — só as sub-props de imagem são sobrescritas aqui via longhand. */
.tab { flex: 0 0 auto; padding: var(--sp-10) var(--sp-12); margin-bottom: calc(-1 * var(--bw-thin)); border: none; background: none; border-bottom: var(--bw-thick) var(--bs-solid) transparent; color: var(--c-gray); font-family: var(--font-sans); cursor: pointer; white-space: nowrap;   /* fs+fw: role action */ transition: color var(--dur-fast) var(--ease-base), border-color var(--dur-fast) var(--ease-base); }
.tab:hover { color: var(--c-ink); }
.tab.is-active { color: var(--c-accent); border-bottom-color: var(--c-accent); }
.tab-panel { display: none; }
.tab-panel.is-active { display: block; }

/* .stack como item de grid tem min-width:auto por padrão e recusa encolher abaixo do conteudo (armadilha
   flex/grid-item), impedindo o overflow-x:auto de .tabs de ativar dentro do form-grid — retorno produtos-tabs-scroll-fix */
.form-grid > .stack { min-width: 0; }

/* ═════════ .detail-frame + .detail-status + .code-block ═════════ */
.detail-frame { padding-top: var(--sp-16); }
/* PASSO 14 [produto-create-detail-paridade]: Produtos zera o padding-top — detail_status
   ficou vazio (Passo 12, espelho estrito do create, que não tem cabeçalho de status), então
   o padding-top de .detail-frame virou um segundo respiro redundante (gap do .stack já dá
   16px entre .tabs e .detail-frame; +16px do padding-top somava 32px, o dobro do create).
   Escopado a .produto-detail (body_class) — as outras 15 telas de detail têm detail_status
   com conteúdo real e não devem perder este respiro; auditado via workflow antes de mudar
   a regra global (components.css:831 continua igual para elas). */
.produto-detail .detail-frame { padding-top: 0; }
/* C3 [detail-labels-tipografia]: card__title sem bold, ESCOPO TRANSITÓRIO só a Produtos —
   .card__title é classe global (role:title-card, atoms.css, compartilhada com
   .form-section__title/.module-card__title; 157 instâncias em 32 arquivos do projeto
   inteiro, confirmado por auditoria — não é específica de detail). Aplicar sem escopo
   mudaria toda a aplicação; decisão explícita foi restringir só a produto_detail.html
   (via body_class .produto-detail, mesmo hook do Passo 14) até validação visual. Depois
   da aprovação: promover pra global (remover escopo, mudar atoms.css:31 direto) OU criar
   modifier próprio (ex.: .card__title--quiet) se a intenção for só ALGUNS card__title, não
   todos. NÃO promover sem essa decisão explícita — o escopo aqui é deliberadamente
   provisório, não a forma final. */
.produto-detail .card__title { font-weight: var(--fw-regular); }
.detail-status { display: flex; align-items: center; gap: var(--sp-12); flex-wrap: wrap; }
.detail-status__title { margin: 0; }   /* tipografia: role title-section */
.code-block { margin: 0; padding: var(--sp-12); background: var(--c-surface-sunken); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-control); font-family: var(--font-mono); font-size: var(--fs-12); line-height: var(--lh-base); color: var(--c-ink); white-space: pre; overflow: auto; max-height: calc(var(--sidebar-w) * 2); }

/* ═════════ form layout (CRUD) — campo = .field; estes são só LAYOUT ═════════ */
.form-stack { display: flex; flex-direction: column; gap: var(--sp-16); }   /* C6: 20→16 (sp-20 estrutural fora da escala 8/12/16/24) */
.form-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-16); }
.form-grid--2 { grid-template-columns: 1fr 1fr; }
.form-grid--3 { grid-template-columns: 1fr 1fr 1fr; }
.form-grid__cell--end { align-self: end; }   /* produtos-grid-alinhamento: item alinhado à base da row (par de célula mais alta, ex.: textarea) — mesmo idioma de components.css:559 .formset__row-actions */
@media (max-width: 640px) { .form-grid--2, .form-grid--3 { grid-template-columns: 1fr; } }
.form-section { display: flex; flex-direction: column; gap: var(--sp-12); }
.form-section__title { margin: 0 0 var(--sp-4); }   /* tipografia: role title-card */
.form-actions { display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-8); flex-wrap: wrap; margin-top: var(--sp-16); }   /* C6: 20→16 (legado tb era 16) */
@media (max-width: 520px) {
  /* form-actions-mobile-v3: botões centrados no meio vertical da faixa COMPLETA (de fim do
     campo anterior até fim do card/container). MEDIDO: faixa 92px (396→488, +16 margin-bottom),
     botão 44px, espaço livre 48px. Distribuição simétrica: margin-top ajustado para reduzir
     espaço acima de 32px para 16px (colapse com margin anterior), margin-bottom --sp-16 cria
     16px abaixo, resultado: 16 + 44 + 16 = 76px (o meio). Laterais/widths preservados. */
  .form-actions { justify-content: space-between; padding-inline: var(--sp-14); margin-top: 0; margin-bottom: var(--sp-16); }
  .form-actions > button, .form-actions > a, .form-actions > [type="submit"] { flex: 1; }
}

/* ═════════ .confirm-card (delete) ═════════ */
.confirm-card { display: flex; flex-direction: column; gap: var(--sp-12); max-width: calc(var(--container) / 2); padding: var(--sp-24); background: var(--c-white); border: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); border-radius: var(--r-container); box-shadow: var(--shadow-sm); }
.confirm-card__title { margin: 0; }   /* tipografia: role title-section */
.confirm-card__subject { margin: 0; }   /* tipografia: role prose (16/lh-relaxed/70ch — S4/C4) */
.confirm-card__warning { padding: var(--sp-12) var(--sp-14); border-left: var(--bw-accent) var(--bs-solid) var(--c-yellow); background: color-mix(in srgb, var(--c-yellow) 12%, var(--c-white)); color: var(--c-ink); border-radius: var(--r-sm); }
.confirm-card__actions { display: flex; justify-content: flex-end; gap: var(--sp-8); margin-top: var(--sp-8); flex-wrap: wrap; }
.confirm-card__actions form { display: flex; gap: var(--sp-8); margin: 0; }

/* ═════════ .error-page (página de erro HTTP 4xx/5xx — card centralizado) ═════════ */
.error-page { min-height: 100dvh; display: grid; place-items: center; padding: var(--sp-24); }
.error-page > .card { max-width: calc(var(--container) / 2.5); width: 100%; }
.error-page__code { font-size: var(--fs-12); font-weight: var(--fw-bold); letter-spacing: var(--ls-wide); color: var(--c-gray); }
.error-page ul { margin: 0; padding-left: var(--sp-20); display: flex; flex-direction: column; gap: var(--sp-6); font-size: var(--fs-14); color: var(--c-gray); }

/* ═════════ .ui-actions-menu (kebab — menu de ações por linha) ═════════ */
/* Disclosure nativo: <details class="ui-actions-menu"> abre/fecha sozinho (sem JS).
   <summary class="ui-actions-menu__trigger"> = o botão ⋮; .__pop = o popover; os
   itens (.__item) são <a> (links) ou <button> em <form> (ações POST). O JS de
   components.js só FECHA o menu ao clicar fora / Esc (estado, não markup). */
.ui-actions-menu { position: relative; display: inline-block; }
.ui-actions-menu > summary { list-style: none; }
.ui-actions-menu > summary::-webkit-details-marker { display: none; }
.ui-actions-menu > summary::marker { content: ""; }
/* B2/B7d: FUNDIDO com o .btn--icon quiet — mesma anatomia byte-a-byte (ghost 44, r-pill, cinza,
   hover/aberto accent-soft+accent). Classe própria mantida só porque <summary> não é <button>
   (não deve herdar o .btn base); qualquer mudança no quiet replica aqui. */
.ui-actions-menu__trigger {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--btn-h); height: var(--btn-h);
  border-radius: var(--r-pill);
  color: var(--c-gray);
  background: transparent;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-base), color var(--dur-fast) var(--ease-base);
}
.ui-actions-menu__trigger:hover,
.ui-actions-menu__trigger:focus-visible,
.ui-actions-menu[open] .ui-actions-menu__trigger {
  background: var(--c-accent-soft);
  color: var(--c-accent);
}
.ui-actions-menu__trigger:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: var(--ring-offset); }
.ui-actions-menu__pop {
  position: absolute; top: calc(100% + var(--sp-6)); right: 0;
  z-index: var(--z-dropdown);
  min-width: calc(var(--sidebar-collapsed-w) * 2.5);
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-6);
  background: var(--c-white);
  border: var(--bw-thin) var(--bs-solid) var(--c-border-strong);
  border-radius: var(--r-pop);
  box-shadow: var(--shadow-lg);
}
.ui-actions-menu__pop form { margin: 0; }
.ui-actions-menu__item {
  display: flex; align-items: center; gap: var(--sp-8); width: 100%;
  padding: var(--sp-8) var(--sp-10);
  border: none; background: transparent;
  border-radius: var(--r-sm);
  font-family: var(--font-sans); font-size: var(--fs-13); font-weight: var(--fw-semibold);
  color: var(--c-ink); text-align: left; text-decoration: none; cursor: pointer;
  transition: background var(--dur-fast) var(--ease-base);
}
.ui-actions-menu__item:hover { background: var(--c-surface-hover); }   /* S4/A14 */
.ui-actions-menu__item:focus-visible { outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: calc(-1 * var(--ring-offset)); }
.ui-actions-menu__item .material-symbols-outlined { font-size: var(--icon-sm); opacity: var(--op-muted); }
.ui-actions-menu__label { flex: 1; min-width: 0; }
.ui-actions-menu__item--danger { color: var(--c-danger); }
.ui-actions-menu__item--danger:hover { background: color-mix(in srgb, var(--c-danger) 8%, var(--c-white)); }
.ui-actions-menu__sep { height: var(--bw-thin); margin: var(--sp-4) var(--sp-6); background: var(--c-border); }
/* coluna de ações: encolhe à direita e não deixa o ⋮ acidentar largura */
.ui-actions-col { width: var(--btn-h); text-align: right; white-space: nowrap; }
/* o popover não pode ser clipado pelo overflow do .table-wrapper: enquanto um menu
   está aberto, o wrapper deixa de clipar (o overflow-x:auto computaria overflow-y:auto). */
.table-wrapper:has(.ui-actions-menu[open]) { overflow: visible; }

/* ═════════ .ui-filter-menu (popover de filtros de página — irmã do kebab) ═════════ */
/* Disclosure nativo como o .ui-actions-menu (design do legado .ui-filter-menu/core.css:468-516,
   com tokens do refac). O trigger é o PRÓPRIO <summary class="btn btn--icon"> (o legado punha um
   <button> dentro do summary); o __pop carrega .field's + .__actions (Limpar/Aplicar). Ancora à
   DIREITA (no legado era left:0, mas aqui vive no .page-head__actions, encostado na borda direita). */
.ui-filter-menu { position: relative; display: inline-block; }
.ui-filter-menu > summary { list-style: none; }
.ui-filter-menu > summary::-webkit-details-marker { display: none; }
.ui-filter-menu > summary::marker { content: ""; }
.ui-filter-menu__pop {
  position: absolute; top: calc(100% + var(--sp-6)); right: 0;
  z-index: var(--z-dropdown);
  min-width: calc(var(--sidebar-collapsed-w) * 3.5);
  display: flex; flex-direction: column; gap: var(--sp-10);
  padding: var(--sp-12);
  background: var(--c-white);
  border: var(--bw-thin) var(--bs-solid) var(--c-border-strong);
  border-radius: var(--r-pop);
  box-shadow: var(--shadow-lg);
}
.ui-filter-menu__actions { display: flex; gap: var(--sp-8); justify-content: flex-end; }

/* ═════════ .upload (campo de arquivo — componente GLOBAL, 3 módulos) ═════════ */
/* O <input type=file> nativo é coberto e transparente; a área toda (.upload, um
   <label>) abre o seletor. O JS de components.js escreve o nome em __filename
   (3 camadas: HTML data-upload, CSS estado, JS texto). Sem drag (não-legado).
   Agnóstico ao módulo: accounts/catalog/people consomem o mesmo componente. */
.upload {
  position: relative;
  display: flex; flex-direction: column; gap: var(--sp-6);
  padding: var(--sp-16);
  border: var(--bw-thin) var(--bs-dashed) var(--c-border-strong);
  border-radius: var(--r-container);
  background: var(--c-white);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-base), background var(--dur-fast) var(--ease-base);
}
.upload:hover { border-color: var(--c-accent); background: color-mix(in srgb, var(--c-accent) 6%, var(--c-white)); }
.upload:focus-within { border-color: color-mix(in srgb, var(--c-accent) 38%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent); }   /* kit-focus-legado: glow */
.upload input[type="file"] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.upload__cta { display: inline-flex; align-items: center; gap: var(--sp-8); color: var(--c-accent); }   /* fs+fw: role action */
.upload__cta .material-symbols-outlined { font-size: var(--icon-md); }
.upload__filename { font-size: var(--fs-13); color: var(--c-ink); }
/* .upload__hint — tipografia integralmente via role caption (12/gray) */

/* ═════════ .ui-switch (toggle deslizante on/off — reusável) ═════════ */
/* <label.ui-switch><input type=checkbox><span.ui-switch__track><span.ui-switch__thumb>
   O .check do kit é checkmark (quadrado); este é o slider on/off (afordância de toggle). */
.ui-switch { position: relative; display: inline-flex; align-items: center; cursor: pointer; }
.ui-switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.ui-switch__track {
  position: relative; display: inline-block;
  width: calc(var(--sp-20) + var(--sp-16)); height: var(--sp-20);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--c-gray) 45%, var(--c-white));
  transition: background var(--dur-fast) var(--ease-base);
}
.ui-switch__thumb {
  position: absolute; top: var(--sp-2); left: var(--sp-2);
  width: var(--sp-16); height: var(--sp-16);
  border-radius: var(--r-circle);
  background: var(--c-white); box-shadow: var(--shadow-sm);
  transition: transform var(--dur-fast) var(--ease-base);
}
.ui-switch input:checked + .ui-switch__track { background: var(--c-cta); }
.ui-switch input:checked + .ui-switch__track .ui-switch__thumb { transform: translateX(var(--sp-16)); }
.ui-switch input:focus-visible + .ui-switch__track { box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent); }   /* kit-focus-legado: glow (track sem borda própria — só o shadow) */
.ui-switch input:focus-visible { outline: none; }   /* produtos-focus-fix-v2: suprime o ring nativo do input (opacity:0, sobre o track) — glow do track acima é o indicador */
.ui-switch input:disabled + .ui-switch__track { opacity: var(--op-disabled); cursor: not-allowed; }
.ui-switch input:disabled { cursor: not-allowed; }

/* ═════════ .module-card (card de módulo com toggle) ═════════ */
.module-card { display: flex; flex-direction: column; gap: var(--sp-8); padding: var(--sp-12); background: var(--c-white); border: var(--bw-thin) var(--bs-solid) var(--c-border-subtle); border-radius: var(--r-container); box-shadow: var(--shadow-sm); transition: border-color var(--dur-fast) var(--ease-base), opacity var(--dur-fast) var(--ease-base); }   /* C6: pad 16→12 (nível denso, = kpi--sm; era o pad do legado) */
.module-card.is-enabled { border-color: color-mix(in srgb, var(--c-success) 45%, var(--c-white)); }
.module-card.is-saving { opacity: var(--op-muted); }
.module-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-12); }
.module-card__title { margin: 0; }   /* tipografia: role title-card */
.module-card__desc { margin: 0; }   /* tipografia: role subtext */
.module-card__foot { display: flex; align-items: center; gap: var(--sp-8); }

/* ═════════ .toast (feedback flutuante — reusável; usa .alert dentro/junto) ═════════ */
.toast { position: fixed; right: var(--sp-16); bottom: var(--sp-16); z-index: var(--z-toast); max-width: var(--sidebar-w); opacity: 0; transform: translateY(var(--sp-8)); pointer-events: none; transition: opacity var(--dur-base) var(--ease-base), transform var(--dur-base) var(--ease-base); }
.toast.is-visible { opacity: 1; transform: none; pointer-events: auto; }

/* ═════════ .thumb (miniatura de imagem — listas; global: studio + ecommerce) ═════════ */
/* B7/R3: 56px estrutural (linha de lista ~80px total). O calc(--btn-h*2) antigo acoplava a thumb
   à altura de botão (com --btn-h 44 do S1 daria 88px — acoplamento acidental, não decisão). */
.thumb { width: 56px; height: 56px; border-radius: var(--r-container); object-fit: cover; background: var(--c-surface-sunken); border: var(--bw-thin) var(--bs-solid) var(--c-border); }

/* ═════════ .bulk-bar (ações em massa — ADR-0005; par visual do data-bulk-* de components.js) ═════════ */
/* Aparece só com seleção ativa (components.js liga .is-active quando count > 0 na tabela-alvo
   [data-bulk-bar="<key>"] ↔ [data-bulk-table="<key>"]). Ações são .btn normais com data-bulk-action. */
.bulk-bar { display: none; align-items: center; gap: var(--sp-8); padding: var(--sp-8) var(--sp-12); background: var(--c-surface-sunken); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-control); }
.bulk-bar.is-active { display: flex; flex-wrap: wrap; }
.bulk-kebab, .bulk-kebab.is-active { display: none; }   /* card-lista-f3-selecionar-todos: kebab de bulk do card de produtos é MOBILE-ONLY — a exibição (com .is-active) vive no @media ≤520px do card (§.table--stack); fora dele a _bulk_bar acima segue dona das ações em massa */
.bulk-bar__count { font-weight: var(--fw-semibold); }
.bulk-bar__spacer { flex: 1; }

/* ═════════ .avatar (inicial OU imagem em círculo — GLOBAL; nasce no topnav, herdam membros/whatsapp) ═════════ */
/* Aparência canônica = legado: círculo claro outline + inicial navy. Parametrizável por
   tamanho (--avatar-size, default --icon-lg; modifiers --sm/--lg) e cor (variantes --accent/--ink). */
.avatar {
  --avatar-size: var(--icon-lg);
  display: inline-grid; place-items: center; flex: 0 0 auto;
  width: var(--avatar-size); height: var(--avatar-size);
  border-radius: var(--r-circle);
  background: var(--c-white);
  border: var(--bw-thin) var(--bs-solid) var(--c-border);
  color: var(--c-ink);   /* S1: --c-navy-dark morreu (Δ3.2 de --c-ink, DESIGN-SPEC A2) — inicial do avatar absorve o ink */
  font-size: var(--fs-12); font-weight: var(--fw-semibold);
  overflow: hidden; user-select: none;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar--sm { --avatar-size: var(--icon-md); }
.avatar--lg { --avatar-size: var(--icon-xl); }
.avatar--accent { background: var(--c-accent); border-color: transparent; color: var(--c-white); }
.avatar--ink    { background: var(--c-ink);   border-color: transparent; color: var(--c-white); }

/* ═════════ .profile-head (E0.10 — banner + .avatar sobreposto + identidade, centralizado) ═════════
   Contexto INTERNO (ERP) — tokens da casa (--sp-*/--fs-*/--fw-*/--c-*), NUNCA --sf-* (vitrine).
   Reusa o átomo .avatar existente (.avatar--lg) — nenhum átomo de avatar novo. */
.profile-head { display: flex; flex-direction: column; align-items: center; text-align: center; margin-bottom: var(--sp-24); }
.profile-head__banner { width: 100%; height: var(--sp-32); background: var(--c-surface-tint); border-radius: var(--r-container); }
/* E0.11: --avatar-size é o ponto de extensão documentado do átomo .avatar (components.css:1057) —
   escala "hero" própria do profile-head (2x --icon-xl, o teto da escala de ícones), não um
   átomo novo. margin-top usa a MESMA variável do tamanho (nunca um número solto) pra nunca
   dessincronizar sobreposição de escala se o tamanho mudar. */
.profile-head__avatar { --avatar-size: calc(var(--icon-xl) * 2); margin-top: calc(-1 * var(--avatar-size) / 2); border-width: var(--bw-thick); border-color: var(--c-white); }
/* E0.11: respiro consistente — gap único no flex column (era gap + margin-top redundante
   entre código→badges, respiro desigual em relação a título→código). */
.profile-head__identity { margin-top: var(--sp-12); display: flex; flex-direction: column; align-items: center; gap: var(--sp-6); }
.profile-head__code { font-size: var(--fs-11); color: var(--c-gray); }
.profile-head__badges { display: flex; align-items: center; gap: var(--sp-8); }
.profile-head__actions { display: flex; align-items: center; gap: var(--sp-8); margin-top: var(--sp-12); }
.profile-head__avatar-control { position: relative; }
.profile-head__avatar--upload { position: relative; cursor: pointer; }
.profile-head__avatar--upload .upload-tile__preview { border-radius: inherit; }
.profile-head__avatar-input, .profile-head__avatar-clear { position: absolute; width: var(--bw-thin); height: var(--bw-thin); padding: 0; margin: calc(-1 * var(--bw-thin)); overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.profile-head__avatar-control:focus-within .profile-head__avatar--upload { box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent); }
.profile-head__avatar-control.is-clear-marked .profile-head__avatar--upload { opacity: var(--op-disabled); }

/* ═════════ .wa-preview (bolha estilo WhatsApp — preview de mensagem; reusável p/ chat) ═════════ */
.wa-preview { background: color-mix(in srgb, var(--c-accent) 6%, var(--c-white)); border: var(--bw-thin) var(--bs-solid) var(--c-border); border-radius: var(--r-container); padding: var(--sp-16); }
.wa-preview__meta { margin-bottom: var(--sp-8); }   /* tipografia: role caption */
.wa-preview__bubble { display: inline-block; max-width: var(--sidebar-w); padding: var(--sp-12) var(--sp-14); border-radius: var(--r-pop); background: var(--c-white); border: var(--bw-thin) var(--bs-solid) var(--c-border); box-shadow: var(--shadow-sm); white-space: pre-wrap; line-height: var(--lh-base); }   /* tipografia: role body (+lh proprio) */

/* ═════════ .ia-progress (barra de progresso de geração IA; .is-active via JS) ═════════ */
.ia-progress { display: none; height: var(--sp-6); margin-top: var(--sp-8); border-radius: var(--r-pill); background: var(--c-surface-sunken); overflow: hidden; }
.ia-progress.is-active { display: block; }
.ia-progress__bar { height: 100%; width: 60%; background: var(--c-accent); border-radius: var(--r-pill); transition: width var(--dur-base) var(--ease-base); }

/* ═════════ F7 [ks-fechar-frontend] — fixes mobile PROMOVIDOS da bancada (ADR-0018/L12=a) ═════════
   Viviam sob .ks em kitchen_sink.css (era T0; re-localizados e movidos em F7) — a bancada
   renderizava melhor que o produto nos mesmos breakpoints (inversão da função de espelho).
   Telas do produto que mudam @≤640: lists com .ui-filter-menu (12 arquivos — o pop right:0
   deixa de estourar a viewport), tabelas .table/.table--stack (22 lists + details) e colunas
   .ui-actions-col (23 arquivos com kebab). Prova visual runtime registrada no retorno de F7. */
@media (max-width: 640px) {
  .ui-filter-menu__pop {
    left: 0;
    right: auto;
    min-width: min(calc(var(--sidebar-collapsed-w) * 3.5), calc(100vw - var(--sp-32)));
    max-width: calc(100vw - var(--sp-32));
  }
  .table {
    table-layout: fixed;
    width: 100%;
  }
  .table th,
  .table td {
    overflow-wrap: anywhere;
  }
  .table--stack tbody td.ui-actions-col {
    width: 100%;
    white-space: normal;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: var(--sp-8);
  }
}

/* ═════════ .media-block (form de produto: preview de imagem + gerador IA) ═════════ */
/* Fidelidade do legado: catalog.css:824 (.catalog-preview-grid — grid minmax(220px,300px) + 1fr,
   gap 18, imagem à ESQUERDA / campos à direita) e :238-266/:896 (.produto-preview-box — 4/5,
   borda dashed ink, radius md, placeholder centrado). Dimensões via tokens --media-preview-*. */
/* v4: a THUMB é a referência (dimensões da v2: coluna minmax(220,300), ratio 4/5, altura da
   faixa = altura do preview). A coluna direita é grid 1fr/1fr: as 2 linhas botão+minis se
   distribuem pela altura total, sem sobra. (Bug v3 medido: preview flex com aspect-ratio
   definia o cross-size hipotético da linha — stretch estica os menores, nunca encolhe o maior.) */
.media-block { display: grid; grid-template-columns: minmax(var(--media-preview-min-w), var(--media-preview-w)) auto minmax(0, 1fr); row-gap: var(--sp-18); column-gap: var(--sp-24); align-items: stretch; }   /* v11: respiro horizontal tiles↔selects maior que o vertical (sp-24 do kit) */
/* v7: 3ª coluna — os 5 people-combos empilhados na faixa da thumb, à direita dos tiles */
.media-block__combos { min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: space-between; gap: var(--sp-16); }   /* v15: 1º/último nos extremos da faixa da thumb, intermediários proporcionais (space-between). gap = piso: sem efeito em >=860px onde a faixa da thumb dá folga (medido 29px, >16); torna-se o espaçamento real em ~700-860px onde .media-block__combos vira linha cheia sem faixa de referência e space-between zera (medido 0px) — media-block-combos-gap */
.media-block__combos > .field { border: 0; background: transparent; }   /* v15: só a caixa do .combo-input é visível — wrapper sem borda/bg própria (escopado à coluna) */
/* v16: variante flat — .combo-input da coluna sem borda/bg (chrome base: components.css:577
   border --bw-thin solid --c-border-strong + bg --c-white); hover/focus (base :583 só trocava
   border-color, invisível sem borda) SUBSTITUÍDO por outline explícito — a11y preservada mesmo
   flat. Escopado à coluna; .combo global (outras telas) intocado. */
.media-block__combos .combo > .combo-input { border-color: transparent; background: transparent; }
.media-block__combos .combo:focus-within > .combo-input { border-color: transparent; outline: var(--ring-w) var(--bs-solid) var(--ring-color); outline-offset: var(--ring-offset); }
/* produtos-thumb-design-unico: mesmo fundo/borda pontilhada/hover do .upload-tile (:952-953)
   — família visual única entre thumb grande e os 4 minithumbs. Dimensões/ratio/padding
   intocados (só bg/border/hover mudam de valor). */
.media-block__preview { width: 100%; aspect-ratio: 4 / 5; display: flex; align-items: center; justify-content: center; padding: var(--sp-12); border: var(--bw-thin) var(--bs-dashed) var(--c-border-strong); border-radius: var(--r-container); background: var(--c-white); overflow: hidden; transition: border-color var(--dur-fast) var(--ease-base), background var(--dur-fast) var(--ease-base); }
.media-block__preview:hover { border-color: color-mix(in srgb, var(--c-accent) 38%, transparent); background: color-mix(in srgb, var(--c-accent) 6%, var(--c-white)); }
.media-block__controls { min-width: 0; min-height: 0; display: grid; grid-template-rows: 1fr 1fr; gap: var(--sp-12); }
/* v14: cada célula do grid É o field-row do as_tile (sem wrapper intermediário) */
.media-block__controls > .field { min-height: 0; }
.media-block__preview a { display: block; width: 100%; height: 100%; }
.media-block__preview img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: var(--r-container); }
/* linha de progresso da arte IA (consome o owner .ia-progress; wrapper é só layout de linha) */
.media-block__progress { display: flex; align-items: center; gap: var(--sp-12); }
.media-block__progress .ia-progress { flex: 1; margin-top: 0; }
/* v12: antes de cortar, empilha — abaixo de 960 (mesma fronteira que o legado usava p/ o preview,
   catalog.css:275) os combos saem da 3ª coluna e viram fileira LARGURA TOTAL abaixo de thumb+tiles.
   Conta do estouro em 3 colunas: piso col1 220 + tiles ~298 (2×~145 flex:none, largura derivada do
   ratio — NÃO encolhem) + gaps 48 ≈ 566 antes dos combos; em viewport com sidebar isso não sobra
   coluna útil, e o min-width:0 da col2 deixava os tiles transbordarem sobre a 3ª. */
@media (max-width: 960px) {
  .media-block { grid-template-columns: minmax(var(--media-preview-min-w), var(--media-preview-w)) minmax(0, 1fr); }
  .media-block__combos { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .media-block { grid-template-columns: 1fr; }
  .media-block__preview { width: min(100%, var(--media-preview-w)); }
  .media-block__controls { grid-template-rows: none; }
  .media-block__combos { justify-content: flex-start; gap: var(--sp-12); }   /* v15: empilhado sem faixa de referência — space-between sem conteúdo esticando não separa; volta a fluxo com gap */
  .upload-tile { height: auto; flex: 1; }
}
/* media-block-mobile-simetria: medido em 375px — .media-block/.upload-row já eram simétricos
   (17px/17px, herdado do padding do .card), a assimetria real era só o thumb grande: width
   min(100%,--media-preview-w) o trava em 300px numa coluna de 341px, sobrando 41px só à direita
   (sem centralizar). Tiles: a regra ≤640px acima (linha 1007, .upload-tile{flex:1}) é código
   MORTO — empata em especificidade (0,1,0) com a base .upload-tile (linha ~1019, flex:none) e
   perde por ordem-de-fonte (a base vem DEPOIS no arquivo); tiles ficavam flex:0 0 auto (114-132px
   fixos, não cresciam). Não mexi na regra ≤640px pré-existente (fora do escopo desta tarefa) — só
   sobreponho com um seletor mais específico dentro de ≤520px. */
@media (max-width: 520px) {
  .media-block__preview { width: 100%; margin-inline: 0; }   /* thumb-grande-mobile-fullwidth: substitui o cap min(100%,--media-preview-w) (components.css:1004) por 100% — alinha às bordas dos grupos de tiles (mesma coluna); aspect-ratio:4/5 herdado (linha 983), inalterado — altura cresce junto */
  .upload-row .upload-tile { flex: 1 1 0; width: auto; height: auto; }   /* especificidade (0,2,0) vence a base .upload-tile (0,1,0) independente de ordem-de-fonte; aspect-ratio:4/5 herdado da base, não alterado — cresce mantendo o ratio */
}

/* v5: grupo de 2 minithumbs-input — o CONTROLE é o tile (fundo branco, borda pontilhada,
   glifo upload + label dentro, ratio 4/5 herdado da thumb; input file coberto, mesmo truque
   do .upload:749). Tile 2 (--saved) = arquivo salvo, mesmo visual de placeholder, não clicável.
   O [data-upload-preview] é absoluto e COBRE o placeholder ao escolher (engine intocado). */
/* v13: box-model ÚNICO — tile sem padding (o respiro do texto mora no __hint, que não altera o
   box); espaçamento entre tiles = gap do grid dos tiles (sp-12, mesmo token das rows), nunca
   padding/margin. Imagens (preview/salvo) já eram inset:0 e agora casam com o placeholder. */
.upload-row { display: flex; flex-direction: row; align-items: stretch; gap: var(--sp-12); }   /* v14: direction explícita — a classe compõe com .field (que é column) e precisa vencê-la */
.upload-row > .field__error { flex-basis: 100%; }
.upload-tile { position: relative; flex: none; height: 100%; min-width: 0; aspect-ratio: 4 / 5; display: inline-flex; align-items: center; justify-content: center; background: var(--c-white); border: var(--bw-thin) var(--bs-dashed) var(--c-border-strong); border-radius: var(--r-container); overflow: hidden; cursor: pointer; transition: border-color var(--dur-fast) var(--ease-base), background var(--dur-fast) var(--ease-base); }
.upload-tile:hover, .upload-tile:focus-within { border-color: color-mix(in srgb, var(--c-accent) 38%, transparent); background: color-mix(in srgb, var(--c-accent) 6%, var(--c-white)); }
.upload-tile input:focus-visible { outline: none; }   /* produtos-focus-fix-v2: suprime o ring nativo do input file (opacity:0, cobre o tile) — glow do tile (:focus-within acima) é o indicador */
.upload-tile:focus-within { box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 18%, transparent), 0 0 16px color-mix(in srgb, var(--c-accent) 10%, transparent); }   /* kit-focus-legado: glow só no foco (hover mantém só a borda, como já era) */
/* produtos-tiles-todos-upload: --saved vira label[for] do mesmo input — clicável, mesmo hover
   de ação do tile 1 (herda do .upload-tile base); cursor:pointer já vem da base. */
.upload-tile input[type="file"] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.upload-tile__hint { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4); min-width: 0; max-width: 100%; padding: var(--sp-8); color: var(--c-text-placeholder); text-align: center; }
.upload-tile__label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-12); font-weight: var(--fw-semibold); }
.upload-tile__preview { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: var(--c-white); }
.upload-tile__preview[hidden] { display: none; }
.upload-tile__preview img, .upload-tile > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* BOM-tabela-v3 (espelho do legado): linha secundária discreta com os campos que o legado
   não exibia (mantidos no POST); extras em fluxo com chrome canônico; dropdown do selectlist
   liberado do clip do wrapper (mesmo idioma da mitigação do kebab, :has em .table-wrapper). */
.formset__row--secondary td { background: var(--c-surface-subtle); }
.formset__extras { display: flex; flex-wrap: wrap; gap: var(--sp-8); }
.formset__extras .field__wrap { flex: 1 1 10rem; min-width: 0; }
.formset__row--deleted + .formset__row--secondary { opacity: var(--op-disabled); }
.table-wrapper:has(details.selectlist[open]) { overflow: visible; }

/* BOM-tabela-v5: fidelidade COMPUTADA ao legado (probe Playwright 2026-07-22,
   /produtos/1/editar/ em :8000 × :8001, getComputedStyle). Token só quando EXATO;
   literal COM proveniência quando o legado diverge do kit — o visual do legado vence
   (decreto v5). Escopo estrito .formset__table (BOM); demais tabelas intocadas. */
.formset__table { font-size: var(--fs-16); color: var(--c-ink); background: transparent; }   /* decreto produtos-bom-sem-tabela: sem superfície própria — o card por trás já é a moldura */
.formset__table tbody td { padding: var(--sp-12) var(--sp-14); border-bottom: 0; }   /* decreto: sem divisória entre linhas — só o espaçamento (padding) preservado */
/* .table-wrapper:has(.formset__table): moldura genérica (border/bg/radius, components.css:348)
   removida SÓ para esta tabela — demais .table-wrapper (listas, etc.) intocados */
.table-wrapper:has(.formset__table) { background: transparent; border: 0; border-radius: 0; }
.formset__table .field__wrap > input, .formset__table .field__wrap > select { height: var(--control-h); padding: var(--sp-8) var(--sp-10); border-color: var(--c-border); font-size: var(--fs-16); }
  /* legado .input core.css:942-960 + computado: 52px · padding 8px 10px · borda #e5e7eb (=--c-border) · 16px — vence o chrome sp-14/border-strong/fs-14 do kit DENTRO da tabela */
.formset__table .field__wrap > input { font-weight: var(--fw-regular); }   /* computado legado: input preenchido = 400 (kit poria 700 via data-has-value) */
.formset__table .field__wrap > select { font-weight: var(--fw-bold); }     /* computado legado: select = 700 */
/* Mobile (<= 520px, paridade com .tabs): row do BOM empilha campos (1 por linha, largura total).
   Gap = var(--sp-16), mesmo token de .form-grid (components.css:720) — ritmo vertical dos campos
   acima do card. Sem borda/bg/sombra por linha (decreto produtos-bom-sem-tabela permanece: sem
   moldura própria). DELETE é o último <td> no DOM — cai naturalmente ao fim da row empilhada,
   sem posicionamento especial (nada de UI nova). */
@media (max-width: 520px) {
  .formset__table tbody tr[data-formset-row] { display: flex; flex-direction: column; gap: var(--sp-16); }
  .formset__table tbody tr[data-formset-row] td { display: block; width: 100%; padding: 0; }
}

/* ═════════ .is-busy — estado ocupado genérico do kit (L4, ADR-0041) ═════════
   Consumido por Crontex.setBusy (components.js): substitui a manipulação direta de
   opacity/pointerEvents que vivia em JS de módulo (era bling_onboarding.js:195-226).
   Token-driven; par com aria-busy="true" setado pelo motor. */
.is-busy { opacity: var(--op-disabled); pointer-events: none; }
.date-range-picker { position:fixed; left:attr(data-left px); top:attr(data-top px); z-index:var(--z-popover); width:min(19rem,calc(100vw - 1rem)); padding:var(--sp-8); background:var(--c-white); border:var(--bw-thin) var(--bs-solid) var(--c-border); border-radius:var(--r-pop); box-shadow:var(--shadow-sm); color:var(--c-ink); }
.date-range-picker__head,.date-range-picker__nav,.date-range-picker__actions { display:flex; align-items:center; gap:var(--sp-6); }
.date-range-picker__head { justify-content:space-between; }.date-range-picker__nav { justify-content:space-between; margin-block:var(--sp-6); text-transform:capitalize; font-size:var(--fs-13); color:var(--c-navy); }.date-range-picker__fields{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-6)}.date-range-picker__fields label{display:grid;gap:var(--sp-2);font-size:var(--fs-12);color:var(--c-gray)}.date-range-picker__fields input{min-width:0;padding:var(--sp-6);border:var(--bw-thin) var(--bs-solid) var(--c-border);border-radius:var(--r-sm);background:var(--c-white);font:inherit;color:var(--c-navy);outline:0}.date-range-picker__fields input::placeholder{color:var(--c-gray)}.date-range-picker__fields input:focus-visible{border-color:var(--c-accent);box-shadow:0 0 0 var(--ring-w) var(--c-accent-soft)}.date-range-picker__grid{display:grid;grid-template-columns:repeat(7,1fr);gap:var(--sp-2)}.date-range-picker__day{border:var(--bw-thin) var(--bs-solid) transparent;background:transparent;padding:var(--sp-6);border-radius:var(--r-pill);cursor:pointer;color:var(--c-ink)}.date-range-picker__day:hover,.date-range-picker__day:focus-visible{background:var(--c-surface-hover);outline:0}.date-range-picker__day.is-range{background:color-mix(in srgb,var(--c-accent) 12%,var(--c-white));border-radius:0}.date-range-picker__day.is-start,.date-range-picker__day.is-end{background:var(--c-accent);color:var(--c-white)}.date-range-picker__error{min-height:1rem;margin:var(--sp-4) 0 0;color:var(--c-danger);font-size:var(--fs-12)}.date-range-picker__actions{justify-content:flex-end}.date-range-picker__actions button,.date-range-picker__head button,.date-range-picker__nav button{border:0;background:transparent;color:var(--c-gray);cursor:pointer}.date-range-picker__actions button{min-height:2rem;padding:var(--sp-4) var(--sp-6);font-size:var(--fs-12)}.date-range-picker__actions [data-apply]{color:var(--c-accent);font-weight:var(--fw-medium)}
