.field {
  margin-bottom: var(--space-2);
}

/* Panel Parametros was taller than the viewport at 100% zoom on common screens (1366x768), forcing
   an internal scroll the legacy never has -- spacing (not font size) was compressed to fit. Item 18
   (docs/melhorias-de-UI-UX.md) collapsed the dead "Base de Dados" section by default, returning some
   of that space: .field's margin-bottom below relaxed from 5px back to 8px (.field's own base
   value), and .stat-list's gap (further down) from 2px to 4px. */
.panel-params h2:not(:first-child) {
  margin-top: 12px;
}

.panel-params h2 {
  margin-bottom: 6px;
}

.panel-params .field {
  margin-bottom: 8px;
}

.panel-params button {
  margin-top: 5px;
}

.panel-params .stat-subhead {
  margin-top: 4px;
}

.panel-params .stat-footnote {
  margin-top: 5px;
  line-height: 1.3;
}

.panel-params input[type='range'] {
  margin-top: 2px;
}

.panel-params .btn-primary {
  padding: 9px;
}

.panel-params .btn-secondary {
  padding: 5px 0;
}

.panel-params .warn {
  margin: 2px 0;
}

/* Só usado hoje pelo painel Negociação (Ticker + os 8 campos do contrato) -- Prazo/Financeiro/Taxa
   CDI do painel Parâmetros usam .field-header label acima. Uppercase + letter-spacing iguais a
   .cf-label da legacy (wwwroot/legacy/index.html:526-529). */
.field label {
  display: block;
  color: var(--color-label);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  margin-bottom: 4px;
}

.field input {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
}

.field input[readonly] {
  background: var(--color-bg);
}

/* Item 15: approved divergence from legacy -- #cf-forward reads as a calculated output (no border,
   transparent background, bold navy mono figure) instead of a gray input like the editable fields
   around it. Specificity (.panel-detail .field input.output-value) beats the panel's own gray-
   background override below, in both rest and :focus state. */
.panel-detail .field input.output-value {
  border: none;
  background: transparent;
  padding-left: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--color-header);
}

.panel-detail .field input.output-value:focus {
  background: transparent;
  border-color: transparent;
}

/* Prazo/Financeiro (painel Parâmetros): label + número inline sem caixa, igual à legacy
   (wwwroot/legacy/index.html:608-627,375-381) -- só estes dois campos, não os do painel
   Negociação (que ficam com a caixa genérica ".field input" acima). */
.field-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

.field-header label {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-label);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  margin-bottom: 0;
}

.field-header-value {
  display: flex;
  align-items: baseline;
  gap: 4px;
}

.field-unit {
  font-size: 11px;
  font-weight: 400;
  color: var(--color-label);
}

/* Fonte do CDI em vigor, ao lado do valor -- visível só para Admin (main.js, renderCdiSource).
   Mesmo tamanho de .field-unit para não desalinhar a linha do campo. */
.cdi-source {
  font-size: 11px;
  font-weight: 600;
  color: var(--color-label);
}

/* `fallback` é aviso, não erro: amarelo e não o vermelho de .warn, pelo mesmo critério de
   .warn--stale -- o valor em vigor é o do cdiConfig, configurado e legítimo, só possivelmente
   desatualizado. Vermelho aqui treinaria a mesa a ignorar vermelho. */
.cdi-source--fallback {
  color: var(--color-warning);
}

.field .param-num {
  width: 112px;
  padding: 0 2px;
  border: none;
  border-bottom: 2px solid var(--color-border);
  border-radius: 0;
  background: transparent;
  font-family: var(--font-ui);
  font-size: 17px;
  font-weight: 700;
  color: var(--color-header);
  text-align: right;
  outline: none;
}

.field .param-num:focus {
  border-bottom-color: var(--color-primary);
}

/* Thumb custom da legacy (docs/_legado/index.html:157-176) -- portado igual pra WebKit/Blink.
   Firefox ganha um ::-moz-range-thumb equivalente que a legacy nunca teve (melhoria deliberada,
   não regressão -- accent-color abaixo já cobria Firefox antes desta mudança). */
input[type='range'] {
  width: 100%;
  height: 4px;
  padding: 0;
  border: none;
  margin-top: var(--space-1);
  border-radius: 2px;
  background: var(--color-border);
  accent-color: var(--color-primary);
  appearance: none;
  outline: none;
  cursor: pointer;
}

input[type='range']::-webkit-slider-thumb {
  appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 0 3px rgb(29 180 232 / 20%);
  cursor: pointer;
}

input[type='range']::-webkit-slider-thumb:hover {
  background: var(--color-primary-hover);
}

input[type='range']::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border: none;
  border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 0 3px rgb(29 180 232 / 20%);
  cursor: pointer;
}

input[type='range']::-moz-range-thumb:hover {
  background: var(--color-primary-hover);
}

.stat-footnote {
  font-size: 10px;
  color: var(--color-label);
  margin-top: var(--space-2);
  line-height: 1.5;
}

/* Caixa da Taxa CDI -- igual a .number-input-wrap da legacy (wwwroot/legacy/index.html:178-206).
   Lá, os dois <span> filhos (valor + unidade) herdam fundo cinza + borda-direita da regra
   ".number-input-wrap span", então a caixa inteira fica cinza com um divisor no meio -- não branca
   lisa. Reproduzido aqui explicitamente (não dá pra herdar de um "span" genérico porque .field-unit
   já é usado, sem fundo, em outros lugares). */
.static-value-box {
  display: flex;
  align-items: center;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--color-bg);
}

.static-value-box .static-value {
  padding: 6px 10px;
  border-right: 1px solid var(--color-border);
}

.static-value-box .field-unit {
  padding: 6px 10px;
}

.static-value {
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 700;
  color: var(--color-header);
}

.btn-primary {
  width: 100%;
  padding: 12px;
  border: none;
  border-radius: var(--radius);
  background: var(--color-header);
  color: var(--color-panel);
  font-weight: 600;
  letter-spacing: 0.3px;
  cursor: pointer;
  margin-top: var(--space-2);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

.btn-primary:hover {
  background: var(--color-header-hover);
}

/* Bolinha azul antes do texto -- igual ao .run-btn .dot da legacy (wwwroot/legacy/index.html:
   228-233,638-640). Só usada no "Calcular Ranking" hoje. */
.btn-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-primary);
  flex-shrink: 0;
}

/* Botão "Calcular Ranking" fica azul (em vez de navy) enquanto pulsa "dirty" -- mesmo comportamento
   da legacy (.run-btn.dirty), não só o pulso; ver @keyframes btn-pulse abaixo.
   Deliberate divergence from legacy (item 3, approved): legacy leaves the inherited white text and
   the cyan .btn-dot on this cyan background, giving ~2.2:1 text contrast and a dot that's invisible
   (same color as the background). Text switches to --color-header (navy, ~5.7:1) and the dot to
   --color-panel (white) below so both stay legible -- background and pulse animation unchanged. */
.btn-primary.dirty {
  background: var(--color-primary);
  color: var(--color-header);
}

.btn-primary.dirty .btn-dot {
  background: var(--color-panel);
}

.btn-secondary {
  width: 100%;
  padding: 6px 0;
  border: 1px solid var(--color-header-hover);
  border-radius: var(--radius);
  background: transparent;
  color: var(--color-header-hover);
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
  margin-top: var(--space-2);
}

.btn-secondary:hover:not(:disabled) {
  background: var(--color-bg);
}

.btn-secondary:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* .stat-grid da legacy (wwwroot/legacy/index.html:237-250) é flex column com gap -- sem borda
   nenhuma entre linhas, espaçamento vem só do gap. A borda pontilhada que existia aqui era do design
   original da F7 (pré-pivô), nunca existiu na legacy -- removida. */
.stat-list {
  display: flex;
  flex-direction: column;
  /* Relaxed from 2px to 4px (item 18) once collapsing "Base de Dados" gave the panel room -- full
     space-2 (8px) still doesn't fit the viewport at 100% zoom, see .panel-params above. */
  gap: 4px;
  margin: 0;
}

/* Item 26: raised from 9px to keep a 10px typographic floor -- applies to both the p.stat-subhead
   usage and the <summary class="stat-subhead"> from item 18 (same class, same rule). */
.stat-subhead {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-primary);
  margin-top: var(--space-2);
}

/* Item 18: "Base de Dados" collapses by default via <details> -- content keeps the .stat-list/
   .stat-footnote styles above untouched (the class only moved onto <summary>, still a p.stat-subhead
   look via that same class). Only the disclosure marker needs its own, deliberately understated
   styling here. */
.stat-details summary {
  cursor: pointer;
}

.stat-details summary::marker {
  font-size: 9px;
  color: var(--color-label);
}

.stat-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2);
  font-size: 12px;
}

.stat-row dt {
  color: var(--color-label);
}

.stat-row dd {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  text-align: right;
}

.hint {
  color: var(--color-label);
  font-size: 12px;
}

/* Status line variants: error and success states for messages that otherwise share a slot with
   neutral text (e.g. #cf-status) -- .hint stays the neutral default, these two signal an outcome. */
.status-error {
  color: var(--color-negative-text);
  font-weight: 600;
}

.status-ok {
  color: var(--color-positive-text);
  font-weight: 600;
}

.warn {
  color: var(--color-negative-text); /* alias de --color-negative, ver tokens.css */
  font-size: 11px;
  font-weight: 600;
  min-height: 14px;
  margin: var(--space-1) 0;
}

/* Neutral recolor of .warn (item 10): color-only override, so glyph metrics and line count stay
   identical to .warn in both states (no layout shift) -- used before the user has touched any
   contract field, when "preço a termo indisponível" is an expected state, not a warning yet. */
.warn--neutral {
  color: var(--color-label);
}

.contract-fields {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 var(--space-2);
  /* Item 6: the fields now come AFTER the heatmap -- breathing room against the grid above.
     10px (was space-3) -- part of the ~54px lift, see .detail-head note. */
  margin-top: 10px;
}

/* Item 21: warning block (rate floor + price unavailable) right above the Confirmar button, with
   two .warn lines' worth of min-height (14px each + margins) reserved -- messages appearing or
   vanishing must not move the button. 38px still fits both lines (14+8+14=36); trimmed from 44 as
   part of the ~54px lift, see .detail-head note. */
.contract-warns {
  min-height: 38px;
}

/* Reserved height, same reasoning as .contract-warns (item 21). The 06/08/2026 note that used to
   live here ("#cf-status is the LAST element, so a message pushes nothing") stopped being true on
   07/08/2026, when the panel was reordered and the heatmap became the last child: every status
   message shoved the heatmap down and tipped the column into a scrollbar (user report, 11/08/2026).
   34px = two 16.8px lines, measured against the real strings at this panel's 385px content width --
   only the 409 "price changed" warning wraps; every other message is a single line. */
#cf-status {
  /* The margin-bottom is the floor of the elastic gap: `.heatmap-head { margin-top: auto }` reaches
     zero on a short window, and this 8px is what keeps the heatmap off the button block. */
  margin: 2px 0 8px;
  min-height: 34px;
}

/* Painel Negociação: nasce aberto (igual ao docs/_legado/index.html#detail-panel real, que carrega
   com a classe "open" já aplicada -- "nasce recolhido" só descreve o estado default do CSS, nunca
   alcançado no arquivo congelado) e colapsa pra width:0 ao fechar, devolvendo o espaço ao ranking
   (.panel-ranking é flex:1 em layout.css). */
/* Elastic-height column (11/08/2026). The panel no longer has a fixed content height (794px, which
   only fit at innerHeight >= 888px) and adapts to the window instead: the working block (ticker,
   score, fields, warnings, button) packs at the top, the heatmap anchors to the bottom via
   `.heatmap-head { margin-top: auto }`, and the space between them is the elastic slack -- it grows
   on a tall window and shrinks down to `#cf-status`'s margin-bottom on a short one, moving the
   heatmap up toward the button without ever overlapping it. Once the slack is gone the heatmap
   itself compresses (see #heatmap-wrap). Only after that does .panel's `overflow-y: auto`
   (layout.css) come back into play. Motivation: browser zoom shrinks the layout viewport in CSS px,
   which reproduced the very same scrollbar. */
.panel-detail {
  display: flex;
  flex-direction: column;
  transition: width 0.25s ease, padding 0.25s ease, border-width 0.25s ease;
}

/* Only the heatmap may shrink. Without this the other children inherit `flex-shrink: 1`, and TWO of
   them declare `min-height` -- `.contract-warns` (38px) and `#cf-status` (34px). Declaring
   `min-height` switches off the flex item's automatic minimum size, so on a short window they would
   shrink DOWN TO that value and clip their text instead of growing, with the Confirmar button
   (which comes after them and has an opaque background) painting over the overflow. With the app's
   real strings the overflow is 1px; a slightly longer warning is all it takes to turn it into the
   collision. Defensive rule: the cost is zero and the failure would be silent. */
.panel-detail > *:not(#heatmap-wrap) {
  flex-shrink: 0;
}

.panel-detail:not(.open) {
  width: 0;
  padding: 0;
  border-width: 0;
  overflow: hidden;
}

/* Faixa de cabeçalho com borda inferior, igual à legacy (.card-header/.detail-head,
   docs/_legado/index.html:271-277,434-441) -- separa visualmente o título do corpo do painel. */
.panel-header,
.detail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: calc(-1 * var(--space-3)) calc(-1 * var(--space-3)) var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

/* .panel-ranking não tem padding próprio (layout.css) -- não há mais nada pra este sangrar até, e
   sem zerar a margem ele passaria da borda do card. #ranking-wrap encosta direto embaixo, sem gap
   extra, igual ao #table-wrap da legacy (zero margem depois do border-bottom do .card-header). */
.panel-ranking .panel-header {
  margin: 0;
}

.panel-header h2,
.detail-head h2 {
  margin: 0;
}

/* User request (06/08/2026): lift the Confirmar button so it lands inside the visible panel on
   mid-height screens without scrolling. The space comes from spots that are imperceptible alone:
   the small trims below (.score-row/.contract-fields/.contract-warns/.panel-detail .field), the
   empty #cf-status reservation under the button, and the panel's own bottom padding -- NOT from
   the .detail-head header, which keeps its original proportions (explicit user preference). */
.panel-detail .field {
  margin-bottom: 6px; /* -2px vs generic .field; ×5 field rows in this panel */
}

.panel-detail {
  padding-bottom: 8px; /* dead space under the last element (the heatmap); was the .panel 16px */
}

/* Item 1: botão de reabertura do painel Negociação, visível só quando o painel está fechado --
   mesma linguagem discreta do .how-to-btn (borda fina, fundo transparente, 12px). */
.btn-reopen {
  padding: 5px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--color-label);
  font-size: 12px;
  cursor: pointer;
}

.btn-reopen:hover {
  color: var(--color-header);
}

/* Item 3: chip discreto com os parâmetros do último cálculo, no header do ranking. margin-left
   aproxima do h2 (.panel-header é space-between); margin-right: auto mantém o #detail-reopen (item
   1) ancorado na borda direita do header, em vez de space-between espalhar os três igualmente. */
.rank-params-chip {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-label);
  border: 1px solid var(--color-border);
  padding: 2px 8px;
  border-radius: var(--radius);
  margin-left: var(--space-2);
  margin-right: auto;
  white-space: nowrap; /* "90 dias · R$ 100.000 — desatualizado" must never wrap inside the chip */
  flex-shrink: 0;
}

/* Estado "desatualizado" (ranking calculado com parâmetros diferentes dos atuais) -- âmbar, não
   vermelho: é um aviso de idade, não um erro (mesmo raciocínio do item 12). */
.rank-params-chip.stale {
  color: var(--color-warning);
  border-color: var(--color-warning);
  font-weight: 600;
}

/* Barra de filtros da tabela (17/08/2026), na convenção do DataTables: rótulo + campo acima da tabela,
   alinhados à direita.
   A primeira versão punha o campo no .panel-header e o estilizava como .rank-params-chip. Ficou ruim
   por quatro motivos que o usuário apontou de uma vez: parecia chip de status e não input, era pequeno
   demais (11ch/11px), o `type="search"` renderizava com afordâncias nativas destoantes, e o cabeçalho
   já estava lotado. Linha própria resolve os quatro e deixa espaço para outros filtros. */
/* Rótulo do filtro, no cabeçalho do ranking. Tom de rótulo, não de chip: quem lê tem de entender que o
   que vem depois é um campo, e não mais um indicador. */
.table-filter-label {
  font-size: 12px;
  color: var(--color-label);
  margin-left: var(--space-2);
  flex-shrink: 0;
}

/* Métricas do input do formulário (.field input: mono, borda de 1px), não as do chip -- é o mesmo gesto
   de digitar que o broker faz nos campos de contrato, e deve parecer o mesmo controle. O padding é
   menor que o dos campos do contrato (4px contra 6px) porque aqui ele divide a linha com o botão de
   Negociação, e a altura do cabeçalho não pode crescer -- foi o espaçamento extra que condenou a versão
   em barra própria. Largura em `ch` com folga para ver o que se digitou. */
.table-filter-input {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--color-text);
  background: var(--color-panel);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 4px 8px;
  margin-left: 6px;
  width: 14ch;
  flex-shrink: 0;
}

.table-filter-input:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 1px;
}

/* Mensagem de filtro sem resultado. Fica fora de #ranking-wrap (ver index.html), então não herda a
   centralização que o :has() aplica a um .hint solitário lá dentro -- daí o padding próprio. */
.ranking-filter-empty {
  padding: var(--space-3) var(--space-4);
  margin: 0;
}

/* Item 11: color-scale legend, reused in the ranking header and above the heatmap -- the gradient
   itself is painted via CSSOM by legend.js (paintLegends()), never as style="" in this markup. */
.score-legend {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-ui); /* overrides the mono inherited from h2.heatmap-head in that instance */
  font-size: 9px;
  color: var(--color-label);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.score-legend-bar {
  width: 72px;
  height: 6px;
  flex-shrink: 0; /* same reasoning as .score-bar-bg below -- don't let a cramped header squeeze it */
  border-radius: 3px;
}

/* Ranking header: chip's margin-right:auto (above) already pushes the legend + #detail-reopen to
   the right edge together -- this just keeps a gap between the two when the button isn't hidden. */
.panel-header .score-legend {
  margin-right: var(--space-2);
}

/* The chip is hidden until a ranking is calculated (item 3) -- without its margin-right:auto in the
   flex flow, justify-content:space-between would center the legend instead of anchoring it right.
   Only kicks in while the chip is absent; once it's shown, its own auto margin does the job. */
.panel-header:has(#rank-params-chip[hidden]) .score-legend {
  margin-left: auto;
}

/* Heatmap header: h2.heatmap-head is itself the flex row (text + ticker); margin-left:auto pushes
   the legend to the far right of that row, next to the ticker. */
.heatmap-head .score-legend {
  margin-left: auto;
}

.detail-close {
  border: none;
  background: none;
  font-size: 18px;
  line-height: 1;
  color: var(--color-label);
  cursor: pointer;
  padding: 2px 6px;
}

.detail-close:hover {
  color: var(--color-text);
}

/* Fundo cinza-claro nos campos do painel Negociação -- igual a .cf-input/.ticker-input-wrap input
   da legacy (wwwroot/legacy/index.html:196-206,462-465,530-534), que nunca ficam brancos em
   repouso, só ao focar. */
.panel-detail .field input {
  background: var(--color-bg);
}

.panel-detail .field input:focus {
  background: var(--color-panel);
  border-color: var(--color-primary);
}

.ticker-input-wrap {
  display: flex;
  gap: var(--space-2);
}

.ticker-input-wrap input {
  flex: 1;
  width: auto;
}

.btn-ok {
  padding: 6px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--color-text);
  font-weight: 600;
  cursor: pointer;
}

.btn-ok:hover {
  background: var(--color-bg);
}

/* Item 5: positions the edge shadows OUTSIDE the scrollport -- a ::before/::after directly on
   #ranking-wrap would scroll along with the content. The .scroll-left/.scroll-right classes are
   toggled by ranking.js (updateScrollShadows) whenever content is cut off on that side; z-index 2
   sits above the sticky thead (z-index 1). The background-attachment:local technique was rejected
   because the sticky navy thead and the hover/selected row backgrounds paint over wrap-level
   background shadows. */
.ranking-scroll-frame {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.ranking-scroll-frame::before,
.ranking-scroll-frame::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 14px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
  z-index: 2;
}

.ranking-scroll-frame::before {
  left: 0;
  background: linear-gradient(to right, color-mix(in srgb, var(--color-header) 18%, transparent), transparent);
}

.ranking-scroll-frame::after {
  right: 0;
  background: linear-gradient(to left, color-mix(in srgb, var(--color-header) 18%, transparent), transparent);
}

.ranking-scroll-frame.scroll-left::before {
  opacity: 1;
}

.ranking-scroll-frame.scroll-right::after {
  opacity: 1;
}

/* Item 19: a lone hint (initial empty state, "Calculando…", error, "Nenhum resultado.") gets
   centered in the card instead of sitting at the top-left of a mostly-blank area. Scoped with
   :has() so the normal <table> case (never a lone child) keeps its default block layout -- only
   the wrapper's layout mode changes, ranking.js keeps writing plain <p class="hint">...</p> via
   innerHTML for those states and never needs to know .empty-state/this rule exists. */
#ranking-wrap:has(> .hint:only-child),
#ranking-wrap:has(> .empty-state:only-child) {
  display: flex;
  align-items: center;
  justify-content: center;
}

#ranking-wrap > .hint:only-child {
  text-align: center;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  text-align: center;
}

.empty-state .hint-secondary {
  font-size: 11px;
}

/* Scrollport próprio, sem padding -- ver nota em .panel-ranking (layout.css). */
#ranking-wrap {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* Item 5: column tiers -- .panel-ranking is the container (layout.css). Under 900px of card width
   the tier-3 columns (Ret D-1, Ret 12m) hide; under 700px the tier-2 ones (Ret 30d, Ret 90d, Vol)
   hide too. With the Negociação panel open at 1366 the card has ~540px (tier 1 only); closed,
   ~990px (all columns). #ranking-wrap's overflow-x stays as the safety net, with the
   .ranking-scroll-frame shadows above signalling cut-off content. */
@container ranking (max-width: 900px) {
  #ranking-wrap .col-t3 {
    display: none;
  }
}

@container ranking (max-width: 700px) {
  #ranking-wrap .col-t2 {
    display: none;
  }

  /* Header space is the scarcest resource at this width: title + params chip + filtro + reopen
     button don't fit on one line (~540px card with the Negociação panel open at 1366).
     Até 17/08/2026 quem cedia aqui era a legenda de cor, que ocupava este lugar. Ela saiu do
     cabeçalho (o filtro tomou a posição), e o que cede agora é o RÓTULO do filtro, não o campo:
     esconder um rótulo custa uma dica: esconder o campo custaria a função. O placeholder não
     substitui o rótulo em largura normal, mas aqui o `aria-label` do <label for> continua ligado ao
     input para quem usa leitor de tela, mesmo com o texto visualmente fora. */
  .panel-ranking .panel-header .table-filter-label {
    display: none;
  }

  .panel-ranking .panel-header .table-filter-input {
    width: 9ch;
  }
}

/* Item 2: recálculo em voo com tabela já na tela -- opacidade reduzida em vez de trocar o innerHTML
   pelo hint "Calculando…", pra não perder o contexto durante a espera. */
#ranking-wrap.loading {
  opacity: 0.5;
  pointer-events: none;
}

#ranking-wrap table th,
#ranking-wrap table td {
  padding: 6px 12px;
  border-bottom: 1px solid var(--color-border);
  text-align: right;
  font-family: var(--font-mono);
  white-space: nowrap;
}

.rank-num {
  font-size: 11px;
  font-weight: 600;
  color: var(--color-label);
  width: 28px;
}

.ticker-name {
  font-weight: 700;
  font-size: 13px;
  color: var(--color-header);
  letter-spacing: 0.5px;
}

.metric-val {
  font-size: 12px;
  color: var(--color-label);
}

/* Aviso de atraso e horário da última cotação, no cabeçalho do ranking. Não é erro nem alerta -- é a
   condição sob a qual toda cotação desta tela existe, então tem a MESMA aparência do chip de
   parâmetros ("90 dias · R$ 100.000"), a pedido do usuário (17/08/2026).
   A aparência vem de reusar `.rank-params-chip` no próprio elemento, não de copiar as regras: se o
   chip mudar, este acompanha. Aqui só as margens diferem.
   Só `margin-right: auto`, e `margin-left: 0` explícito para não herdar o do chip. Com duas margens
   automáticas no header (a deste elemento e a do `.rank-params-chip`), a folga se divide em duas
   partes iguais e o aviso fica com o MESMO respiro dos dois lados. Pôr `auto` também à esquerda
   parece simétrico e não é: as duas automáticas vizinhas se somam, e o vão do lado do chip sai o
   dobro do da legenda (medido: 164px contra 82px).
   Encostar na legenda era o defeito relatado, e a causa era um comentário CSS quebrado neste bloco,
   que invalidava a regra de margem inteira -- não cache do navegador, como eu tinha suposto. */
.quote-delay-note {
  margin-left: 0;
  margin-right: auto;
}

/* Idade da cotação é aviso, não erro (item 12): amarelo, não o vermelho padrão de .warn. */
.warn--stale {
  color: var(--color-warning);
}

#ranking-wrap table th:first-child,
#ranking-wrap table td:first-child,
#ranking-wrap table th:nth-child(2),
#ranking-wrap table td:nth-child(2) {
  text-align: left;
}

#ranking-wrap table th {
  background: var(--color-header);
  color: rgb(255 255 255 / 75%);
  font-weight: 600;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  padding: 11px 14px;
  position: sticky;
  top: 0;
  /* Sem isso, linhas do tbody que passam por baixo durante o scroll pintam por cima do cabeçalho
     sticky -- thead vem antes no DOM, mas sem stacking context explícito quem pinta depois (tbody)
     vence visualmente onde as caixas se sobrepõem. */
  z-index: 1;
}

#ranking-wrap table tbody tr {
  cursor: pointer;
}

#ranking-wrap table tbody td {
  border-bottom-color: #f0f4f8;
}

#ranking-wrap table tbody tr:hover {
  background: #f5f9ff;
}

/* Item 24: the 12% tint alone was too subtle next to the hover background (#f5f9ff) -- an inset
   left border accent makes the selected row identifiable at a glance. focus-visible below still
   wins on tab/keyboard focus since its own outline is drawn on top, and outline/box-shadow don't
   compete (outline paints outside the border box, box-shadow's inset variant paints inside it). */
#ranking-wrap table tbody tr.selected {
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-panel));
  box-shadow: inset 3px 0 0 var(--color-primary);
}

/* Item 13: linhas focadas via teclado (tabindex="0") ganham o mesmo fundo do highlight .selected,
   mais um outline próprio -- outline-offset negativo pra não vazar pra fora da célula (a linha não
   tem borda própria pra "encaixar" um outline positivo). */
#ranking-wrap table tbody tr:focus-visible {
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-panel));
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

#ranking-wrap table th.sortable {
  cursor: pointer;
  user-select: none;
}

#ranking-wrap table th.sortable:hover {
  color: #fff;
}

#ranking-wrap table th.sort-asc::after {
  content: ' \25B2';
  font-size: 9px;
}

#ranking-wrap table th.sort-desc::after {
  content: ' \25BC';
  font-size: 9px;
}

/* Item 23: sortable headers only revealed the affordance on hover -- a discreet neutral "up-down"
   marker in the resting state hints they're clickable. Excluded once a sort direction is active
   (its own ::after above already covers that state). */
#ranking-wrap table th.sortable:not(.sort-asc):not(.sort-desc)::after {
  content: ' \2195';
  font-size: 9px;
  opacity: 0.45;
}

/* Destaque top-3 do ranking -- hex literal ouro/prata/bronze da legacy (docs/_legado/index.html:320-322),
   pivô deliberado (05/08/2026) de volta à metáfora de medalha que a F7 tinha rejeitado. */
.rank-badge {
  font-weight: 700;
}

.rank-1 {
  color: #b8860b;
}

.rank-2 {
  color: #607d8b;
}

.rank-3 {
  color: #8d4e27;
}

/* Botão "Calcular Ranking" pulsando quando prazo/financeiro mudam depois de um ranking já
   calculado (docs/_legado/index.html:367-372, markDirty()) -- avisa que a tabela na tela não
   reflete mais os parâmetros atuais. */
@keyframes btn-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgb(29 180 232 / 50%); }
  60% { box-shadow: 0 0 0 8px rgb(29 180 232 / 0%); }
}

.btn-primary.dirty {
  animation: btn-pulse 1.6s ease-in-out infinite;
}

.heatmap-scroll {
  overflow-x: auto; /* contém o overflow no próprio widget -- sem isso, um filho mais largo que
                       o painel força o .panel inteiro (que já tem overflow-y: auto) a rolar
                       também no eixo x, arrastando os campos acima do heatmap junto */
  /* Middle link of the definite-height chain that lets `table { height: 100% }` resolve (see
     #heatmap-wrap) -- without it the table's percentage would fall back to `auto` and the grid
     would not compress on a short window. */
  height: 100%;
}

#heatmap-wrap table {
  table-layout: fixed;
  /* width:100%, igual à legacy (table.heatmap, wwwroot/legacy/index.html:483) -- estica as células
     pra ocupar o painel todo (430px) em vez de ficarem no tamanho mínimo (46px/68px, ~344px), que
     sobrava um vão em branco à direita não presente na legacy. table-layout:fixed continua
     respeitando os width mínimos abaixo se o painel ficar mais estreito que 344px (responsivo <1024px),
     então o scroll horizontal do .heatmap-scroll continua funcionando como rede de segurança. */
  width: 100%;
  /* Makes the rows share the block's available height instead of each sitting at its intrinsic
     height -- this is what compresses the grid when the window shrinks (see #heatmap-wrap). */
  height: 100%;
}

/* Estado vazio ("selecione um ticker") centralizado e com respiro vertical, igual a .heatmap-hint da
   legacy (wwwroot/legacy/index.html:478-481) -- o .hint genérico (usado em vários outros lugares da
   tela) não centraliza nem tem esse padding. */
#heatmap-wrap .hint {
  text-align: center;
  padding: 48px 0;
}

/* Vertical padding goes into clamp(vh) together with the border and the font below: the three of
   them are the row's height FLOOR (font + padding + border), and without shrinking that floor the
   grid stops compressing before it fits. The ceilings are the design values and are only left below
   ~865px of viewport, so nothing changes visually on a normal screen. vh rather than a media query
   because browser zoom shrinks the layout viewport in CSS px -- the case the user reported. */
#heatmap-wrap table th,
#heatmap-wrap table td {
  padding: clamp(1px, 0.46vh, 4px) 3px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: clamp(8px, 1.16vh, 10px);
  width: 46px;
}

/* Blocos separados, não grade fechada -- igual à legacy (wwwroot/legacy/index.html:496-500):
   borda grossa na cor do painel funciona como respiro entre células, não como linha de grade. */
#heatmap-wrap table td {
  /* FLOOR, not the design height: in CSS tables `height` on a td counts as a minimum, and it is
     `table { height: 100% }` that stretches the rows up to #heatmap-wrap's 217px ceiling. The
     design's 32px still holds on a tall window -- 20px is only how far a row may compress. */
  height: 20px;
  /* The border is breathing room between blocks, not a grid line (see the note above) -- it goes
     into the same clamp(vh) as the padding for the same reason: it weighs 2x on the row's floor. */
  border: clamp(1px, 0.35vh, 3px) solid var(--color-panel);
  border-radius: 3px;
}

/* Números legíveis sobre as cores saturadas do farol: 1px acima do resto da tabela e semibold.
   A COR da tinta não é daqui -- é por célula, computada por contraste real contra o fundo
   (color.js#scoreInk, aplicada via CSSOM pelo heatmap.js). O fundo das células não muda. */
#heatmap-wrap table td.heatmap-cell {
  /* The vh multipliers for padding/border/font are calibrated to leave the design value only below
     ~865px of viewport -- exactly where the row actually starts compressing. Above that the grid is
     pixel for pixel the one from before. */
  font-size: clamp(9px, 1.27vh, 11px);
  font-weight: 600;
}

#heatmap-wrap table th:first-child,
#heatmap-wrap table td:first-child {
  width: 68px;
  text-align: right;
  padding-right: 6px;
  /* Fixa a coluna de rótulo (financeiro) enquanto só os dados rolam -- sem isso, rolar pra ver
     360d empurra o rótulo pra fora e a linha vira número sem contexto de qual financeiro é. */
  position: sticky;
  left: 0;
  background: var(--color-panel);
  z-index: 1;
}

.how-to-btn {
  padding: 5px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: transparent;
  color: var(--color-label);
  font-size: 12px;
  cursor: pointer;
}

.how-to-btn:hover {
  color: var(--color-header);
  border-color: var(--color-header-hover);
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgb(13 31 60 / 50%); /* var(--color-header) com alfa -- igual à legacy */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  z-index: 10;
}

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

.modal-box {
  position: relative;
  background: var(--color-panel);
  border-radius: var(--radius);
  max-width: 640px;
  max-height: 85vh;
  overflow-y: auto;
  padding: var(--space-4);
}

.modal-box h2 {
  margin: 0 0 var(--space-3);
  padding-right: var(--space-4); /* não fica embaixo do botão de fechar */
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 700;
  color: var(--color-header);
  text-transform: none;
  letter-spacing: normal;
}

.modal-section {
  margin-bottom: var(--space-3);
}

.modal-section h3 {
  margin: 0 0 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--color-primary);
}

.modal-section p {
  margin: 0 0 8px;
  line-height: 1.5;
  text-align: justify; /* user request (06/08/2026): justified body text in "Como usar" */
}

.modal-close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  border: none;
  background: none;
  font-size: 20px;
  line-height: 1;
  color: var(--color-label);
  cursor: pointer;
}

#score-wrap .score-value {
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 700;
}

/* Item 7: the score gets its own row right below the ticker (label left, value right) -- replaces
   .detail-summary-row, which squeezed two <h2>s + a 20px value into a ~385px line and wrapped
   "CHANCE LIQ. ANTEC." mid-title. "// HEATMAP <ticker>" is a standalone header again. */
.score-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  /* 2px, not space-2: the panel became a flex container (see .panel-detail) and margins stopped
     collapsing. The 6px margin-bottom of the .field above, which used to collapse with these 8px
     into a single 8px, now ADDS UP -- 6+2 gives back exactly the previous spacing without costing
     +6px of height in a panel whose slack is fought over in 16px increments. */
  margin-top: 2px;
}

.score-row h2 {
  margin: 0;
}

.score-row #score-wrap {
  text-align: right;
}

/* Só o ticker acima do grid, grande -- o .heatmap-title do PoC (docs/_legado/index.html:470-473,
   20px mono bold navy), sem rótulo "Heatmap" nem legenda. É um <div>, não <h2>, de propósito:
   escapa do uppercase e do prefixo '// ' que .panel h2 impõe. O rodapé fica abaixo do botão
   Confirmar (hierarquia do PoC, 07/08/2026); a margem separa do bloco de ação. */
.heatmap-head {
  /* auto, not space-4: this is the hinge of the elastic layout (see .panel-detail). It pushes the
     heatmap block to the bottom of the panel and absorbs all the slack -- on a tall window the gap
     grows, on a short one it reaches zero and the heatmap rises to #cf-status's 8px floor. */
  margin-top: auto;
  margin-bottom: 4px;
}

/* 217px (25px of thead + 6 rows of 32px) used to be `min-height` -- a RESERVATION, so the empty
   state would not make the scrollbar jump when the response landed. It became a target height on
   11/08/2026 so the grid can both hold its design size and compress: `flex-shrink: 1` lets the
   block shrink on a short window, with `table { height: 100% }` redistributing the available height
   across the rows. The old reservation is no longer needed because the heatmap is anchored to the
   bottom of the panel (`.heatmap-head { margin-top: auto }`): a shorter empty state only widens the
   gap above it, and nothing above moves. Do NOT put `min-height` back -- it locks the compression. */
#heatmap-wrap {
  /* Neither `min-height: 0` nor a non-visible `overflow`, on purpose -- BOTH zero out the flex
     item's automatic minimum size (the second one by spec: a flex item with non-visible overflow
     has an automatic minimum of zero). With either of them the block shrank past the table's
     content height and the grid was CLIPPED -- measured: a 49px block holding a 143px table.
     Letting the default `min-height: auto` do its job stops the compression at the table's real
     minimum and hands the overflow back to the panel's scrollbar: hiding data is worse than a
     scrollbar (user decision, 11/08/2026). */
  flex: 0 1 auto;
  /* `height`, not `max-height`: it has to be a TARGET, not a ceiling. As a ceiling the block stopped
     at the table's content height (183px) and rows sat at 26px even with space to spare -- the
     design's 32px vanished on a tall window. As a declared height it holds on a tall window and
     `flex-shrink: 1` reduces it on a short one, down to the content minimum.
     217px = 25px of thead + 6 rows of 32px. Those 32px are the ceiling of the `td` clamp (a row only
     gets there because `table { height: 100% }` stretches it) -- change one, change the other. */
  height: 217px;
}

/* Below layout.css's breakpoint the column stops being viewport-height-bound (`.layout` switches to
   flex-direction: column) and the panel grows with its content. The whole elastic machine above
   loses its meaning here: with no bounded height there is no slack for the gap to absorb, and the
   clamp(vh) values would shrink the grid according to the WINDOW height rather than the panel's
   actual space -- measured at 900x700, the cell font dropped to 9px inside an 857px-wide panel with
   room to spare. The 1024px is duplicated from layout.css on purpose: component styling does not
   migrate over there. */
@media (max-width: 1024px) {
  .heatmap-head {
    margin-top: var(--space-4); /* `auto` resolveria para 0 num container de altura livre */
  }

  #heatmap-wrap table th,
  #heatmap-wrap table td {
    padding: 4px 3px;
    font-size: 10px;
  }

  #heatmap-wrap table td {
    height: 32px;
    border-width: 3px;
  }

  #heatmap-wrap table td.heatmap-cell {
    font-size: 11px;
  }
}

.heatmap-ticker {
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 700;
  text-transform: none;
  letter-spacing: normal;
  color: var(--color-header);
}

.score-bar-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  justify-content: flex-end;
}

.score-bar-bg {
  width: 60px;
  height: 5px;
  border-radius: 3px;
  background: var(--color-bg);
  overflow: hidden;
  flex-shrink: 0;
}

.score-bar-fill {
  height: 100%;
  border-radius: 3px;
}

.score-bar-val {
  font-weight: 700;
  min-width: 44px;
  text-align: right;
}

.modal-box--narrow {
  max-width: 420px;
}

.modal-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.modal-actions button {
  width: auto;
  flex: 1;
  margin-top: 0;
}

.confirm-summary {
  font-family: var(--font-mono);
  font-size: 13px;
}

.confirm-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 3px 0;
}

.confirm-label {
  color: var(--color-label);
}

.confirm-value {
  font-weight: 700;
  text-align: right;
}

.value-positive {
  color: var(--color-positive-text);
}

.value-negative {
  color: var(--color-negative-text);
}

/* Motivo "// rótulo" da legacy (.section-label, docs/_legado/index.html:118-129) -- todo <h2> de
   painel (sidebar, card-header do ranking, detail-head da negociação) ganha o prefixo; escopado a
   .panel pra não pegar os <h2> de modal, igual à legacy (.modal-title nunca tem o prefixo). */
.panel h2 {
  font-family: var(--font-mono);
  letter-spacing: 1.2px;
}

.panel h2::before {
  content: '// ';
  color: var(--color-primary);
}

/* ---- F11: portão de login, chip de sessão e administração ---- */

/* Reusa .modal-overlay/.modal-box, mas com fundo OPACO: o de trás não é conteúdo que o usuário possa
   ver por baixo enquanto decide, é a aplicação que ele ainda não tem direito de enxergar.
   ------------------------------------------------------------------------------------------------
   RETORNO PONTUAL AO BRAND GUIDE (13/08/2026, `docs/Giant steps brand guide.html`).
   O pivô de 05/08/2026 (`docs/DECISOES.md`) tirou o guia da tela real e adotou a identidade da
   legacy. Isto NÃO reverte aquilo: só a tela de login volta ao guia, e por uma razão específica --
   ela é a **capa** do produto, e "capa" é a única coisa para a qual o guia dá uma receita fechada
   (o Hero dele). Da porta para dentro, a aplicação segue a legacy.
   As cores do guia (#003250, #35A3DC) são vizinhas das da app (#0d1f3c, #1db4e8), mas não são as
   mesmas -- por isso vivem em custom properties LOCAIS deste bloco, e não em tokens.css: a
   divergência é deliberada, tem escopo de uma tela, e não deve vazar para o resto por herança. */
.login-gate {
  --brand-navy-deep: #00172a;
  --brand-navy: #003250;
  --brand-navy-lit: #004a72;
  --brand-ceu: #35a3dc;
  --brand-ceu-deep: #2a8fc4;

  /* §05 "Alternância de Fundos", linha Hero. O guia proíbe cor sólida plana em fundo de seção. */
  background: linear-gradient(160deg, var(--brand-navy-deep) 0%, var(--brand-navy) 55%, var(--brand-navy-lit) 100%);

  /* O overlay só centra o palco; quem posiciona manchete e cartão é o .login-stage abaixo. */
  padding: 0;
}

/* O `.hero-inner` do guia, traduzido: largura máxima de 1200px, centrado, com respiro lateral. É ele
   que encosta o texto na esquerda -- sem o palco, os dois blocos ficam colados no meio da tela e a
   manchete perde o alinhamento que o hero do guia tem.
   `align-items: center` alinha cartão e manchete pelo meio; `space-between` empurra um para cada borda. */
.login-stage {
  position: relative; /* mesma razão do cartão: ficar acima dos glows e da malha */
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 64px;
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-4) 48px;
}

/* As duas camadas do `.hero` do guia (linhas 41-53 do HTML), na mesma ordem: primeiro os glows
   radiais, depois a malha por cima. Pseudo-elementos porque `background` já é o degradê acima e
   empilhar tudo numa propriedade só tornaria qualquer ajuste futuro ilegível. */
.login-gate::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 80% 60% at 80% 20%, rgb(53 163 220 / 18%) 0%, transparent 60%),
    radial-gradient(ellipse 50% 40% at 10% 80%, rgb(34 190 198 / 12%) 0%, transparent 50%);
  pointer-events: none;
}

/* A malha de 64px -- a textura que dá o ar de gestora quantitativa em vez de "tela escura". A 6% ela
   é quase subliminar de perto e só se percebe no conjunto, que é o ponto. */
.login-gate::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgb(53 163 220 / 6%) 1px, transparent 1px),
    linear-gradient(90deg, rgb(53 163 220 / 6%) 1px, transparent 1px);
  background-size: 64px 64px;
  pointer-events: none;
}

/* Sem isto o cartão fica ATRÁS dos dois pseudo-elementos: eles são filhos do overlay e vêm depois
   dele na ordem de pintura. `position: relative` é o que cria o contexto para o z-index valer. */
.login-box {
  position: relative;
  z-index: 1;
  /* Largura DECLARADA, e não herdada do conteúdo (13/08/2026). O cartão nunca teve `width`: `.modal-box`
     não define uma, então ele sempre foi dimensionado pelo texto mais largo lá dentro -- que era a linha
     `// Sistema de Negociação de Termos`. Quando ela saiu do cartão, ele encolheu de ~313px para ~248px
     sozinho, e o campo de e-mail passou a cortar endereços de tamanho normal.
     Os 420px vêm do `.modal-box--narrow` que a marcação já pedia e que o `max-width: 380px` daqui
     mascarava. Com eles sobram ~354px úteis no input: ~42 caracteres em mono de 14px. */
  flex-shrink: 0;
  width: 420px;
  max-width: 100%;
  text-align: center;
  /* Sombra "Nível 3 · Modais" do §04. Contra o fundo escuro é ela que faz o cartão flutuar em vez de
     parecer um recorte. O cartão segue BRANCO de propósito: os campos do projeto inteiro são
     desenhados para fundo claro, e um cartão escuro obrigaria a uma segunda escola de formulário
     (input, label, foco, placeholder) para uma tela só. */
  box-shadow: 0 24px 64px rgb(0 23 42 / 45%);
}

/* §04 "Botões · Primário": gradiente 135deg no azul céu, sombra colorida, e o lift de 2px no hover.
   Escopado em .login-box para não alcançar o "Calcular Ranking" da tela de negociação, que é navy e
   tem estados próprios (.dirty com pulso). */
.login-box .btn-primary {
  background: linear-gradient(135deg, var(--brand-ceu), var(--brand-ceu-deep));
  /* Texto NAVY, e não o branco que o `.btn-p` do guia usa. Divergência deliberada, e não a primeira:
     é exatamente a mesma correção que o `.btn-primary.dirty` já carrega (ver o comentário dele acima,
     e o item 3 de `docs/melhorias-de-UI-UX.md`) -- branco sobre o azul céu dá ~2,8:1, e este texto tem
     13px, tamanho que a WCAG AA cobra 4,5:1. O guia especifica o botão branco porque no material dele
     ele aparece sobre fundo escuro; num cartão claro a conta muda. */
  color: var(--color-header);
  box-shadow: 0 4px 14px rgb(53 163 220 / 30%);
  transition: transform 0.2s, box-shadow 0.2s;
}

.login-box .btn-primary:hover {
  background: linear-gradient(135deg, var(--brand-ceu), var(--brand-ceu-deep));
  box-shadow: 0 8px 24px rgb(53 163 220 / 40%);
  transform: translateY(-2px);
}

/* O guia pede o lift em todo elemento interativo, mas quem prefere menos movimento não deve levá-lo. */
@media (prefers-reduced-motion: reduce) {
  .login-box .btn-primary {
    transition: none;
  }

  .login-box .btn-primary:hover {
    transform: none;
  }
}

/* §02, "Label de Seção": Raleway 700 · 11px · #35A3DC · uppercase · ls 3px. Adotado só o TRACKING.
   A fonte fica Inter -- trocar só aqui quebraria a tipografia na passagem para a app. E a cor fica a
   do token, porque o #35A3DC do guia rende 2,8:1 sobre branco (o token dá 5,05:1) e este label tem
   10px: a especificação é para label sobre o fundo ESCURO do material institucional, onde aquele azul
   contrasta. Copiá-la para um cartão claro seria seguir a letra e errar o propósito. */
.login-box .field label {
  letter-spacing: 3px;
}

.login-brand {
  height: 34px;
  /* Era space-2 quando havia o `// Sistema de...` logo abaixo para separar o wordmark do formulário.
     Sem ele, o respiro tem de vir daqui, senão o logo encosta no primeiro campo. */
  margin-bottom: var(--space-4);
}

/* A manchete: o `<h1>` do hero do guia (Raleway 900, duas linhas, a segunda em degradê recortado no
   texto). Substitui o `// Sistema de Negociação de Termos` que ficava dentro do cartão.
   O `//` em mono NÃO desaparece do produto -- ele continua no header da aplicação, que é onde a pessoa
   cai um segundo depois de entrar. O que mudou é que a capa deixou de repetir a legenda do header. */
.login-hero {
  /* Largura suficiente para a eyebrow caber em UMA linha: em uppercase com 4px de tracking ela é o
     elemento mais largo do bloco, mais larga que a manchete. Medida no navegador, não estimada -- ver
     o breakpoint lá embaixo, que sai desta conta. */
  max-width: 600px;
}

/* `.hero-eyebrow` do guia: 11px, 600, uppercase, tracking 4px, azul céu, com o tracinho de 32px em
   degradê à esquerda -- inline, como lá, e não solto acima. O azul céu aqui é o uso CERTO da cor: sobre
   o fundo escuro ele contrasta (é sobre o cartão branco que ele falhava, e ali foi revertido). */
.login-eyebrow {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 28px;
  font-size: 11px;
  font-weight: 600;
  /* 3px, e não os 4px do guia: com 4 a frase renderiza 598px dentro de um limite de 600 -- dois pixels
     de folga, que qualquer diferença de rasterização entre máquinas transforma em quebra de linha. A
     alternativa era subir o limite e empurrar o breakpoint acima de 1240px, tirando telas de 1280 da
     composição em duas colunas. Um pixel de tracking custa menos que isso. */
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--brand-ceu);
}

.login-eyebrow::before {
  content: '';
  flex-shrink: 0;
  width: 32px;
  height: 2px;
  background: linear-gradient(90deg, var(--brand-ceu), #22bec6);
}

.login-headline {
  /* A faixa do guia é 72-88px (§02, "Hero H1 — Raleway 900 · 72–88px · ls -2px"; o CSS do hero dele usa
     88 fixo). O clamp fica dentro dela em vez de fixar um número: a 1440px dá os 88px do teto, e no
     ponto em que o layout vira coluna (1180px) ainda dá 72px, o piso da faixa -- ou seja, na composição
     em duas colunas a manchete nunca sai do que o guia especifica. Abaixo disso ela encolhe até 40px,
     que já é território de celular, onde 72px não caberia de jeito nenhum. */
  font-family: "Raleway", var(--font-ui);
  font-size: clamp(40px, 6.1vw, 88px);
  font-weight: 900;
  line-height: 0.95;
  letter-spacing: -2px;
  color: #fff;
  margin-bottom: 24px;
}

/* `background-clip: text` com a cor transparente -- o degradê do `.line2` do guia. Se algum navegador
   não suportar, o texto cai para o branco herdado acima, que é legível: degradação sem buraco. */
.login-headline-accent {
  display: block;
  background: linear-gradient(90deg, var(--brand-ceu), #22bec6);
  background-clip: text;
  color: transparent;
}

.login-lede {
  font-size: 15px;
  line-height: 1.7;
  color: rgb(255 255 255 / 62%);
}

/* Empilha antes de a manchete e o cartão começarem a se espremer. Abaixo disto a capa vira o que era
   antes -- uma coluna só, centrada --, e a manchete encolhe pelo clamp acima.
   `flex-direction: column` EXPLÍCITO, e não confiando no `flex-wrap` acima: wrap só quebra quando os
   itens não cabem, e a manchete encolhe antes disso -- a 820px os dois ainda cabiam lado a lado, e o
   `text-align: center` daqui pintava texto centralizado numa composição que continuava em duas colunas.
   Visto na captura, não deduzido. */
/* O ponto onde a linha deixa de caber: manchete 600 + gap 64 + cartão 420 + 96 de padding lateral =
   1180. Abaixo disso o palco vira coluna, senão a eyebrow quebra em duas linhas (é ela, e não a
   manchete, o elemento mais largo do bloco) ou o cartão é espremido -- as duas piores que empilhar.
   O número saiu de medir a eyebrow renderizada, não da soma acima, que é só a checagem. */
@media (width <= 1180px) {
  .login-stage {
    flex-direction: column;
    /* `nowrap` obrigatório aqui: com `wrap`, uma coluna que não cabe na altura não vira rolagem -- vira
       DUAS COLUNAS. A 820x400 (janela baixa, formulário de troca de senha) a manchete e o cartão
       voltavam a ficar lado a lado, sobrepostos. Reproduzido antes de corrigir. */
    flex-wrap: nowrap;
    gap: 40px;

    /* Centrado quando sobra espaço, rolando a partir do topo quando não sobra. As duas margens `auto`
       abaixo é que fazem isso: o espaço livre se divide entre elas (= centrado) e some quando não há
       (= topo visível). `justify-content: center` sozinho cortaria o topo de forma INALCANÇÁVEL numa
       janela baixa -- o conteúdo transborda para fora do início do contêiner e a rolagem não chega lá. */
    justify-content: flex-start;
    overflow-y: auto;
    max-height: 100%;
  }

  /* `flex-shrink: 0` nos dois: sem isto o flex prefere ESMAGAR o cartão a deixar o contêiner rolar --
     medido, ele caía de 340px para 126px numa janela de 400px, cortando o próprio formulário por
     dentro enquanto a conta de altura fechava e nada parecia errado. Numa coluna, o certo é o
     conteúdo manter o tamanho e a rolagem aparecer. */
  /* 520px e não 460: sem o tracinho (escondido abaixo) a eyebrow ainda pede ~500px, e em 460 ela
     quebrava em duas linhas -- rótulo em caixa alta com tracking, centralizado e em duas linhas fica
     ruim de um jeito difícil de justificar. Medido a 1000px de viewport. */
  .login-hero {
    flex-shrink: 0;
    margin-top: auto;
    max-width: 520px;
    text-align: center;
  }

  /* Empilhado, a eyebrow centra junto com o resto -- e o tracinho sai: um traço de 32px pendurado à
     esquerda de um texto centralizado fica torto. Ele é ornamento do alinhamento à esquerda. */
  .login-eyebrow {
    justify-content: center;
  }

  .login-eyebrow::before {
    display: none;
  }

  .login-box {
    flex-shrink: 0;
    margin-bottom: auto;
  }

}

/* Os campos do login ficam fora de .panel-detail, que é onde vive a única regra de :focus do projeto
   -- sem isto, o campo não daria nenhum retorno visual ao receber foco. */
.login-form {
  text-align: left;
}

.login-form .field input:focus {
  background: var(--color-panel);
  border-color: var(--color-primary);
  outline: none;
}

.login-hint {
  font-size: 12px;
  color: var(--color-label);
  margin-bottom: var(--space-3);
}

/* Altura reservada, como .contract-warns e #cf-status: a mensagem de erro aparece e some, e sem
   reservar o espaço o botão pularia sob o cursor. */
.login-status {
  min-height: 32px;
  margin-top: var(--space-2);
  font-size: 12px;
}

/* [hidden] precisa vencer o display abaixo -- sem esta regra o chip (e o botao Sair) aparecem no
   portao de login, porque `display: flex` sobrepoe o `[hidden] { display: none }` do proprio browser.
   Mesma armadilha vale para qualquer elemento que combine [hidden] com display explicito. */
.user-chip[hidden] {
  display: none;
}

.user-chip {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: var(--space-3);
  padding-left: var(--space-3);
  border-left: 1px solid var(--color-border);
}

.user-chip-name {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text);
}

.user-chip-role {
  font-family: var(--font-mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .5px;
  color: var(--color-panel);
  background: var(--color-header);
  border-radius: var(--radius);
  padding: 2px 6px;
}

.user-chip-logout {
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-label);
  border-radius: var(--radius);
  padding: 3px 8px;
  font-size: 11px;
  cursor: pointer;
}

.user-chip-logout:hover {
  color: var(--color-header);
  border-color: var(--color-header-hover);
}

/* Visibilidade da administração. Conveniência de tela, nunca autorização -- quem recusa é a política
   no servidor, e esconder um botão não impede ninguém de chamar o endpoint. */
.admin-only,
.admin-only-admin {
  display: none;
}

body.can-manage .admin-only {
  display: inline-block;
}

body.can-manage.is-admin .admin-only-admin {
  display: inline-block;
}

/* Abas da administração (13/08/2026). Visual de aba de verdade -- a selecionada é a que tem a régua da
   cor primária embaixo --, e não dois botões que mudam de cor: a diferença entre "estou aqui" e
   "clique aqui" tem de ser lida sem pensar. */
.admin-tabs {
  display: flex;
  gap: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-2);
  flex-shrink: 0;
}

.admin-tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px; /* a régua da aba cobre a borda do .admin-tabs, em vez de ficar acima dela */
  padding: 8px 2px;
  width: auto;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--color-label);
  cursor: pointer;
}

.admin-tab:hover {
  color: var(--color-header);
}

/* Pelo aria-selected, e não por uma classe `.active` paralela: o estado que o leitor de tela usa e o
   estado que o olho usa passam a ser o mesmo, e não há como atualizar um e esquecer o outro. */
.admin-tab[aria-selected="true"] {
  color: var(--color-header);
  border-bottom-color: var(--color-primary);
  font-weight: 600;
}

/* O painel é quem recebe a altura que sobra, e a passa para a tabela. Sem o `min-height: 0` o flex se
   recusa a encolher abaixo do conteúdo e a página volta a rolar -- que é o defeito que as abas vieram
   resolver. */
.admin-panel {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* Sem isto o painel escondido continua visível: `display: flex` acima vence o `[hidden]` do browser.
   É a mesma armadilha já documentada no chip do usuário, algumas dezenas de linhas acima. */
.admin-panel[hidden] {
  display: none;
}

.admin-actions {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  flex-shrink: 0;
}

.admin-actions button {
  width: auto;
  padding: 6px 12px;
}

.admin-form {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
}

.admin-form h3 {
  font-size: 13px;
  margin-bottom: var(--space-2);
  color: var(--color-header);
}

.admin-form .field input:focus,
.admin-form .field select:focus {
  background: var(--color-panel);
  border-color: var(--color-primary);
  outline: none;
}

.admin-form .field select {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-family: var(--font-ui);
  background: var(--color-panel);
}

/* A tabela é o único componente da F11 sem reuso: o CSS do ranking é todo escopado em #ranking-wrap,
   então uma classe própria é o caminho -- copiar aquelas regras por id criaria duas fontes de verdade
   para o mesmo visual. */
.data-table-wrap {
  /* Sem `max-height` fixo desde 13/08/2026: eram 45vh, e duas tabelas de 45vh mais o cabeçalho não
     cabem em 100vh -- a página inteira ganhava rolagem em vez da tabela. Quem limita a altura agora é o
     flex do .admin-panel, que entrega à tabela exatamente o que sobra da viewport.
     O `min-height` é o piso: numa janela baixa a tabela encolhe, e sem ele encolheria até sobrar só o
     cabeçalho. Por ora só usuarios.html usa esta classe, e sempre dentro daquela coluna flex -- quem
     trouxer a classe para fora dela herda o piso sem o flex que o justifica, e aí ele vira arbitrário. */
  min-height: 84px;
  overflow-y: auto;
  /* Sem overflow-x, um e-mail longo empurra a coluna de acoes para fora da caixa e os botoes ficam
     inalcancaveis -- reproduzido com os enderecos de teste, que sao longos de proposito. */
  overflow-x: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

.data-table thead th {
  position: sticky;
  top: 0;
  background: var(--color-header);
  color: var(--color-panel);
  text-align: left;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .6px;
  padding: 6px 8px;
  white-space: nowrap;
}

.data-table td {
  padding: 6px 8px;
  border-top: 1px solid var(--color-border);
  vertical-align: middle;
}

/* O e-mail e a unica coluna sem limite natural de tamanho. Sem isto ela sozinha decide a largura da
   tabela e empurra as acoes para fora do campo de visao. */
.data-table td:nth-child(2) {
  max-width: 260px;
  overflow-wrap: anywhere;
}

/* A coluna de acoes NAO e sticky. Chegou a ser, de quando a tabela ainda vivia num modal e estourava na
   horizontal -- em pagina propria ela cabe, e o `background` opaco que o sticky exige virava uma faixa
   branca por cima do resto da linha (hover e linha inativa incluidos). Se um dia a tabela voltar a
   rolar, o sticky volta junto com a cor de fundo herdada da linha, nao fixa. */

.data-table tbody tr:hover {
  background: var(--color-bg);
}

/* Desativado continua visível, e de propósito: revogar acesso é `is_active = false`, nunca DELETE --
   apagar orfanaria a atribuição das solicitações que a pessoa já enviou. */
.data-table tr.row-inactive td {
  color: var(--color-label);
  font-style: italic;
}

.row-actions {
  white-space: nowrap;
  text-align: right;
}

.row-action {
  border: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-header-hover);
  border-radius: var(--radius);
  padding: 2px 6px;
  margin-left: 4px;
  font-size: 11px;
  cursor: pointer;
}

.row-action:hover {
  border-color: var(--color-header-hover);
  background: var(--color-bg);
}

/* Desativar em vermelho e reativar em ambar: as duas sao a mesma acao em sentidos opostos, e a cor e o
   que as distingue de relance numa lista longa -- "Editar" e "Nova senha" ficam neutras porque nao
   mudam o acesso de ninguem.
   O vermelho e o --color-negative do tokens.css. O guia da casa reserva esse tom para "negativo
   financeiro", e aqui ele significa acao destrutiva -- reuso consciente: e a convencao mais forte que
   existe para um botao que tira o acesso de alguem, e nesta tela nao ha valor financeiro nenhum com que
   colidir. */
.row-action--danger {
  color: var(--color-negative);
  border-color: var(--color-negative);
}

.row-action--danger:hover {
  color: var(--color-panel);
  background: var(--color-negative);
  border-color: var(--color-negative);
}

.row-action--warn {
  color: var(--color-warning);
  border-color: var(--color-warning);
}

.row-action--warn:hover {
  color: var(--color-panel);
  background: var(--color-warning);
  border-color: var(--color-warning);
}

.row-action-none {
  color: var(--color-border);
}

/* Botão que se comporta como link -- usado no "Esqueci minha senha" e no "Voltar ao login". <button> e
   não <a href="#">: não é navegação, é ação, e um href falso quebra o histórico e o Ctrl+clique. */
.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--color-primary);
  font-size: 12px;
  text-decoration: underline;
  cursor: pointer;
}

.link-button:hover {
  color: var(--color-primary-hover);
}

.login-forgot {
  margin-top: var(--space-2);
  text-align: center;
}

/* Separador entre as duas tabelas da página de administração. */

