/* =====================================================================
   Accendo Hub — app shell (internal CRM)
   Layer stack (load order in fragments/head.html):
     1. fonts-brand.css    (@font-face → /fonts/geist, geist-mono, newsreader)
     2. vendor/bootstrap 5.3.8
     3. brand/accendo.css  (marketing design tokens + Bootstrap overrides)
     4. hub.css            (this file — app chrome only)

   Prefer Bootstrap utilities/components. Prefer brand tokens (var(--navy),
   var(--blue), var(--ink), …). Do not re-declare the brand palette here.
   ===================================================================== */

/*
 * CRM density: slightly smaller type than marketing, tables stay readable.
 * Override only inside .hub-body so brand/marketing tokens stay intact.
 *
 * Surface guideline (card-on-paper):
 *   --paper / gray = app canvas only (headers, toolbars, tab chrome, spacing).
 *   Working content (forms, tables, empty states, audit, stats) sits on white
 *   cards: .panel, .hub-table-panel, .stat-card, .kanban-card, etc.
 *   Do not leave body copy floating on the gray canvas.
 */
.hub-body {
  --hub-fs: 0.875rem;           /* 14px body — CRM density */
  --hub-fs-sm: 0.8125rem;       /* 13px secondary */
  --hub-fs-xs: 0.6875rem;       /* 11px labels / thead */
  --hub-fs-title: 1.25rem;      /* 20px page titles */
  --hub-lh: 1.45;
  /* Cell air: modest vertical rhythm so dense type still breathes */
  --hub-cell-y: 0.55rem;
  --hub-cell-x: 0.75rem;
  --hub-sidebar-w: 15rem;
  --hub-content-topbar-h: 3.25rem;

  /*
   * Semáforo (opportunity temperature): a true thermal gradient. The warm end burns
   * red→orange→amber (Caliente/Cálida/Fría), then crosses over to cool light→deep blue
   * (Helada/Glaciar). Each pair is a text/icon tone plus a light background tint for the
   * status chip and the lit gauge step.
   */
  --sem-hot: #c8452b;
  --sem-hot-tint: #fbe7e1;
  --sem-warm: #e07b39;
  --sem-warm-tint: #fbeede;
  /* Fría is the pivot of the ramp, where a deal stops being warm and starts being cold, so it is
     the one rung that is neither. A desaturated periwinkle marks that crossover and drops the
     amber this used to be: amber read as a warning the band does not mean, and at #eaa92e on its
     own tint it was also the least legible pair in the palette. */
  --sem-cool: #6f6bb0;
  --sem-cool-tint: #edecf8;
  --sem-cold: #4f9fcb;
  --sem-cold-tint: #e4f0f8;
  --sem-frozen: #2c6a95;
  --sem-frozen-tint: #dbe7f0;

  /* The temperature ramp, sun to snowflake. Its own three tokens rather than the sem-* above,
     because --sem-hot's saturated red belongs to the topbar counter and the chart alert, where
     red still means what it always meant. A band is a reading, never an alarm, so the hot end
     is a warm amber instead. */
  --sem-caliente: #d99a2b;
  --sem-caliente-tint: #faf1d8;
  --sem-fria: #4f9fcb;
  --sem-fria-tint: #e4f0f8;
  --sem-glaciar: #6f6bb0;
  --sem-glaciar-tint: #edecf8;

  /*
   * hub-chart.js reads every `--hub-chart-*` token by name off the chart element itself
   * (getComputedStyle), never a hardcoded hex — see that file's header. `ours`/`customer`/`watched`
   * are "Ciclo comercial"'s own colour keys, matching the three-way ball split
   * ValueStreamStep#ball() already carries; a future chart adds its own `--hub-chart-<key>` here
   * rather than hardcoding a colour in JS.
   */
  --hub-chart-default: var(--blue);
  --hub-chart-ours: var(--blue);
  --hub-chart-customer: var(--muted);
  --hub-chart-watched: var(--sem-cool);
  --hub-chart-alert: var(--sem-hot);
  --hub-chart-tick: var(--ink);
  --hub-chart-label: var(--ink);
  --hub-chart-sublabel: var(--muted);
  --hub-chart-value: var(--body);

  margin: 0;
  min-height: 100vh;
  background: var(--paper);
  color: var(--body);
  font-family: var(--font-sans);
  font-size: var(--hub-fs);
  line-height: var(--hub-lh);
}

/* Bootstrap text controls inherit CRM density */
.hub-body .btn {
  font-size: var(--hub-fs);
  line-height: 1.35;
  padding: 0.35rem 0.7rem;
}

.hub-body .btn-sm {
  font-size: var(--hub-fs-sm);
  padding: 0.2rem 0.45rem;
}

.hub-body .form-control,
.hub-body .form-select {
  font-size: var(--hub-fs);
  line-height: 1.4;
  padding: 0.4rem 0.55rem;
}

/* Reserve right-side room for Bootstrap's chevron on every hub select. The density
   overrides above use a padding shorthand that zeroes the space Bootstrap keeps for
   the chevron background, so option text ran under the arrow. Selects only (never
   text inputs); specificity beats the shorthand and the -sm/quote select rules. */
.hub-body select.form-select {
  padding-right: 1.9rem;
}

.hub-body .form-label,
.hub-body .hub-form label {
  font-size: var(--hub-fs-sm);
}

.hub-body .alert {
  font-size: var(--hub-fs);
  padding: 0.55rem 0.75rem;
}

.hub-body .badge {
  font-size: var(--hub-fs-xs);
  font-weight: 500;
}

/*
 * Item 2 (lane L): a thumb needs more room than a cursor. Measured at 375px against real data —
 * 51 undersized controls on Pipeline, 11 on Entradas, 8 on Empresas — every one of them a button,
 * a row action, a tab or a pagination link sized for a mouse.
 *
 * Scoped to (hover: none), a coarse pointer, never to a viewport width: a mouse on a resized
 * desktop window must stay exactly as dense as it is today, and a touch laptop at a wide viewport
 * still needs the floor. min-height/min-width are a separate property from the height/width each
 * control already sets elsewhere (kanban's own compact .hub-icon-btn among them), so this floors
 * every one of them without having to repeat or out-specificity any of those rules.
 *
 * Forty pixels is the floor the brief names. Icon-only controls (square already, nothing else on
 * the row to crowd) get forty four, since the extra four pixels cost nothing there.
 */
@media (hover: none) {
  .hub-body .btn,
  .hub-tab-item,
  .hub-user-chip {
    min-height: 40px;
  }

  .hub-nav-toggle,
  .hub-icon-btn,
  div.dt-container.dt-bootstrap5 .page-link {
    min-height: 44px;
    min-width: 44px;
  }
}

code,
kbd,
pre,
.font-mono {
  font-family: var(--font-mono);
}

/*
 * Money in a table cell. Two jobs, both learned from a real screen: never break the symbol away
 * from its amount onto a second line, and reserve enough width that a Colombian peso figure at its
 * usual size fits on one. COP is quoted in whole pesos, so a real opportunity runs to nine digits
 * plus two thousands separators plus the symbol ("$ 999.999.999"), which is what the min-width is
 * measured against. Tabular figures so the decimal columns line up down the page rather than
 * drifting with the digits above them.
 */
.hub-money-cell {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  min-width: 8.5rem;
}

.serif {
  font-family: var(--font-serif);
}

/* ── Shell: brand sidebar (full Accendo color) + content column ── */
.hub-shell {
  display: grid;
  grid-template-columns: 1fr;
  min-height: 100vh;
  min-height: 100dvh;
  padding: 0;
  max-width: 100%;
  align-items: stretch;
  background: var(--paper);
  transition: grid-template-columns 0.22s ease;
}

/*
 * Rail collapse. Two properties carry it and they cannot be kept in step: the rail slides on a
 * composited `transform`, while the grid track it has to stay flush with is a layout property on
 * the main thread. Same duration and same easing, different clocks, so any main-thread work during
 * the 220ms pulls them apart. The two rules below make that drift harmless in both directions
 * rather than pretending it will not happen.
 */
@media (min-width: 992px) {
  .hub-shell {
    grid-template-columns: var(--hub-sidebar-w) 1fr;
    overflow-x: clip; /* clip the slid-out rail; clip (not hidden) keeps sticky working */
    /* A flat band sized to the rail rather than a hard-stop gradient, because background-size
       animates and background-image does not. It used to be switched to `none` on collapse, which
       is not interpolatable: the navy vanished in one frame while everything else took 220ms. Now
       it shrinks on the same clock as the grid track, so when the rail slides out ahead of the
       layout, the seam it opens is navy instead of bare canvas. */
    background-image: linear-gradient(to right, var(--navy), var(--navy));
    background-repeat: no-repeat;
    background-size: var(--hub-sidebar-w) 100%;
    transition: grid-template-columns 0.22s ease, background-size 0.22s ease;
  }
  .hub-sidebar {
    min-width: var(--hub-sidebar-w);
    transition: transform 0.22s ease;
    /* Above the sticky topbar (1020). Drift the other way has the rail reach further right than
       the track, over the content column; with no z-index of its own the rail lost that race and
       slid underneath the content chrome, which is what made opening the menu look broken. Above
       it, the same overlap reads as a drawer passing over. Still below offcanvas and modals. */
    z-index: 1030;
  }
  /* Collapse the rail to reclaim width for dense views (quote wizard on small laptops). */
  .hub-shell.is-nav-collapsed {
    grid-template-columns: 0 1fr;
    background-size: 0 100%;
  }
  .hub-shell.is-nav-collapsed .hub-sidebar {
    transform: translateX(-100%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hub-shell,
  .hub-sidebar {
    transition: none;
  }
}

/* Brand rail — full Accendo navy */
.hub-sidebar {
  background: var(--navy);
  color: var(--white);
  padding: 1.1rem 0.85rem 1rem;
  flex-direction: column;
  gap: 1rem;
  align-self: stretch;
  min-height: 100vh;
  min-height: 100dvh;
  position: sticky;
  top: 0;
  max-height: 100vh;
  max-height: 100dvh;
  overflow-y: auto;
  border-right: none;
}

.hub-sidebar-brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  color: var(--white);
  padding: 0.25rem 0.4rem 0.75rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  margin-bottom: 0.25rem;
}

.hub-sidebar-brand:hover {
  color: var(--blue-soft);
}

.hub-sidebar-logo {
  height: 1.35rem;
  width: auto;
  display: block;
}

.hub-sidebar-hub {
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  opacity: 0.92;
}

.hub-sidebar-brand-compact {
  border-bottom: none;
  margin: 0;
  padding: 0;
}

.hub-sidebar-footer {
  margin-top: auto;
  padding: 0.75rem 0.4rem 0.15rem;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.hub-sidebar-footer-label {
  font-size: var(--hub-fs-xs);
  letter-spacing: 0.02em;
  text-transform: none;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.45);
  font-weight: 500;
}

.hub-nav {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  width: 100%;
}

/* Links + group summaries share one row style on the brand rail */
.hub-nav-on-brand .hub-nav-link {
  color: rgba(255, 255, 255, 0.88);
  border-radius: 8px;
  padding: 0.45rem 0.7rem;
  font-weight: 500;
  font-size: var(--hub-fs-sm);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  width: 100%;
  border: 0;
  background: transparent;
  cursor: pointer;
  line-height: 1.3;
}

.hub-nav-on-brand .hub-nav-link .ph {
  font-size: 1.05rem;
  opacity: 0.9;
  flex-shrink: 0;
}

.hub-nav-on-brand .hub-nav-link:hover {
  background: rgba(255, 255, 255, 0.1);
  color: var(--white);
}

/* Current page leaf */
.hub-nav-on-brand .hub-nav-link.is-active {
  background: rgba(255, 255, 255, 0.16);
  color: var(--white);
  font-weight: 600;
  box-shadow: inset 3px 0 0 0 var(--blue-soft, #7eb6e0);
}

.hub-nav-on-brand .hub-nav-link.is-active .ph {
  opacity: 1;
}

/* Branch that contains the current page */
.hub-nav-on-brand .hub-nav-summary.is-branch-active {
  color: var(--white);
  font-weight: 600;
}

/* Collapsible groups (level 1 and 2) */
.hub-nav-group {
  margin: 0;
  width: 100%;
}

.hub-nav-group > .hub-nav-summary {
  list-style: none;
  user-select: none;
}

.hub-nav-group > .hub-nav-summary::-webkit-details-marker {
  display: none;
}

.hub-nav-chevron {
  margin-left: auto;
  font-size: 0.95rem !important;
  opacity: 0.65 !important;
  transition: transform 0.15s ease;
}

.hub-nav-group[open] > .hub-nav-summary .hub-nav-chevron,
.hub-nav-group.is-open > .hub-nav-summary .hub-nav-chevron {
  transform: rotate(90deg);
}

.hub-nav-children {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  margin: 0.15rem 0 0.35rem;
  padding: 0.1rem 0 0.15rem 0.35rem;
  border-left: 1px solid rgba(255, 255, 255, 0.12);
  margin-left: 0.85rem;
}

.hub-nav-on-brand .hub-nav-sub {
  padding: 0.38rem 0.55rem;
  font-size: var(--hub-fs-sm);
  font-weight: 450;
}

.hub-nav-on-brand .hub-nav-sub .ph {
  font-size: 0.95rem;
}

/* Disabled nav leaf: a released-soon feature. Shown for wayfinding, not clickable. */
.hub-nav-on-brand .hub-nav-link.is-disabled {
  opacity: 0.5;
  cursor: default;
  pointer-events: none;
}

.hub-nav-on-brand .hub-nav-link.is-disabled .hub-nav-soon {
  margin-left: auto;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.28);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.85;
}

/* Section caption (Comercial, Verticales, ...): a plain heading above a run of flat entries. Never
   a link, never collapsible — an uppercase, tracked, muted label reads as structure rather than
   as one more clickable row (2026-09-06 menu regroup, workbench/HUB.md). */
.hub-nav-caption {
  margin: 0.85rem 0.7rem 0.3rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
}

/* Content column: own top bar (not tied to the brand rail) */
.hub-main-column {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 100vh;
  min-height: 100dvh;
}

.hub-content-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: var(--hub-content-topbar-h);
  padding: 0.45rem 1.25rem;
  background: var(--white);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 1020;
}

.hub-content-topbar-start,
.hub-content-topbar-end {
  display: flex;
  align-items: center;
  min-width: 0;
}

.hub-content-topbar-end {
  gap: 0.75rem;
}

.hub-nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--paper);
  color: var(--ink);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}

.hub-nav-toggle:hover {
  background: var(--navy-tint);
  border-color: var(--blue-soft);
}

.hub-nav-toggle:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* Topbar counter, beside the user chip. Tinted rather than solid: it reports a fact (a customer
   answered and nobody has read it), so it must not read as an alarm. The class name predates the
   deletion of the attention ladder and is now the only "urgent" left anywhere near this chip. */
.hub-urgent-chip {
  display: inline-flex;
  align-items: center;
  padding: 0.3rem 0.65rem;
  border-radius: 999px;
  background: var(--sem-hot-tint);
  color: var(--sem-hot);
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.hub-urgent-chip:hover,
.hub-urgent-chip:focus-visible {
  background: var(--sem-hot);
  color: var(--white);
}

/* Same chip, different news: a customer answered a diagnostic. It reads in the cold blue rather
   than the hot tone because this is good news that needs doing, not something running late, and
   alarm color belongs to what is overdue or destructive. */
.hub-urgent-chip.is-answered {
  background: var(--sem-frozen-tint);
  color: var(--sem-frozen);
}

.hub-urgent-chip.is-answered:hover,
.hub-urgent-chip.is-answered:focus-visible {
  background: var(--sem-frozen);
  color: var(--white);
}

/* User chip (content top bar) */
.hub-user-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  max-width: 16rem;
  padding: 0.25rem 0.55rem 0.25rem 0.3rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  font: inherit;
  cursor: pointer;
}

.hub-user-chip:hover,
.hub-user-chip:focus-visible {
  border-color: var(--blue-soft);
  background: var(--white);
  color: var(--ink);
}

.hub-user-chip::after {
  margin-left: 0.15rem;
  opacity: 0.55;
}

.hub-user-avatar {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  flex-shrink: 0;
  overflow: hidden;
  background: var(--navy);
  color: var(--white);
}

.hub-user-avatar-icon {
  font-size: 1rem;
  line-height: 1;
  opacity: 0.95;
}

/* Photo sits on top of the Phosphor user icon; on error JS removes the img. */
.hub-user-avatar-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  border-radius: 999px;
  background: var(--navy);
}

.hub-user-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  line-height: 1.2;
  text-align: left;
}

.hub-user-name {
  font-size: var(--hub-fs-sm);
  font-weight: 600;
  color: var(--ink);
  max-width: 11rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hub-user-email {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  max-width: 11rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hub-user-dropdown {
  min-width: 14rem;
  font-size: var(--hub-fs-sm);
}

.hub-user-dropdown-email {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}

.hub-logout-form {
  margin: 0;
}

.hub-logout-btn {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
}

/*
 * Item 1 (lane L): the top bar carries what a person needs at 375px and drops what they do not.
 * Measured wide on every route (449px against a 375px viewport, the end cluster alone 390px), and
 * it was the same two things every time: the identity chip spelling out a name and an email that
 * already sit inside the menu it opens, and an unbounded status pill next to it.
 *
 * The identity chip collapses to the avatar; nothing is lost, since the dropdown repeats the email
 * (.hub-user-dropdown-email). The status pill truncates rather than moves below the bar: moving it
 * would make the sticky header a different height on a page with an unread diagnostic than on one
 * without, and the count that matters sits first in the string ("2 diagnósticos respondidos"), so
 * an ellipsis only ever eats the trailing words, never the number.
 */
@media (max-width: 575.98px) {
  .hub-user-meta {
    display: none;
  }

  .hub-urgent-chip {
    max-width: 9rem;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* Offcanvas (phone) — same brand paint as desktop rail */
.hub-offcanvas {
  --bs-offcanvas-width: min(18rem, 88vw);
  background: var(--navy);
  color: var(--white);
  border-right: none;
}

.hub-offcanvas-header {
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

.hub-offcanvas-body {
  padding-top: 1rem;
}

.hub-main {
  padding: 1rem 1.25rem 1.5rem;
  min-width: 0;
  max-width: 100%;
  width: 100%;
  overflow-x: clip;
  flex: 1 1 auto;
}

/* Login (standalone) */
.hub-login-body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  background:
    radial-gradient(1200px 600px at 10% -10%, rgba(73, 141, 190, 0.35), transparent 55%),
    radial-gradient(900px 500px at 100% 100%, rgba(57, 54, 100, 0.45), transparent 50%),
    var(--navy);
  color: var(--white);
  font-family: var(--font-sans);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}

/* Lattice layer over the navy gradient: a field of small squares that light up slowly and fade
   back out. Metrics and behaviour live in js/hub/login-lattice.js; this block owns placement and
   the three colours the module reads back with getComputedStyle, so the brand tokens stay the
   single source of truth for the colour of the light. */
.hub-login-lattice {
  --lattice-cell: var(--blue-soft);    /* a square at rest, painted at a few percent alpha */
  --lattice-lit: var(--blue-bright);   /* a square carrying light */
  --lattice-peak: var(--white);        /* the top of a glimmer */
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  /* Light over the navy gradient has to read as light, not as a pale film laid on top of it. */
  mix-blend-mode: screen;
}

.hub-login {
  position: relative;
  z-index: 1;
  width: min(24rem, 100%);
}

.hub-login-panel {
  background: rgba(11, 25, 49, 0.88);
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 18px;
  padding: 2.25rem 1.85rem 1.85rem;
  backdrop-filter: blur(10px);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
}

.hub-login-brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 1.75rem;
}

.hub-login-brand img {
  height: 1.5rem;
  width: auto;
}

.hub-login-title {
  font-family: var(--font-serif);
  font-size: 1.75rem;
  font-weight: 500;
  margin: 0 0 0.65rem;
  letter-spacing: -0.02em;
  color: var(--white);
}

.hub-login-lead {
  margin: 0 0 1.85rem;
  color: rgba(255, 255, 255, 0.72);
  font-size: 0.95rem;
  line-height: 1.55;
}

.hub-login-actions {
  margin-top: 0.25rem;
}

/* "Sign in with Google" — Google's official dark-theme pill button. Colours, radius, and label
   wording (English, unrelated to the rest of the UI's Colombian Spanish copy) are fixed by
   Google's branding guidelines, not by Hub's brand tokens: https://developers.google.com/identity/branding-guidelines
   The state layer (.gsi-button-dark-state) is Google's own hover/focus/active affordance; the
   :focus-visible outline below is Hub's own addition, since the reference button ships with
   `outline: none` and relies on the state layer alone, which is not enough for keyboard users. */
.gsi-button-dark {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 40px;
  padding: 0 12px;
  border: 1px solid #8E918F;
  border-radius: 20px;
  background: #131314;
  overflow: hidden;
  text-decoration: none;
  cursor: pointer;
  transition: box-shadow 0.218s ease;
}

.gsi-button-dark:hover {
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3), 0 1px 3px 1px rgba(0, 0, 0, 0.15);
}

.gsi-button-dark:focus-visible {
  outline: 2px solid var(--blue-soft);
  outline-offset: 2px;
}

.gsi-button-dark-state {
  position: absolute;
  inset: 0;
  background: #E3E3E3;
  opacity: 0;
  transition: opacity 0.218s ease;
}

.gsi-button-dark:hover .gsi-button-dark-state {
  opacity: 0.08;
}

.gsi-button-dark:active .gsi-button-dark-state,
.gsi-button-dark:focus-visible .gsi-button-dark-state {
  opacity: 0.12;
}

.gsi-button-dark-content {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
}

.gsi-button-dark-icon {
  display: block;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

.gsi-button-dark-label {
  font-family: "Roboto", var(--font-sans);
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 0.25px;
  color: #E3E3E3;
  white-space: nowrap;
}

.hub-login-divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.75rem 0 1.25rem;
  color: rgba(255, 255, 255, 0.45);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hub-login-divider::before,
.hub-login-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: rgba(255, 255, 255, 0.14);
}

.hub-login-local {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.hub-login-field label {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.8rem;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.7);
}

.hub-login-field .form-control {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--white);
  border-radius: 8px;
  padding: 0.55rem 0.7rem;
}

.hub-login-field .form-control::placeholder {
  color: rgba(255, 255, 255, 0.35);
}

.hub-login-field .form-control:focus {
  background: rgba(255, 255, 255, 0.1);
  border-color: var(--blue-soft);
  color: var(--white);
  box-shadow: 0 0 0 0.15rem rgba(146, 187, 216, 0.25);
}

.hub-login-local-submit {
  margin-top: 0.25rem;
  width: 100%;
  padding: 0.7rem 1rem;
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 10px;
  background: transparent;
  color: var(--white);
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
}

.hub-login-local-submit:hover {
  background: rgba(255, 255, 255, 0.08);
}

.hub-login-footnote {
  margin: 1.5rem 0 0;
  font-size: 0.75rem;
  color: rgba(255, 255, 255, 0.62);
  line-height: 1.5;
}

.hub-login-footnote .hub-login-local-link {
  margin-top: 0;
  margin-right: 0.15rem;
}

.hub-login-local-access {
  margin: 0.45rem 0 0;
}

/* Terms of use (public, linked from login) — readable on login canvas.
   The login container is a 24rem card, which is the wrong shape for a legal document: it turned
   fifteen clauses into a phone-width ribbon. The page widens to a real document and sets the text
   in two columns, so the measure stays readable (~55 characters) instead of growing with the panel. */
.hub-login.hub-terms {
  width: min(64rem, 100%);
}
.hub-terms .hub-terms-panel {
  max-width: none;
  max-height: min(92vh, 60rem);
  overflow: auto;
  text-align: left;
  padding: 2.5rem 2.75rem 2rem;
}
.hub-terms-meta {
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.55);
  line-height: 1.45;
  margin: 0 0 1.25rem;
}
.hub-terms-body {
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.82);
  line-height: 1.55;
  columns: 2;
  column-gap: 3rem;
}
.hub-terms-body h2 {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--white);
  margin: 1.25rem 0 0.4rem;
  /* A clause heading stranded at the foot of a column reads as a different clause than the one it
     introduces, so headings stay with their text and no paragraph is split across the fold. */
  break-after: avoid;
  break-inside: avoid;
}
.hub-terms-body h2:first-child {
  margin-top: 0;
}
.hub-terms-body p,
.hub-terms-body li {
  break-inside: avoid;
}
@media (max-width: 900px) {
  .hub-terms-body {
    columns: 1;
  }
}
.hub-terms-body p,
.hub-terms-body ul {
  margin: 0 0 0.65rem;
}
.hub-terms-body ul {
  padding-left: 1.2rem;
}
.hub-terms-body li {
  margin-bottom: 0.35rem;
}

.hub-login-local-link {
  display: inline-block;
  margin-top: 0.55rem;
  color: rgba(255, 255, 255, 0.55);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  font-size: 0.72rem;
}

.hub-login-local-link:hover {
  color: var(--blue-soft);
  border-bottom-color: var(--blue-soft);
}

.hub-login-alert {
  border-radius: 8px;
  padding: 0.6rem 0.75rem;
  font-size: 0.85rem;
  margin-bottom: 1.25rem;
}

.hub-login-alert-ok {
  background: rgba(110, 164, 203, 0.2);
  border: 1px solid rgba(110, 164, 203, 0.45);
  color: var(--blue-soft);
}

.hub-login-alert-error {
  background: rgba(180, 60, 60, 0.2);
  border: 1px solid rgba(220, 120, 120, 0.4);
  color: #f0c4c4;
}

.hub-content {
  width: 100%;
  max-width: none;
  font-size: var(--hub-fs);
  line-height: var(--hub-lh);
}

.hub-crumbs {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  margin-bottom: 0.85rem;
  letter-spacing: 0.02em;
}

/*
 * Flash messages (fragments/flash.html) sit between the breadcrumb and the page itself, so they
 * never share a row with the page's own action buttons — but Bootstrap's bare `.alert` margin left
 * that gap to chance, and a page whose header sits close behind it read as one crowded block of
 * buttons and flash messages, wrong spacing throughout. One rhythm value, the same one
 * `.page-header-row` already uses, so a flash reads as its own line
 * rather than decoration stuck to whatever follows it. Costs nothing when there is nothing to say:
 * the wrapper is always in the DOM, but empty of an `.alert` it has no margin of its own.
 */
.hub-flash-stack:has(.alert) {
  margin: 0 0 1.15rem;
}
.hub-flash-stack .alert {
  margin: 0 0 0.55rem;
}
.hub-flash-stack .alert:last-child {
  margin-bottom: 0;
}

/*
 * Views use the full width of the main column (sidebar already reserved).
 * Do not re-introduce page max-widths — tables and headers should breathe.
 */
.hub-page {
  width: 100%;
  max-width: none;
}

/* Vertical rhythm between title block and body (no horizontal rules) */
.page-header {
  margin: 0 0 1.15rem;
  padding: 0;
}

.page-header h1,
.hub-page-title {
  margin: 0 0 0.25rem;
  font-size: var(--hub-fs-title);
  font-family: var(--font-sans);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.25;
  color: var(--ink);
}

.page-header-text {
  min-width: 0;
  flex: 1 1 auto;
}

.page-header-actions {
  flex-shrink: 0;
}

.page-header .subtitle,
.subtitle {
  margin: 0;
  color: var(--muted);
  max-width: none;
  font-size: var(--hub-fs-sm);
  line-height: 1.4;
}

.page-header-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.65rem 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.15rem;
}

.page-header-row > .btn,
.page-header-row > .action-row {
  flex-shrink: 0;
}

/* Stack of sections under a page header */
.page-header + .panel,
.page-header + .hub-table-panel,
.page-header + .hub-form,
.page-header + .stat-row,
.page-header + .kanban-scroll,
.page-header + .hub-detail-toolbar {
  margin-top: 0.55rem;
}

/* Identity + toolbar read as one block: less gap under the title when toolbar follows */
.page-header:has(+ .hub-detail-toolbar) {
  margin-bottom: 0;
}

/*
 * Detail ficha toolbar: navigation (Regresar) left, mutate (Editar / Eliminar) right.
 * Invisible row — spacing only, no bar. Keeps destructive actions away from back.
 */
.hub-detail-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.65rem 1rem;
  margin: 0 0 1.15rem;
  padding: 0;
}

.hub-detail-toolbar-end {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  margin-left: auto;
}

/* Icon + word actions (detail pages). List row actions stay icon-only. */
.hub-btn-icon-text {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--hub-fs-sm);
  line-height: 1.3;
  padding: 0.35rem 0.7rem;
}

.hub-btn-icon-text .ph {
  font-size: 1.05em;
  line-height: 1;
  flex-shrink: 0;
}

/*
  Permission-denied primary action (see fragments/gated-action.html). Reads as a button but is
  inert: dimmed, no hover lift, not-allowed cursor. Keeps pointer-events so the "sin permiso"
  title tooltip still appears on hover (Bootstrap's .disabled would suppress it).
*/
.hub-action-denied,
.hub-action-denied:hover,
.hub-action-denied:focus {
  opacity: 0.55;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
  filter: grayscale(0.25);
}

/* Soft danger outline aligned with brand red (not Bootstrap purple-pink) */
.btn-outline-danger.hub-btn-icon-text {
  --bs-btn-color: #c23b3b;
  --bs-btn-border-color: #e0a0a0;
  --bs-btn-hover-bg: #fdecea;
  --bs-btn-hover-border-color: #c23b3b;
  --bs-btn-hover-color: #9a2e2e;
  --bs-btn-active-bg: #fdecea;
  --bs-btn-active-border-color: #9a2e2e;
  --bs-btn-active-color: #9a2e2e;
}

.hub-detail-toolbar + .panel,
.hub-detail-toolbar + .hub-meta-panel,
.hub-detail-toolbar + .hub-tab-bar {
  margin-top: 0;
}

/*
 * Company-spine tabs (and any future entity hub tabs).
 * Path-driven ?tab= — active underline, counts for list tabs.
 *
 * The bar used to wrap onto a second row once the tabs stopped fitting, which happened to still
 * look fine at the one width (335px) anyone had measured it against — luck, not design. The
 * company spine carries nine tabs (CompanyTab), which never fit a phone. Rather than wrap or
 * clip, the bar scrolls sideways inside itself, and hub-tab-bar-scroll.js brings the active tab
 * into view once on load so it is never the one left past the fold.
 */
.hub-tab-bar {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-end;
  gap: 0.15rem 0.1rem;
  margin: 0 0 1rem;
  padding: 0;
  border-bottom: 1px solid var(--line-soft);
  overflow-x: auto;
  /*
   * Not redundant, do not delete. Once one axis is not `visible` the other computes to `auto`
   * (CSS Overflow 3), and .hub-tab-item sits one pixel below the bar (margin-bottom: -1px) so it
   * covers the underline. That single pixel of vertical overflow was enough for Windows to draw a
   * full vertical scrollbar, arrow buttons and all, inside a 38px tab strip: measured live at
   * clientHeight 38 against scrollHeight 39.
   */
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

/*
 * The bar still scrolls, by wheel, trackpad and touch, and draws no scrollbar at all (Matt,
 * 2026-09-13). The arrows were a bug and the thin thumb that replaced them still read as furniture.
 * What says a tab is hidden is a soft fade at the edge that hides it, which hub-tab-bar-scroll.js
 * turns on only while that edge really hides one. scrollbar-width hides the bar in Chromium 121+
 * and Firefox; the pseudo-element hides it in Safari. Setting scrollbar-width also switches off
 * every ::-webkit-scrollbar rule in Chromium, which is the trap the 2026-09-10 fix fell into, and
 * here it is exactly what is wanted.
 */
.hub-tab-bar {
  scrollbar-width: none;
}

.hub-tab-bar::-webkit-scrollbar {
  display: none;
}

.hub-tab-bar.is-clipped-end {
  mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
}

.hub-tab-bar.is-clipped-start {
  mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
}

.hub-tab-bar.is-clipped-start.is-clipped-end {
  mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent);
}

.hub-tab-item {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 0.35rem;
  padding: 0.55rem 0.75rem;
  margin-bottom: -1px;
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  /* Normalize <button> chrome so JS-toggled tabs (home) match the <a> tabs
     (company spine): no UA background, outset border, radius, or font. */
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  border-radius: 0;
  border-bottom: 2px solid transparent;
  font-family: inherit;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.hub-tab-item:hover {
  color: var(--ink);
}

/*
 * The ring is drawn inside the tab (negative offset) because the bar clips its vertical overflow:
 * the browser default sits outside the box and would be cut off at the top and bottom edges.
 */
.hub-tab-item:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}

.hub-tab-item.is-active {
  color: var(--navy);
  border-bottom-color: var(--navy);
  font-weight: 600;
}

.hub-tab-count {
  font-size: var(--hub-fs-xs);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--muted);
  background: var(--paper);
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  padding: 0.05rem 0.4rem;
  min-width: 1.35rem;
  text-align: center;
}

.hub-tab-item.is-active .hub-tab-count {
  color: var(--navy);
  border-color: var(--line);
  background: var(--navy-tint, var(--paper));
}

.hub-tab-panel {
  width: 100%;
  margin: 0 0 1rem;
}

/* Create / primary actions sit above the table in every list tab */
.hub-tab-create {
  margin: 0 0 1rem;
}

.hub-tab-create + .hub-table-panel,
.hub-tab-create + .empty-hint {
  margin-top: 0;
}

.hub-tab-bar + .hub-tab-panel {
  margin-top: 0;
}

.panel + .panel,
.hub-meta-panel + .panel,
.panel + .hub-form {
  margin-top: 0.85rem;
}

/* UI paradigm: no horizontal bars / HR separators — ever (AGENTS.md §3).
   Use spacing, type weight, and panels for hierarchy.
   Backstop only, for an <hr> that slips into a template. The .section-divider and .hub-hr
   selectors that used to sit here were deleted 2026-08-04: no template carried either class, and
   styling a banned pattern reads as support for it. */
hr {
  display: none !important;
}

/* Brand eyebrow — always Geist sans (never serif / system default) */
.eyebrow {
  font-family: var(--font-sans);
  margin: 0 0 0.3rem;
  font-size: var(--hub-fs-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}

/* Per-step dwell on the diagnostic detail: a run of steps read across, not a second meta grid.
   hub-meta-grid fits as many 14rem columns as the width allows, so seven per-step figures beside
   four headline ones produced two rows that lined up with nothing. */
.diagnostic-dwell {
  margin-top: 1.35rem;
}

.diagnostic-dwell-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 2rem;
}

.diagnostic-dwell-list li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.diagnostic-dwell-step {
  font-size: var(--hub-fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.diagnostic-dwell-time {
  font-variant-numeric: tabular-nums;
}

/* ── Form pages (canonical: /companies/new) ──
   Form pages use the shared .page-header / .hub-page-title system like every other screen. The
   .hub-form-header and .hub-form-title overrides that used to live here went with
   fragments/form-header.html on 2026-08-04: no template used either class. */
.hub-form-page .subtitle {
  max-width: none;
}

/* Fields may stay readable-width; the page itself is full width */
.hub-form-page .hub-form .field {
  max-width: 36rem;
}

.hub-form-page .hub-form .form-control,
.hub-form-page .hub-form .form-select {
  max-width: 100%;
  width: 100%;
  border-color: var(--line);
  border-radius: 8px;
  font-family: var(--font-sans);
  color: var(--ink);
}

.hub-form-page .hub-form .form-control:focus {
  border-color: var(--blue);
  box-shadow: 0 0 0 0.2rem rgba(73, 141, 190, 0.2);
}

.field-req {
  display: inline-block;
  margin-left: 0.35rem;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--blue-deep);
  background: var(--navy-tint);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  vertical-align: middle;
}

.field-opt {
  display: inline-block;
  margin-left: 0.35rem;
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  vertical-align: middle;
}

/* A fieldset used only to disable a group of controls at once (an emitted document's form).
   The browser's own fieldset chrome would draw a border and indent everything inside it, so it is
   reset to nothing: this element carries behaviour, never appearance. min-width:0 is the long
   standing fix for a fieldset refusing to shrink inside a flex or grid parent. */
.hub-fieldset-plain {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

/* Inline money edit inside a table cell (catalog unit freight) — amount + currency + OK on one
   row, same nowrap discipline as .hub-percent-input so a narrow column never stacks the group. */
.hub-money-form {
  margin: 0;
}
.hub-money-input {
  max-width: 14rem;
  flex-wrap: nowrap !important;
  display: flex;
  align-items: stretch;
}
.hub-money-input .form-control {
  width: 1%;
  flex: 1 1 auto;
  min-width: 0;
}
.hub-money-input .form-select {
  flex: 0 0 auto;
  width: auto;
  padding-right: 1.6rem;
}

/* Percent suffix group (landed, margins) — always one horizontal row */
.hub-percent-input {
  max-width: 12rem;
  flex-wrap: nowrap !important;
  display: flex;
  align-items: stretch;
}
.hub-percent-input .form-control {
  letter-spacing: 0.02em;
  width: 1%;
  flex: 1 1 auto;
  min-width: 0;
}
.hub-percent-suffix {
  min-width: 2.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  background: var(--white);
  border-left: 1px solid var(--line-soft);
  font-weight: 600;
  flex: 0 0 auto;
  white-space: nowrap;
}

/*
 * Hub checkbox — calm square control (not Bootstrap form-switch pills).
 * Use .hub-check / .hub-check-input / .hub-check-label on forms.
 */
.hub-check {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  margin: 0;
  padding: 0.45rem 0;
  cursor: pointer;
  user-select: none;
}
.hub-check-input {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  border: 1.5px solid var(--line);
  border-radius: 4px;
  background: var(--white);
  cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease, box-shadow 0.12s ease;
}
.hub-check-input:hover {
  border-color: var(--blue-soft);
}
.hub-check-input:focus {
  outline: none;
  border-color: var(--blue);
  box-shadow: 0 0 0 3px rgba(73, 141, 190, 0.22);
}
.hub-check-input:checked {
  border-color: var(--navy);
  background-color: var(--navy);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-6'/%3E%3C/svg%3E");
  background-size: 0.75rem 0.75rem;
  background-position: center;
  background-repeat: no-repeat;
}
.hub-check-input:checked:hover {
  border-color: var(--navy);
  background-color: var(--navy-2, #102036);
}
.hub-check-label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  color: var(--ink);
  line-height: 1.4;
  cursor: pointer;
}
.hub-check-hint {
  display: block;
  margin-top: 0.15rem;
  font-size: var(--hub-fs-xs);
  font-weight: 400;
  color: var(--muted);
  line-height: 1.35;
}

/* Role group on party (tercero) form */
.party-roles {
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
}
.party-roles-legend {
  font-size: var(--hub-fs-sm);
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 0.15rem;
  padding: 0;
}
.party-roles-box {
  margin-top: 0.35rem;
  padding: 0.15rem 0.85rem;
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  background: var(--white);
}

/* Never use Bootstrap form-switch pills in hub forms */
.hub-form .form-switch {
  display: none !important;
}

.field-hint {
  margin: 0.25rem 0 0;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  line-height: 1.35;
}

.is-invalid-field label {
  color: #8b1e1e;
}

.hub-form-summary {
  background: #fdecea;
  color: #5c1a1a;
  border-radius: 12px;
  padding: 0.85rem 1rem;
  margin: 0 0 1rem;
}

.hub-form-summary-title {
  margin: 0 0 0.35rem;
  font-weight: 600;
  font-family: var(--font-sans);
}

.hub-form-summary ul {
  margin: 0;
  padding-left: 1.1rem;
}

.hub-meta-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 0.85rem 1.25rem;
  margin: 0;
}

.hub-meta-grid dt {
  font-size: var(--hub-fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  font-weight: 600;
  margin: 0 0 0.2rem;
}

.hub-meta-grid dd {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--hub-fs);
  line-height: var(--hub-lh);
}

.hub-meta-by {
  display: block;
  font-size: var(--hub-fs-sm);
  color: var(--muted);
  margin-top: 0.15rem;
}

/*
 * One table system for every hub list (DataTables and plain).
 * Markup: .hub-table-panel > table.table.table-striped.table-hover.table-borderless
 *         .align-middle.hub-datatable (rows open through a.hub-row-open in the name cell)
 * Excel: HubDataTable default Buttons (data-hub-export="false" to opt out).
 */
.hub-table-panel {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.85rem 0.9rem 0.65rem;
  width: 100%;
  margin: 0 0 1rem;
  box-shadow: 0 1px 0 rgba(14, 42, 69, 0.03);
  /* Safety net: any table in this card scrolls inside the card, never the page, whether or not
     DataTables ever touched it. Fourteen registries and layouts held a plain table with none of
     DataTables' own markup, so the rule below (which targets the inner layout element DataTables
     builds) never applied to them and the whole page dragged sideways instead — the reference
     column on Entradas ran off the left edge, tab bar and all. This one catches everything else. */
  overflow-x: auto;
}

/* Wide DataTables (e.g. Registro with its Detalle column) scroll horizontally
   inside their card instead of spilling past it. Scrollbar sits under the table;
   the search/length/paging chrome and the card edges stay put. */
.hub-table-panel .dt-container .dt-layout-table {
  overflow-x: auto;
}

/*
 * Responsive (DataTables) — remap the vendored extension's own hardcoded colors onto hub tokens
 * rather than leaving datatables.net's defaults (light grey border, 50%-black arrow) sitting next
 * to the rest of the design system.
 */
table.dataTable > tbody > tr.child ul.dtr-details > li {
  border-bottom-color: var(--line);
}
table.dataTable > tbody > tr.child span.dtr-title {
  color: var(--ink);
  min-width: 110px;
}
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control:before,
table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control:before {
  border-left-color: var(--muted);
}
table.dataTable.dtr-inline.collapsed > tbody > tr.dtr-expanded > td.dtr-control:before,
table.dataTable.dtr-inline.collapsed > tbody > tr.dtr-expanded > th.dtr-control:before {
  border-top-color: var(--muted);
}

/*
 * Mobile stacking for the plain (non-DataTables) tables in the codebase. hub-table-stack.js reads
 * each table's own <thead> once and writes the label every stacked cell shows through ::before
 * (data-label) — one generic rule instead of a data-label attribute hand-written into fourteen
 * templates. A DataTables table (".dataTable") already gets the equivalent promise from Responsive
 * above: rather than stacking, it collapses the columns that matter least and keeps identity and
 * Acciones in view.
 *
 * Opt out per table with data-hub-no-stack when a column is only filled on the first row of a
 * group (see annex/confirm.html's "Equipo" column) — stacking would repeat that blank on every row
 * after the first and read as broken rather than grouped, so that one table keeps scrolling in its
 * card instead (the safety net above).
 */
@media (max-width: 767.98px) {
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
    padding: 0;
    margin: -1px;
  }
  /* A record is a compact grid, not one full-width line per field. Nine short values stacked one
     per row made a card taller than the screen for a record that is mostly two-word answers
     (Matt, 2026-09-07, on /staging). auto-fill means the same rule gives one column on a narrow
     phone and three on a wide one, with no breakpoint to maintain. */
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) tbody tr,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) tfoot tr {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
    gap: 0.15rem 1.1rem;
    padding: 0.7rem 0;
    border-bottom: 1px solid var(--line);
  }
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) tbody tr:last-child,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) tfoot tr:last-child {
    border-bottom: none;
  }
  /* Label above value, both left-aligned: a label/value pair split to opposite edges of a full
     row leaves a gap the eye has to jump, and the jump is what made the old card feel sparse. */
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th {
    display: block;
    text-align: left;
    border: none;
    padding: 0.1rem 0;
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td[data-label]::before,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: var(--hub-fs-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
  }
  /* Prose and controls take the full width of the card: a sentence chopped into a 9.5rem column
     is unreadable, and an actions row belongs on its own line at the end. */
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td.staging-ask,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td.hub-col-actions,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th.hub-col-actions {
    grid-column: 1 / -1;
  }
  /* A merged cell (colspan) is a full-width line of prose, not a labelled field — the row-count
     summary on catalog/item-detail.html's cost table, the "sin líneas" placeholders, and the like
     already read fine without a header prefix in front of them. */
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td[colspan],
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th[colspan] {
    display: block;
    text-align: left;
    grid-column: 1 / -1;
  }
  /* A table nested inside another table's own cell (staging/index.html's inline metrics table,
     opened from a row's detail) stays a plain table: hub-table-stack.js never labels it, and only
     the outermost table in a card stacks. */
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td table,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th table {
    display: table;
  }
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td table thead,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th table thead {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    clip: auto;
    white-space: normal;
    margin: 0;
  }
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td table tr,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th table tr {
    display: table-row;
    padding: 0;
    border-bottom: 0;
  }
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td table td,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) td table th,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th table td,
  .hub-table-panel table:not(.dataTable):not([data-hub-no-stack]) th table th {
    display: table-cell;
    text-align: left;
    padding: 0.2rem 0.4rem;
  }
}

/* DataTables top chrome: length + Excel sit calmly together */
div.dt-container .dt-buttons {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0;
  float: none;
}
div.dt-container .dt-buttons .btn.hub-dt-export,
div.dt-container .dt-buttons .dt-button.hub-dt-export {
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  border-radius: 8px;
  padding: 0.28rem 0.7rem;
  border: 1px solid var(--line);
  background: var(--white);
  color: var(--ink);
  line-height: 1.35;
}
div.dt-container .dt-buttons .btn.hub-dt-export:hover,
div.dt-container .dt-buttons .dt-button.hub-dt-export:hover {
  background: var(--paper);
  border-color: var(--blue-soft, #7eb6e0);
  color: var(--navy);
}
/* Buttons 3 processing state — clear affordance while audit + zip run */
div.dt-container .dt-buttons .dt-button.processing,
div.dt-container .dt-buttons .btn.processing {
  opacity: 0.65;
  pointer-events: none;
  position: relative;
}
div.dt-container .dt-buttons .dt-button.processing::after,
div.dt-container .dt-buttons .btn.processing::after {
  content: "";
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  margin-left: 0.4rem;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  vertical-align: -0.1rem;
  animation: hub-dt-spin 0.6s linear infinite;
}
@keyframes hub-dt-spin {
  to { transform: rotate(360deg); }
}

.hub-datatable,
.hub-table {
  --dt-row-hover: 210, 232, 245;
  font-family: var(--font-sans);
  font-size: var(--hub-fs);
  line-height: var(--hub-lh);
  width: 100%;
  margin: 0;
  border-color: var(--line-soft);
  background: var(--white);
}

/*
 * The way into a record: a real link on the row's own name.
 *
 * Replaces the whole-row click every list table used to carry (removed 2026-09-07). A row that
 * navigates is a control nothing announces: it cannot be middle-clicked or opened in a new tab, a
 * screen reader is never told it leads anywhere, and any control the handler did not recognise
 * opened the detail as a side effect of being pressed. The pointer cursor that used to sit on every
 * row went with it, because the row is not a target any more; the link is.
 */
.hub-row-open {
  color: inherit;
  text-decoration: none;
  font-weight: 500;
}

.hub-row-open:hover,
.hub-row-open:focus-visible {
  color: var(--blue);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Brand sky hover (overrides Bootstrap grey/white hover) */
table.hub-datatable.table-hover > tbody > tr:hover > *,
table.hub-table.table-hover > tbody > tr:hover > * {
  --bs-table-bg-state: transparent;
  background-color: rgba(110, 164, 203, 0.14) !important;
  box-shadow: none !important;
}

table.hub-datatable.table-striped > tbody > tr:nth-of-type(odd):hover > *,
table.hub-table.table-striped > tbody > tr:nth-of-type(odd):hover > * {
  background-color: rgba(110, 164, 203, 0.16) !important;
}

.hub-datatable thead th,
.hub-table th {
  font-size: var(--hub-fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
  background: var(--paper);
  border-bottom-color: var(--line-soft);
  padding: var(--hub-cell-y) var(--hub-cell-x);
  vertical-align: middle;
}

.hub-datatable tbody td,
.hub-table td {
  font-size: var(--hub-fs);
  line-height: var(--hub-lh);
  color: var(--ink);
  border-bottom-color: var(--line-soft);
  padding: var(--hub-cell-y) var(--hub-cell-x);
  vertical-align: middle;
}

.hub-datatable tbody tr:last-child td,
.hub-table tr:last-child td {
  border-bottom: none;
}

.hub-datatable a,
.hub-table a {
  color: var(--blue-deep);
  text-decoration: none;
  font-weight: 500;
}

/* Outcome chips on the system log */
.badge.hub-table-badge-ok {
  background: #e6f7f1;
  color: #0d7a55;
}
.badge.hub-table-badge-warn {
  background: #fff6e5;
  color: #9a6b00;
}
.badge.hub-table-badge-danger {
  background: #fdecec;
  color: #a32020;
}

.hub-datatable a:hover,
.hub-table a:hover {
  color: var(--navy);
}

/* Status / enum chips inside tables (same on list and tab tables) */
.badge.hub-table-badge,
.hub-datatable .badge.hub-table-badge {
  font-weight: 500;
  font-size: var(--hub-fs-xs);
  background: var(--navy-tint);
  color: var(--navy);
  border: 1px solid var(--line-soft);
}

.hub-col-actions {
  width: 1%;
  white-space: nowrap;
}

.hub-col-id {
  width: 1%;
  white-space: nowrap;
}

/*
 * Entity id column (HubDataTable.idCellHtml / fragments/id-cell.html — the one home for both
 * server-rendered and DataTables-rendered tables). The full id is the actual cell text, so the
 * browser's own search-in-page, DataTables search, and a plain copy all see the whole value; only
 * the display is clipped here, with the same full value repeated in `title` for the hover
 * tooltip. Monospace is reserved for codes and ids — this is the one column that earns it.
 */
.hub-id-cell {
  display: inline-block;
  max-width: 8ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
  font-family: var(--font-mono);
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}

/*
 * Semáforo (opportunity temperature) chips — fragments/semaforo.html :: badge / badgeCompact.
 * The sem-* modifier carries a color/tint pair via CSS variables; the chip itself just reads them,
 * so badge, compact badge, kanban accent, and gauge all share one palette definition.
 */
/* Three bands, three classes. The names are the old five-band ones; the values are the ramp.
   sem-hot is Caliente (amber sun), sem-cold is Fria (blue wind), sem-frozen is Glaciar (violet
   snowflake). sem-warm and sem-cool went with the two bands that no longer exist. */
.sem-hot {
  --sem-color: var(--sem-caliente);
  --sem-tint: var(--sem-caliente-tint);
}

.sem-cold {
  --sem-color: var(--sem-fria);
  --sem-tint: var(--sem-fria-tint);
}

.sem-frozen {
  --sem-color: var(--sem-glaciar);
  --sem-tint: var(--sem-glaciar-tint);
}

/*
 * Repair affordance: a record that cannot do its job because a link is missing says so and offers
 * the fix in place. Amber rather than red, because nothing is broken or lost; something is
 * incomplete and the person reading the row is the one who can complete it.
 */
.hub-repair-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: #9a6b00;
  font-weight: 600;
  text-decoration: none;
}
.hub-repair-link:hover {
  text-decoration: underline;
}
@media (max-width: 900px) {

}

/* Colors plain text by the sem-* modifier already on the element (e.g. the opportunity-detail
   subtitle's state word) without the badge/chip chrome. */
.sem-text {
  color: var(--sem-color);
}

.sem-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  line-height: 1.3;
  color: var(--sem-color);
  background: var(--sem-tint);
  white-space: nowrap;
}

.sem-badge i {
  font-size: 1.15em;
  line-height: 1;
  /* Only the Phosphor Regular weight is vendored (no Bold font file), so a thicker stroke stands
     in for a true bold glyph. Drop this if it ever looks rough at a given size. */
  -webkit-text-stroke: 0.4px currentColor;
  text-stroke: 0.4px currentColor;
}

/* Icon-only form for dense contexts (kanban card). The chip grew from 1.3rem to fit the larger,
   faux-bold icon without clipping. */
.sem-badge-compact {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  flex-shrink: 0;
  border-radius: 999px;
  color: var(--sem-color);
  background: var(--sem-tint);
}

.sem-badge-compact i {
  font-size: 1.25rem;
  line-height: 1;
  -webkit-text-stroke: 0.4px currentColor;
  text-stroke: 0.4px currentColor;
}

/* =====================================================================
   Excel-like per-column filter (hub-column-filter.js) — opt-in via
   table[data-hub-col-filter]. Reference implementation: catalog/items.
   Funnel affordance appears on hover/focus of a filterable header cell;
   the popover is a single "contains" text input plus a clear (×).
   ===================================================================== */
.hub-col-filter-header {
  position: relative;
  padding-right: calc(var(--hub-cell-x) + 1.1rem);
}

.hub-col-filter-trigger {
  position: absolute;
  top: 50%;
  right: 0.25rem;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--muted);
  border-radius: var(--radius-btn);
  opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease, background-color 0.12s ease;
}

.hub-col-filter-header:hover .hub-col-filter-trigger,
.hub-col-filter-header:focus-within .hub-col-filter-trigger,
.hub-col-filter-trigger:focus-visible,
.hub-col-filter-active .hub-col-filter-trigger {
  opacity: 1;
}

.hub-col-filter-trigger:hover {
  background: var(--navy-tint);
  color: var(--blue-deep);
}

/* An active filter keeps the funnel visible and tinted, even off-hover. */
.hub-col-filter-active .hub-col-filter-trigger {
  color: var(--blue-deep);
}

.hub-col-filter-popover {
  position: absolute;
  z-index: 1080;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  min-width: 11rem;
  padding: 0.35rem;
  background: #fff;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-btn);
  box-shadow: var(--elev-1, 0 4px 18px rgba(15, 23, 42, 0.08));
}

.hub-col-filter-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-btn);
  padding: 0.3rem 0.5rem;
  font-size: var(--hub-fs-sm);
  color: var(--ink);
}

.hub-col-filter-input:focus {
  outline: none;
  border-color: var(--blue-deep);
  box-shadow: 0 0 0 0.15rem rgba(73, 141, 190, 0.22);
}

.hub-col-filter-clear {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: var(--hub-fs);
  line-height: 1;
  border-radius: var(--radius-btn);
}

.hub-col-filter-clear:hover {
  background: var(--navy-tint);
  color: var(--blue-deep);
}

/* Icon-only row actions (Phosphor) */
.hub-row-actions {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  justify-content: flex-end;
}

.hub-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--ink);
  text-decoration: none;
  line-height: 1;
  cursor: pointer;
  font-size: 1.15rem;
  transition: background 0.15s ease, color 0.15s ease;
}

.hub-icon-btn:hover {
  background: var(--paper);
  color: var(--blue-deep);
}

.hub-icon-btn:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
}

/* Brand-adjacent danger red (not Bootstrap default purple-pink) */
.hub-icon-btn-danger {
  color: #c23b3b;
}

.hub-icon-btn-danger:hover {
  color: #9a2e2e;
  background: #fdecea;
}

.hub-icon-btn .ph {
  font-size: 1.15rem;
  line-height: 1;
}

/* DT Bootstrap 5 layout chrome */
div.dt-container.dt-bootstrap5 {
  font-family: var(--font-sans);
  color: var(--body);
}

/*
 * DT2 feature chrome: exactly one label + one control.
 * Never hide labels with font-size:0 (that regressed descriptors).
 * Duplicate controls are stripped in HubDataTable.sanitizeFeatureChrome.
 */
div.dt-container.dt-bootstrap5 .dt-search,
div.dt-container.dt-bootstrap5 .dt-length {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.5rem;
}

div.dt-container.dt-bootstrap5 .dt-search label,
div.dt-container.dt-bootstrap5 .dt-length label {
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  color: var(--muted);
  margin: 0;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

/* Hard guard: if a second control ever appears, keep only the last */
div.dt-container.dt-bootstrap5 .dt-search input.form-control ~ input.form-control,
div.dt-container.dt-bootstrap5 .dt-length select.form-select ~ select.form-select {
  display: none !important;
}

div.dt-container.dt-bootstrap5 .dt-search input.form-control,
div.dt-container.dt-bootstrap5 .dt-length select.form-select {
  font-size: var(--hub-fs-sm);
  border-radius: 8px;
  border-color: var(--line);
  font-family: var(--font-sans);
  min-width: 8rem;
  padding: 0.35rem 0.5rem;
}

div.dt-container.dt-bootstrap5 .dt-search input.form-control:focus,
div.dt-container.dt-bootstrap5 .dt-length select.form-select:focus {
  border-color: var(--blue);
  box-shadow: 0 0 0 0.2rem rgba(73, 141, 190, 0.2);
}

div.dt-container.dt-bootstrap5 .pagination {
  --bs-pagination-color: var(--ink);
  --bs-pagination-hover-color: var(--blue-deep);
  --bs-pagination-active-bg: var(--blue);
  --bs-pagination-active-border-color: var(--blue);
  --bs-pagination-focus-box-shadow: 0 0 0 0.2rem rgba(73, 141, 190, 0.25);
  margin-bottom: 0;
}

div.dt-container.dt-bootstrap5 .page-link {
  font-family: var(--font-sans);
  font-size: var(--hub-fs-sm);
  padding: 0.3rem 0.55rem;
}

div.dt-container.dt-bootstrap5 .dt-info {
  font-size: var(--hub-fs-sm);
  color: var(--muted);
  padding-top: 0.35rem;
}

div.dt-container.dt-bootstrap5 table.dataTable.table {
  margin-top: 0.65rem !important;
  border-color: var(--line-soft);
  font-size: var(--hub-fs);
}

div.dt-container.dt-bootstrap5 table.dataTable.table > :not(caption) > * > * {
  border-bottom-color: var(--line-soft);
  padding: var(--hub-cell-y) var(--hub-cell-x);
}

/* Hub primary actions map to brand blue via Bootstrap primary (overridden in accendo.css) */
.btn-primary {
  --bs-btn-bg: var(--blue);
  --bs-btn-border-color: var(--blue);
  --bs-btn-hover-bg: var(--blue-deep);
  --bs-btn-hover-border-color: var(--blue-deep);
  --bs-btn-active-bg: var(--blue-deep);
  --bs-btn-active-border-color: var(--blue-deep);
}

/* Dashboard stat tiles (home). A grid, not a flex wrap: with flex the tiles that wrap onto a short
   last line stretch to fill it, so the lone TRM tile rendered as a full-width slab. auto-fill keeps
   the empty tracks, so an orphan tile stays exactly one column wide and the tiles stay on a shared
   rhythm at every width. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}

/* A card that opens what it counts. The lift is the app's standing signal for "clickable", so the
   TRM card — which counts nothing you can open — deliberately does not get it. */
a.stat-card.is-linked {
  color: inherit;
  text-decoration: none;
  transition: transform 120ms ease, box-shadow 120ms ease;
}

a.stat-card.is-linked:hover,
a.stat-card.is-linked:focus-visible {
  transform: translateY(-1px);
  box-shadow: var(--elev-1, 0 6px 20px rgba(21, 36, 60, 0.12));
}

.stat-card {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.85rem 1rem;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.65rem;
}

.stat-card-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  flex-shrink: 0;
  border-radius: var(--radius-card);
  background: var(--paper);
  color: var(--muted);
  font-size: 1.15rem;
}

.stat-card-text {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

.stat-label {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
}

.stat-value {
  /* Responsive so a long money value (the TRM tile) shrinks with the tile instead of spilling past
     its card at narrow widths; still reads as one line at any size. A figure is never ellipsised, that would silently cut digits from a money value, so the card must be free to shrink
     (min-width: 0 above) rather than clip its content. */
  display: block;
  font-size: clamp(1rem, 2.2vw, 1.25rem);
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  min-width: 0;
  /* A stat is one figure. Without this the TRM tile breaks at the space after "$" and reads as two
     lines of nonsense at narrow tile widths. */
  white-space: nowrap;
}

.hub-trm-provenance {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.hub-trm-provenance .quote-trm-status {
  margin-left: 0;
}

.hub-trm-validity {
  color: var(--muted);
  font-size: var(--hub-fs-xs);
  line-height: 1.3;
}

/*
 * "Ciclo comercial" + Urgente, side by side. Replaces the old "Cuánto tardamos" / "Los pasos" pair
 * of cards, which read the same rows keyed by stage and each drew its own self-scaled bar — a
 * five-day stage and a one-day stage both filled 60% of their own track and looked identical. One
 * shared-axis chart (hub-chart.js, data-hub-chart) now carries both.
 *
 * auto-fit + a wide minmax (not a fixed 2-column grid) is what stacks the two cards on narrow
 * screens without a breakpoint of its own — the same technique .stat-row already uses. Grid's own
 * default align-items: stretch is the whole equal-height mechanism: both cards become exactly as
 * tall as the row's tallest natural content, so the shorter one never has to be measured in JS.
 */
.hub-cycle-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(26rem, 1fr));
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}

.hub-cycle-split > .panel {
  display: flex;
  flex-direction: column;
  margin: 0;
}

/* Fills the card: flex-grow absorbs whatever height the grid's equal-height stretch adds past the
   chart's own natural content, so the legend below stays pinned to the card's bottom edge instead
   of floating in a gap. min-height is the floor before that stretch has anything to work with. */
.hub-chart-canvas {
  flex: 1 1 auto;
  min-height: 16rem;
  width: 100%;
}

.hub-chart-legend {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.9rem;
  margin: 0.6rem 0 0;
  color: var(--muted);
  font-size: var(--hub-fs-xs);
}

.hub-chart-legend-swatch {
  display: inline-block;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 2px;
  margin-right: 0.3rem;
  vertical-align: -1px;
}

.hub-chart-legend-swatch.is-ours { background: var(--hub-chart-ours); }
.hub-chart-legend-swatch.is-customer { background: var(--hub-chart-customer); }
.hub-chart-legend-swatch.is-watched { background: var(--hub-chart-watched); }
.hub-chart-legend-swatch.is-target { background: var(--hub-chart-tick); border-radius: 1px; width: 3px; }

/* The Urgente card: same flex-column contract as the chart card, so its own DataTables chrome
   (search → table → info/paging) stretches to the grid-equalised card height and the pagination
   row lands at the bottom rather than floating right under a short table. */
.hub-attention-card {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.hub-attention-card .hub-tab-bar {
  flex: 0 0 auto;
}

.hub-attention-card .hub-table-panel {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.hub-attention-card div.dt-container.dt-bootstrap5 {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* The bottom info+pagination row is always the container's last child, whatever DataTables 2 names
   it internally — margin-top: auto pins it to the card's bottom edge regardless. */
.hub-attention-card div.dt-container.dt-bootstrap5 > *:last-child {
  margin-top: auto;
}

.action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.badge {
  font-weight: 500;
  font-size: var(--hub-fs-xs);
  background: var(--navy-tint);
  color: var(--navy);
}

/* Muted helper text — always place inside a card (.panel), not bare on paper */
.empty-hint {
  margin: 0;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
  line-height: var(--hub-lh);
}

/*
 * White content cards on gray paper canvas.
 * Prefer .panel for forms/empty/audit blocks; .hub-table-panel for DataTables.
 */
.panel {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.95rem 1.05rem;
  margin: 0 0 1rem;
  width: 100%;
  box-shadow: 0 1px 0 rgba(14, 42, 69, 0.03);
}

.panel-title {
  margin: 0 0 0.6rem;
  font-size: var(--hub-fs);
  font-weight: 600;
  color: var(--ink);
}

.panel-spaced {
  margin-bottom: 1.25rem;
}

/*
 * A panel that is a door: what it holds on the left, the one way in pinned to the far right. Used
 * where a section exists only to point somewhere else (Sistema › Agente), so several such panels
 * stacked put every verb on the same vertical line instead of one under each paragraph.
 */
.panel-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.panel-row-text {
  flex: 1 1 20rem;
  min-width: 0;
}

.panel-row-text > :last-child { margin-bottom: 0; }

.hub-kv {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0.75rem 1.25rem;
  margin: 0;
}

.hub-kv dt {
  font-size: var(--hub-fs-sm);
  color: var(--muted);
  font-weight: 500;
}

.hub-kv dd {
  margin: 0.15rem 0 0;
  font-weight: 600;
}

/*
 * Question and answer, one pair per row. Use it wherever the label is a real sentence: a
 * questionnaire, a derived reading, anything a person wrote rather than a field name.
 *
 * The grids above (hub-meta-grid, hub-kv) fit as many columns as the width allows, which is right
 * for "Visitas" beside a number and wrong for "¿La etiqueta tiene que resistir algo en particular?".
 * At sentence length the label wraps, the columns reflow, and an answer ends up printed above a
 * question it does not belong to, which is what the diagnostic results page was doing.
 * This reads top to bottom, in the order the questions were answered.
 */
.hub-qa {
  margin: 0;
}

.hub-qa > div {
  display: grid;
  grid-template-columns: minmax(0, 24rem) minmax(0, 1fr);
  gap: 0.3rem 2rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--line-soft);
}

.hub-qa > div:last-child {
  border-bottom: 0;
}

.hub-qa dt {
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  color: var(--muted);
}

.hub-qa dd {
  margin: 0;
  font-weight: 500;
}

/* The photographs are the answer, not a footnote to it: they take the full width. */
.hub-qa dd.diagnostic-result-photos {
  grid-column: 1 / -1;
}

@media (max-width: 62rem) {
  .hub-qa > div {
    grid-template-columns: minmax(0, 1fr);
  }
}

/*
 * A settings screen is a fact sheet, not a form (HUB.md §31, "the design settled 2026-09-02").
 * Three columns per row: the setting's name (muted), the truth with an optional muted sentence
 * under it, and one quiet verb on the right that opens the smallest possible inline form. Reuses
 * .hub-qa's row rhythm and hairline separators, same idea, one more column.
 */
.mail-fact-sheet {
  margin: 0;
}

.mail-fact-row {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) minmax(0, 1fr) auto;
  gap: 0.35rem 1.5rem;
  align-items: start;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--line-soft);
}

.mail-fact-row:last-child {
  border-bottom: 0;
}

.mail-fact-label {
  font-size: var(--hub-fs-sm);
  color: var(--muted);
  font-weight: 500;
  padding-top: 0.1rem;
}

.mail-fact-value {
  color: var(--ink);
  font-weight: 500;
}

.mail-fact-sub {
  display: block;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
  font-weight: 400;
  margin-top: 0.2rem;
}

.mail-fact-verb {
  text-align: right;
}

@media (max-width: 48rem) {
  .mail-fact-row {
    grid-template-columns: 1fr;
  }
  .mail-fact-verb {
    text-align: left;
  }
}

.hub-plain-list {
  list-style: none;
  padding: 0;
  margin: 0 0 1rem;
}

.hub-plain-list li {
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--line);
}

/*
 * Base breathing room for every field, not only inside `.hub-form`: a field with no ancestor form
 * class (e.g. a bare `<form>` around a single textarea + submit button) got zero margin-bottom and
 * the button rendered flush against the input above it. One rule, everywhere; `.hub-form-inline`
 * and the panel-scoped variants below still override it where a tighter or wider rhythm is wanted.
 */
.field {
  margin-bottom: 0.8rem;
}

.hub-form label {
  display: block;
  font-size: var(--hub-fs-sm);
  margin-bottom: 0.25rem;
  color: var(--ink);
  font-weight: 500;
}

.hub-form .optional {
  color: var(--muted);
  font-weight: 400;
}

/* Reassert the shared checkbox layout after the generic form-label rule above. */
.hub-form label.hub-check {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  margin: 0;
  padding: 0.45rem 0;
}

/* Text-like inputs only: checkboxes/radios keep their own sizing (.hub-check-input,
   .hub-seg) — otherwise this width:100%/pill rule stretches every checkbox into a
   full-width oval site-wide. */
.hub-form input:not([type="checkbox"]):not([type="radio"]),
.hub-form select,
.hub-form .form-control,
.hub-form .form-select {
  width: 100%;
  max-width: 100%;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  font: inherit;
  font-size: var(--hub-fs);
  line-height: 1.4;
  background: var(--white);
  color: var(--ink);
}

/*
 * Bootstrap input-group needs flex children, not width:100% (else % suffix wraps).
 *
 * The :not() pair is not decoration, it is the whole point: the width:100% rule above carries an
 * element selector and two :not()s, so a plain `.hub-form .input-group > input` loses to it and the
 * suffix or button drops onto its own line. That is what put "Copiar" under the diagnostic link.
 */
.hub-form .input-group > .form-control,
.hub-form .input-group > .form-select,
.hub-form .input-group > input:not([type="checkbox"]):not([type="radio"]),
.hub-form .input-group > select {
  width: 1%;
  max-width: none;
  flex: 1 1 auto;
  min-width: 0;
}
.hub-form .input-group > .input-group-text,
.hub-form .input-group > .btn {
  width: auto;
  max-width: none;
  flex: 0 0 auto;
}

.hub-form input:not([type="checkbox"]):not([type="radio"]):focus,
.hub-form select:focus {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
  border-color: var(--blue);
}

.form-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
/* Wizard step footer: back on the left, reset in the middle, the way forward on the right. The
   empty slot keeps those positions steady on the first step, which has nowhere to go back to. */
.form-actions-wizard {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
}
.form-actions-slot {
  flex: 0 0 auto;
}
/* Reset is a quiet escape hatch, not a third thing competing to be pressed. */
.form-actions-reset {
  color: var(--muted);
  text-decoration: none;
}
.form-actions-reset:hover {
  color: var(--ink);
  text-decoration: underline;
}

/*
 * ZONE 1 — "Informe de gestión". The eyebrow + today's date, the two count pairs (current and
 * previous 90-day window — never a percentage, workbench/HUB.md §7), and the by-kind row above the
 * existing year grid. The by-kind row reuses .hub-summary-* (opportunity-detail's Resumen row):
 * labels above values, hairline rhythm, no boxes.
 */
/* Office-status strip: whether the service clocks are running right now. A pause is not a failure,
   so it never uses the alarm colour; open is quiet, paused is a calm sage note that a consultant
   reads as "nothing is slipping while we are closed", not as an alert. */
.hub-office-status {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius-card);
  margin-bottom: 0.9rem;
  font-size: var(--hub-fs-sm);
}

.hub-office-status > .ph {
  align-self: center;
  font-size: 1rem;
}

.hub-office-status.is-open {
  color: var(--muted);
  background: transparent;
  padding-left: 0;
  padding-right: 0;
}

.hub-office-status.is-paused {
  color: var(--teal-deep);
  background: var(--green-tint);
  border: 1px solid var(--sage-soft);
}

.hub-office-status-headline {
  font-weight: 500;
}

.hub-office-status.is-open .hub-office-status-headline {
  font-weight: 400;
}

.hub-office-status-detail {
  color: var(--muted);
}

.hub-office-status.is-paused .hub-office-status-detail {
  color: var(--teal-mid);
}

@media (max-width: 991.98px) {

}

/*
 * The date half of a commitment/renewal row. hub-commitment-row itself (the date + what + owed
 * grid) and its Upcoming/Por-hacer siblings came out with Inicio's one-table rework (2026-08-30):
 * grep found no template still rendering them, only this date span survives, reused by the
 * renewal radar below.
 */
.hub-commitment-when {
  font-size: var(--hub-fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/*
 * The renewal radar (HUB.md §28) borrows the is-overdue/is-soon modifier idiom already
 * established on .kanban-card-due and .hub-espera-when, rather than a third pair of names for the
 * same two states.
 */
.hub-commitment-when.is-overdue {
  color: var(--danger, #c8452b);
  font-weight: 600;
}

.hub-commitment-when.is-soon {
  color: var(--sem-warm);
  font-weight: 600;
}

/*
 * Radar row (renovaciones): contract, action. A grid so the two columns line up across rows.
 */
.hub-renewal-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 1rem;
  align-items: center;
}

@media (max-width: 767.98px) {
  .hub-renewal-row {
    grid-template-columns: 1fr;
  }
}

/* Section icon beside a page title: muted and slightly smaller than the text, so it identifies
   the record without competing with its name. */
.hub-page-title-icon {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.hub-page-title-icon > .ph {
  font-size: 0.8em;
  color: var(--muted);
  flex: none;
}

/* ── Opportunity detail: identity, actions, the two columns ── */

/*
 * The state chip. The state is the record's position, so its control sits in the subtitle where a
 * reader already looks for it, not as a form in the action bar.
 */
.hub-state-picker {
  position: relative;
  display: inline-block;
}

.hub-state-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--white);
  padding: 0.05rem 0.5rem;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.hub-state-chip:hover,
.hub-state-chip:focus-visible {
  border-color: var(--sem-color, var(--ink));
}

.hub-state-chip .ph {
  font-size: 0.85em;
  color: var(--muted);
}

.hub-state-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 0.35rem);
  left: 0;
  display: grid;
  min-width: 11rem;
  padding: 0.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--white);
  box-shadow: var(--elev-1, 0 6px 20px rgba(21, 36, 60, 0.12));
}

.hub-state-menu button {
  border: 0;
  background: none;
  padding: 0.4rem 0.6rem;
  border-radius: 8px;
  text-align: left;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}

.hub-state-menu button:hover:not(:disabled) {
  background: var(--canvas);
}

.hub-state-menu button:disabled {
  color: var(--muted);
  cursor: default;
}

/*
 * A stage whose verdict is already recorded: still offered, because a mis-closed deal has to be
 * fixable, but marked so nobody changes one by reflex. Calm amber, never red — reopening is a
 * correction, not a destruction — and the word carries the meaning so the colour never has to.
 */
.hub-state-menu button.is-reopening {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.hub-state-menu-reopen {
  font-size: var(--hub-fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--warn-fg);
}

/* The title row: the record's name on the left, the one control that moves it on the right. It is
   the most consequential thing on the page, so it is sized like it rather than tucked into a
   subtitle. Wraps below the title on a narrow screen. */
.hub-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
}

.hub-state-picker-label {
  font-size: var(--hub-fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin-right: 0.5rem;
}

.hub-state-picker-lg .hub-state-chip {
  padding: 0.3rem 0.9rem;
  font-size: 1.05rem;
  font-weight: 600;
}

/* Right-aligned control, so the menu opens back under it rather than off the edge. */
.hub-state-picker-lg .hub-state-menu {
  left: auto;
  right: 0;
}

/* The macro heading. Not a button, because the macro is derived from whichever stage is picked. */
.hub-state-menu-group {
  margin: 0.5rem 0 0.15rem;
  padding: 0 0.6rem;
  font-size: var(--hub-fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.hub-state-menu-group:first-child {
  margin-top: 0.15rem;
}

/* A panel head's right-hand side: the "ver todas" link and the action that adds one. */
.hub-column-head-actions {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/* Rare and destructive actions, one click away: they do not earn permanent width beside the two
   buttons pressed all day. */
.hub-overflow {
  position: relative;
}

.hub-overflow > summary {
  list-style: none;
  cursor: pointer;
}

.hub-overflow > summary::-webkit-details-marker {
  display: none;
}

.hub-overflow-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 0.35rem);
  right: 0;
  display: grid;
  min-width: 12rem;
  padding: 0.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--white);
  box-shadow: var(--elev-1, 0 6px 20px rgba(21, 36, 60, 0.12));
}

.hub-overflow-menu a,
.hub-overflow-menu button {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  border: 0;
  background: none;
  padding: 0.45rem 0.6rem;
  border-radius: 8px;
  text-align: left;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
}

.hub-overflow-menu a,
.hub-overflow-menu a:hover {
  color: var(--ink);
  text-decoration: none;
}

.hub-overflow-menu a:hover,
.hub-overflow-menu button:hover {
  background: var(--canvas);
}

.hub-overflow-menu button.is-danger {
  color: var(--danger, #c8452b);
}

/* Resumen: what the opportunity *is*, in one row read left to right. Labels above values, hairline
   dividers between cells, no boxes — the row is a sentence, not five cards. */
.hub-summary-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2.5rem;
  align-items: flex-start;
}

.hub-summary-cell {
  display: grid;
  gap: 0.3rem;
}

.hub-summary-gauge {
  /* The gauge brings its own label and readout, so the cell only has to give it room. */
  min-width: 15rem;
}

.hub-summary-label {
  font-size: var(--hub-fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.hub-summary-value {
  font-weight: 500;
}

/*
 * La espera: a statement on the left, the one thing that can be resolved on the right.
 *
 * The one panel in the app that is not white. A deal in espera is stopped — nothing will move until
 * somebody delivers something — and rendering that as another white card among white cards said it
 * was ordinary. Pale amber and a warning glyph, which is the app's only anomaly treatment; red is
 * kept for destructive actions and would say "this is wrong" when waiting is often correct.
 */
.hub-espera {
  background: var(--sem-warm-tint);
  border-color: color-mix(in srgb, var(--sem-warm) 30%, transparent);
}

.hub-espera-title {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.hub-espera-title .ph {
  color: var(--sem-warm);
  font-size: 1.1em;
}

.hub-espera {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.hub-espera-when {
  margin-top: 0.35rem;
  font-size: var(--hub-fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.hub-espera-when.is-overdue {
  color: var(--danger, #c8452b);
}

.hub-espera-actions {
  flex: none;
}

/* The provenance line, above the actions: read once to trust the page, then ignored. */
.hub-last-move {
  margin: 0 0 0.75rem;
  font-size: var(--hub-fs-sm);
  color: var(--muted);
}

/*
 * A description is prose, so it gets prose measure rather than the full page width. Since V63
 * this holds the rendered HTML of a Markdown brief (MarkdownProse#toHtml), so it also carries the
 * typography for every element that Markdown can produce: headings, lists, a table, a quote, code.
 * The first/last child rule keeps the card's own padding as the only air above and below the text,
 * so a heading at the top of the brief does not add a second gap on top of the panel's.
 */
.hub-prose {
  max-width: 68ch;
  color: var(--body);
}

.hub-prose > :first-child {
  margin-top: 0;
}

.hub-prose > :last-child {
  margin-bottom: 0;
}

.hub-prose h1,
.hub-prose h2,
.hub-prose h3,
.hub-prose h4,
.hub-prose h5,
.hub-prose h6 {
  margin: 1.1rem 0 0.4rem;
  color: var(--ink);
  font-weight: 600;
  line-height: 1.3;
}

.hub-prose h1 {
  font-size: 1.05rem;
}

.hub-prose h2 {
  font-size: var(--hub-fs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.hub-prose h3,
.hub-prose h4,
.hub-prose h5,
.hub-prose h6 {
  font-size: var(--hub-fs);
}

.hub-prose p {
  margin: 0 0 0.6rem;
}

.hub-prose ul,
.hub-prose ol {
  margin: 0 0 0.6rem;
  padding-left: 1.25rem;
}

.hub-prose li {
  margin: 0.2rem 0;
}

.hub-prose li > ul,
.hub-prose li > ol {
  margin: 0.2rem 0;
}

.hub-prose strong {
  color: var(--ink);
  font-weight: 600;
}

.hub-prose a {
  color: var(--blue);
  text-underline-offset: 2px;
}

.hub-prose blockquote {
  margin: 0 0 0.6rem;
  padding: 0.05rem 0 0.05rem 0.85rem;
  border-left: 3px solid var(--line);
  color: var(--muted);
}

.hub-prose code {
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
  background: var(--line-soft);
  font-family: var(--font-mono);
  font-size: 0.85em;
}

/* A next-steps table is the one Markdown construct that outgrows prose measure on purpose: it
   gets the full card width, with the same airy, hairline-only rhythm as every other Hub table
   (.hub-table, .hub-datatable) rather than a browser default grid. */
.hub-prose table {
  width: 100%;
  max-width: none;
  margin: 0 0 0.75rem;
  border-collapse: collapse;
  font-size: var(--hub-fs-sm);
}

.hub-prose th,
.hub-prose td {
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--line-soft);
  text-align: left;
  vertical-align: top;
}

.hub-prose thead th {
  color: var(--muted);
  font-size: var(--hub-fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

/* One gestión in the summary: date, outcome, note, on one line. */
.hub-gestion-brief {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--line-soft);
  font-size: var(--hub-fs-sm);
}

@media (max-width: 991.98px) {

}

.hub-column-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
}

.hub-column-head a {
  font-size: var(--hub-fs-sm);
}

/* The .hub-entry* family lived here: one compromiso row, its head, meta, note, and late/due state
   pill. V27 replaced the compromiso with the espera, and no template has carried any of these
   classes since. Deleted 2026-08-04: a styled vocabulary for a retired model is how the next
   reader concludes the model is still supported. */

/*
 * The Markdown editor (EasyMDE, static/vendor/easymde) that edits a prose panel in place — the
 * opportunity's Descripción today, any future account brief later. It replaces the small edit
 * modal: full panel width, generous height, so a real note is written where it lives instead of
 * in a box built for a sentence.
 *
 * EasyMDE ships its own classes (.EasyMDEContainer, .editor-toolbar, .CodeMirror, …); this section
 * remaps them onto Hub's own tokens rather than inventing a parallel set, the same way the
 * DataTables override above styles `.dt-container` directly.
 */
.hub-markdown-editor-collapse {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line-soft);
}

.EasyMDEContainer .editor-toolbar {
  padding: 0.4rem 0.5rem;
  background: var(--paper);
  border: 1px solid var(--line);
  border-bottom: none;
  border-top-left-radius: 8px;
  border-top-right-radius: 8px;
}

.EasyMDEContainer .editor-toolbar button {
  height: 1.9rem;
  min-width: 1.9rem;
  border-radius: 6px;
  color: var(--body);
}

.EasyMDEContainer .editor-toolbar button.active,
.EasyMDEContainer .editor-toolbar button:hover {
  background: var(--white);
  border-color: var(--line);
  color: var(--ink);
}

.EasyMDEContainer .editor-toolbar i.separator {
  border-left-color: var(--line);
}

.EasyMDEContainer .CodeMirror {
  border: 1px solid var(--line);
  border-bottom-left-radius: 8px;
  border-bottom-right-radius: 8px;
  padding: 0.75rem 0.85rem;
  font: inherit;
  font-size: var(--hub-fs);
  color: var(--ink);
}

.EasyMDEContainer .CodeMirror-focused {
  outline: 2px solid var(--blue);
  outline-offset: 1px;
}

.EasyMDEContainer .editor-preview {
  background: var(--white);
}

/* ── Pipeline kanban (app-specific) ── */
/*
 * The board is sized for the column nobody planned for.
 *
 * It used to be a page-tall stack: every column shrink-wrapped its cards, so one column holding a
 * hundred deals made the whole document five thousand pixels tall and turned its five neighbours
 * into stubs beside an endless corridor. Measured at 1280x800 with twenty cards drawn in Decisión,
 * the page was 5210px and exactly one card sat above the fold.
 *
 * So the board owns a viewport-sized box and nothing outside it grows:
 *   .kanban-scroll  is that box, and scrolls sideways when there are more stages than fit
 *   .kanban-col     is a flex column, head pinned, body taking the rest
 *   .kanban-col-body scrolls on its own
 * A column with 150 cards is now exactly as tall as a column with none, its head (and the honest
 * total in it) never leaves the screen, and the last card is reachable by scrolling one lane
 * instead of the document.
 *
 * The height comes from pipeline-kanban.js, which measures where the board starts and writes
 * --kanban-fit-height. The fallback below keeps the board usable with no JavaScript at all.
 */
.kanban-scroll {
  /* Column geometry as tokens. The floor is the width at which a card stops being readable, and
     past it the board scrolls sideways rather than shrinking into confetti. 9.5rem is measured, not
     chosen: it is the widest floor at which today's six stages still all fit a 1280px laptop with
     no sideways scroll, which is the screen this board is opened on in a meeting. The card's height
     does not depend on it — every line is clamped — so a narrower column costs ellipsis, never
     rows. */
  --kanban-col-min: 9.5rem;
  --kanban-gap: 0.65rem;
  --kanban-card-pad: 0.4rem 0.5rem;
  overflow: auto;
  height: var(--kanban-fit-height, 34rem);
  -webkit-overflow-scrolling: touch;
  margin-inline: -0.15rem;
  padding-inline: 0.15rem;
  scrollbar-width: thin;
  overscroll-behavior: contain;
}

/* One column per stage, all of them on screen while they fit. The count is data
   (hub.opportunity_stage decides which stages draw a column), so the grid cannot name it:
   auto-flow column with equal fractions fits whatever the table holds. */
.kanban {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(var(--kanban-col-min), 1fr);
  gap: var(--kanban-gap);
  align-items: stretch;
  height: 100%;
}

.kanban-scroll .kanban {
  /* No floor: the grid already refuses to shrink a column below --kanban-col-min, and a fixed
     minimum here forced a scrollbar on a board that fitted. */
  min-width: 100%;
}

/*
 * No fill and no border. The lanes read as columns of white cards on the gray canvas, which is the
 * house rule (whitespace and hairlines, not boxes) and also what makes the drop highlight legible:
 * against a permanently tinted, permanently outlined column it was one more box among boxes.
 */
.kanban-col {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
}

.kanban-col-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0 0.15rem 0.45rem;
  flex: 0 0 auto;
}

.kanban-col-head h2 {
  margin: 0;
  font-size: var(--hub-fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The count is a link into the registry for this state, so a column can be capped without hiding
   anything. It reads as a badge until hovered, when it admits it is a way across. */
.kanban-count {
  font-size: var(--hub-fs-xs);
  background: var(--white);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  color: var(--ink);
  border: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}

.kanban-count:hover,
.kanban-count:focus-visible {
  color: var(--accent-strong, var(--ink));
  border-color: var(--accent, var(--line));
}

/* Pinned above the scrolling body on purpose: it states how much of the column you are looking at,
   which is not a fact to discover after scrolling past twenty cards. */
.kanban-truncated {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  margin: 0 0 0.4rem;
  padding: 0 0.15rem;
  flex: 0 0 auto;
}

.kanban-truncated a {
  color: var(--blue);
}

/*
 * The one scrolling surface per lane. The explicit `min-height` is what actually lets it scroll: a
 * flex item defaults to `min-height: auto`, which refuses to shrink below its content, so the lane
 * would grow to fit 150 cards and push the board open again — the bug this layout exists to remove.
 * Any explicit value overrides `auto`; 4rem is just a floor so an empty lane still reads as a lane.
 */
.kanban-col-body {
  flex: 1 1 auto;
  min-height: 4rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  padding: 0.15rem;
  border-radius: 10px;
  transition: background 0.15s ease, box-shadow 0.15s ease;
}

.kanban-col.is-drop-target .kanban-col-body {
  background: color-mix(in srgb, var(--blue-deep) 8%, var(--white));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue-deep) 35%, var(--line));
}

/*
 * Two rows and a bounded height. The card used to measure 197px because nothing in it was bounded:
 * a name wrapped to four lines, "TRACKVY · ADQUISICION" wrapped to three more, and the countdown
 * took two. Every line is clamped or ellipsed now, so a card cannot surprise the column it sits in,
 * and the height is a design decision rather than a consequence of the longest name in the database.
 */
.kanban-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  background: var(--white);
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  padding: var(--kanban-card-pad, 0.4rem 0.5rem);
  margin-bottom: 0.35rem;
  cursor: grab;
  user-select: none;
  transition: border-color 140ms ease, box-shadow 140ms ease;
}

/* A hover lift is this app's standing signal for "clickable", and a card is: it opens the
   opportunity. Shadow only, never a transform — the card is also a drag source, and lifting it
   under the cursor at the moment a drag starts reads as the drag having gone wrong. */
.kanban-card:hover,
.kanban-card:focus-within {
  border-color: var(--line);
  box-shadow: var(--elev-1, 0 4px 14px rgba(21, 36, 60, 0.09));
}

@media (prefers-reduced-motion: reduce) {
  .kanban-card {
    transition: none;
  }
}

.kanban-card:active {
  cursor: grabbing;
}

.kanban-card.is-dragging {
  opacity: 0.55;
}

/* Row 1: what it is, and how warm it is. */
.kanban-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.3rem;
  min-width: 0;
}

.kanban-card-name {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-weight: 600;
  font-size: var(--hub-fs-sm);
  line-height: 1.25;
  color: var(--ink);
  text-decoration: none;
}

/* The clamp lives on the inner span, never on the anchor: `overflow: hidden` on the anchor would
   clip ::after, the overlay that makes the whole card open the opportunity, down to the width of
   the words. The card would look right and only the text would be clickable. */
.kanban-card-name > span {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.kanban-card-name::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.kanban-card-name:hover {
  color: var(--navy);
}

.kanban-card-name:focus-visible::after {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* The compact temperature chip is sized for this row. 1.75rem set the height of every card whose
   name fitted on one line, which is most of them. */
.kanban-card .sem-badge-compact {
  width: 1.15rem;
  height: 1.15rem;
}

.kanban-card .sem-badge-compact i {
  font-size: 0.95rem;
}

/*
 * Row 2: whose it is, and the one clock that matters, with the console trigger at the end. The
 * company gives up width first (it is the item a reader can also see in the column they are
 * scanning); the clock never truncates, because a half-printed date is worse than none.
 */
.kanban-card-foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 0.35rem;
  margin: 0;
  min-width: 0;
  font-size: var(--hub-fs-xs);
  line-height: 1.35;
  color: var(--muted);
}

.kanban-card-company {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--hub-fs-xs);
  color: var(--blue-deep);
  text-decoration: none;
  font-weight: 500;
}

.kanban-card-company:hover {
  color: var(--navy);
  text-decoration: underline;
}

.kanban-card-countdown {
  margin: 0;
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}

/* The sub-state, in the countdown's place. Muted and small: it is the absence of a reminder, not
   an alarm, and the card is still in its column because its state has not changed. */
/* A promise owed today or earlier, in the countdown's place. Inked rather than muted: unlike the
   forecast it replaces, this one is a fact about something somebody agreed to. */
/*
 * A commitment, coloured by how close it is. It used to be red the moment a date existed, which
 * made a promise for next month look like an emergency and taught everyone that red on this card
 * means nothing. Red is now reserved for a date that has passed.
 */
.kanban-card-due {
  flex: 0 0 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  font-weight: 500;
}

.kanban-card-due.is-soon {
  color: var(--sem-warm);
  font-weight: 600;
}

.kanban-card-due.is-overdue {
  color: var(--sem-hot);
  font-weight: 600;
}

.kanban-card-substate {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  min-width: 0;
  gap: 0.25rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}

.kanban-card-substate .ph {
  font-size: 0.95em;
}

/* The console triggers stay quiet until the card is engaged with: a board of cards each shouting
   two icons is noisier than the information on the cards themselves. */
/* Above .kanban-card-name::after, which is a stretched link covering the whole card. Without this
   the overlay sits on top of the buttons and every click on them opens the opportunity instead, and a scripted .click() still works, so it passes a JS check and fails a real one. */
.hub-registro-triggers {
  position: relative;
  z-index: 2;
  display: inline-flex;
  gap: 0.15rem;
  margin-left: auto;
}

/* Same reason: the company link is a real link and must beat the card-wide overlay. */
.kanban-card-company {
  position: relative;
  z-index: 2;
}

/* Sized to the card's dense second line rather than to a toolbar. At the shared 2rem the trigger
   alone set the height of that row, so every card in the board paid 32px for a button that is
   invisible until the card is hovered. */
.kanban-card .hub-icon-btn {
  width: 1.4rem;
  height: 1.4rem;
  font-size: 1rem;
  border-radius: 6px;
}

.kanban-card .hub-registro-triggers {
  opacity: 0;
  transition: opacity 120ms ease;
  /* The shared rule pushes the trigger to the far right of its row. On the card's first row it
     belongs beside the temperature chip, not with a gap punched between them. */
  margin-left: 0;
  flex: 0 0 auto;
}

.kanban-card:hover .hub-registro-triggers,
.kanban-card:focus-within .hub-registro-triggers {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .kanban-card .hub-registro-triggers {
    transition: none;
  }
}

/* Coarse pointers have no hover, so the triggers are always visible there. */
@media (hover: none) {
  .kanban-card .hub-registro-triggers {
    opacity: 1;
  }
}

/* Perdido has no column. This target appears only while a card is in flight (is-armed), so the
   board never carries a standing invitation to lose a deal.

   Fixed to the viewport rather than placed after the columns: as a block at the bottom of the board
   it sat below the fold, so a consultant mid-drag could neither see it nor reach it. */
.kanban-discard {
  position: fixed;
  left: 50%;
  bottom: 1.25rem;
  transform: translate(-50%, 0.75rem);
  z-index: 1040;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.85rem 1.5rem;
  border: 1px dashed var(--line);
  border-radius: 999px;
  background: var(--white);
  box-shadow: var(--elev-1, 0 6px 20px rgba(21, 36, 60, 0.12));
  color: var(--muted);
  font-size: var(--hub-fs-sm);
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transition: opacity 120ms ease, transform 120ms ease;
}

.kanban-discard.is-armed {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
}

.kanban-discard.is-drop-target {
  border-style: solid;
  border-color: var(--danger, #c8452b);
  color: var(--danger, #c8452b);
}

@media (prefers-reduced-motion: reduce) {
  .kanban-discard {
    transition: none;
  }
}

.kanban-empty {
  margin: 0.5rem;
  font-size: var(--hub-fs-sm);
  color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  .kanban-col-body {
    transition: none;
  }
}

/*
 * Kanban card semáforo: a subtle left-edge accent on a white card plus the pastel compact badge.
 * GANADO/PERDIDO cards get neither class, since a closed deal has no semáforo.
 *
 * Drawn as ::before rather than an inset box-shadow so the card's one box-shadow slot stays free
 * for elevation: as a shadow, the hover lift and the temperature were the same property and the
 * one that won erased the other.
 */
.kanban-card.sem-hot,
.kanban-card.sem-warm,
.kanban-card.sem-cool,
.kanban-card.sem-cold,
.kanban-card.sem-frozen {
  background: var(--white);
}

.kanban-card.sem-hot::before,
.kanban-card.sem-warm::before,
.kanban-card.sem-cool::before,
.kanban-card.sem-cold::before,
.kanban-card.sem-frozen::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  border-radius: 10px 0 0 10px;
  background: color-mix(in srgb, var(--sem-color) 72%, var(--white));
}

/*
 * Opportunity temperature gauge — fragments/semaforo.html :: gauge. A calm five-step ladder
 * (Caliente → Glaciar); the active step lights up, the rest stay muted paper-gray.
 */
.sem-gauge {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.sem-gauge-ladder {
  position: relative;
  display: flex;
  gap: 0.3rem;
  padding-top: 0.6rem;
}

.sem-gauge-step {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 2.5rem;
  border-radius: 8px;
  background: var(--paper);
  color: var(--line);
  opacity: 0.6;
  transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}

.sem-gauge-step i {
  font-size: 1.5rem;
  line-height: 1;
  /* Only the Phosphor Regular weight is vendored (no Bold font file), so a thicker stroke stands
     in for a true bold glyph. Drop this if it ever looks rough at a given size. */
  -webkit-text-stroke: 0.4px currentColor;
  text-stroke: 0.4px currentColor;
}

.sem-gauge-step.is-active {
  opacity: 1;
  background: var(--sem-tint);
  color: var(--sem-color);
}

.sem-gauge-readout {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.sem-gauge-label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  width: fit-content;
  font-weight: 600;
  font-size: var(--hub-fs);
  color: var(--sem-color);
}

.sem-gauge-label i {
  font-size: 1.125rem;
  line-height: 1;
  -webkit-text-stroke: 0.4px currentColor;
  text-stroke: 0.4px currentColor;
}

.sem-gauge-days {
  margin: 0;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
}

@media (prefers-reduced-motion: reduce) {
  .sem-gauge-step {
    transition: none;
  }
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

/* ── Breakpoints (desktop-first; best-effort menus & chrome) ── */
@media (max-width: 1199.98px) {
  .hub-main {
    padding: 1.1rem 1.15rem 1.75rem;
  }
}

@media (max-width: 991.98px) {
  /* Sidebar is display:none via Bootstrap d-lg-*; shell is single column */
  .hub-main {
    padding: 1rem 1rem 1.5rem;
  }

  .page-header h1 {
    font-size: 1.15rem;
  }

  /* .stat-row is a grid, so the tracks already size themselves; the card itself stays min-width: 0
     so its content (the stat value) can shrink to the track instead of overflowing it. */
  /* A card that opens what it counts. The lift is the app's standing signal for "clickable", so the
   TRM card — which counts nothing you can open — deliberately does not get it. */
a.stat-card.is-linked {
  color: inherit;
  text-decoration: none;
  transition: transform 120ms ease, box-shadow 120ms ease;
}

a.stat-card.is-linked:hover,
a.stat-card.is-linked:focus-visible {
  transform: translateY(-1px);
  box-shadow: var(--elev-1, 0 6px 20px rgba(21, 36, 60, 0.12));
}

.stat-card {
    min-width: 0;
  }
}

@media (max-width: 767.98px) {

  .hub-main {
    padding: 0.85rem 0.85rem 1.5rem;
  }

  .page-header-row {
    flex-direction: column;
    align-items: stretch;
  }

  .page-header-row > .btn {
    width: 100%;
    justify-content: center;
  }

  .hub-detail-toolbar {
    flex-direction: column;
    align-items: stretch;
  }

  .hub-detail-toolbar-end {
    margin-left: 0;
    width: 100%;
  }

  .hub-detail-toolbar .btn {
    justify-content: center;
  }

  .hub-detail-toolbar-end .btn {
    flex: 1 1 auto;
  }

  .action-row {
    width: 100%;
  }

  .action-row .btn {
    flex: 1 1 auto;
    justify-content: center;
  }

  .form-actions {
    flex-wrap: wrap;
  }

  .form-actions .btn {
    flex: 1 1 auto;
  }

  /* .hub-form-inline is flex + flex-wrap, not grid (see the canonical rule): each field already
     wraps to its own full-width line on a narrow viewport via its own flex-basis, so nothing here
     needs to force it. */

  /* Below the sidebar breakpoint the board keeps its readable column floor and scrolls sideways;
     the grid's own minmax already guarantees that, so this only stops a two-column board from
     stretching two lanes across a phone. */
  .kanban-scroll .kanban {
    min-width: 52rem;
  }
}

@media (max-width: 575.98px) {
  .stat-row {
    gap: 0.5rem;
  }

  .hub-table th,
  .hub-table td {
    padding: 0.45rem 0.55rem;
    font-size: var(--hub-fs-sm);
  }

  div.dt-container.dt-bootstrap5 table.dataTable.table > :not(caption) > * > * {
    padding: 0.45rem 0.55rem;
  }
}

/* ── Status & elevation tokens ─────────────────────────────────────────────
   Semantic feedback colors composed from brand/accendo.css primitives, plus
   one restrained elevation. Reuse across the app instead of inline hexes. */
:root {
  --elev-1: 0 1px 2px rgba(11, 23, 38, 0.05), 0 10px 26px -14px rgba(11, 23, 38, 0.16);
  --ok-bg: #E6F7F1;
  --ok-fg: #0D7A55;
  --ok-line: #B7E4D4;
  --warn-bg: #FFF6E5;
  --warn-fg: #9A6B00;
  --warn-line: #F0D9A0;
  --bad-bg: #FDECEC;
  --bad-fg: #A32020;
  --bad-line: #F0B8B8;
}

/* ── Quote draft wizard (Web Flow + HTMX) ─────────────────────────────────── */
/*
 * Connected stepper rail: each step is its own POST <form> (real navigation
 * target) and also owns half of the connector line on each side, so two
 * adjoining halves always resolve to the same color — no two-tone seams.
 * States (done / current / ahead) are computed in the template from
 * s.order() vs step.order() and applied as a class on the form.
 */
.quote-wizard-nav {
  display: flex;
  align-items: flex-start;
  margin: 0 0 1.25rem;
  padding: 0.4rem 0.4rem 0;
}
.quote-wizard-pill-form {
  flex: 1 1 0;
  position: relative;
  margin: 0;
  padding-top: 0.35rem;
}
.quote-wizard-pill-form::before,
.quote-wizard-pill-form::after {
  content: "";
  position: absolute;
  top: calc(0.35rem + 14px);
  height: 2px;
  background: var(--line);
}
.quote-wizard-pill-form::before { left: 0; right: 50%; }
.quote-wizard-pill-form::after { left: 50%; right: 0; }
.quote-wizard-pill-form:first-child::before,
.quote-wizard-pill-form:last-child::after {
  display: none;
}
.quote-wizard-pill-form.is-done::before,
.quote-wizard-pill-form.is-done::after {
  background: var(--blue);
}
.quote-wizard-pill-form.is-current::before {
  background: var(--blue);
}
.quote-wizard-pill {
  position: relative;
  z-index: 1;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.42rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.quote-wizard-pill:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 3px;
  border-radius: 8px;
}
.quote-wizard-body.panel {
  margin-bottom: 0;
  padding: 1.15rem 1.25rem 1.25rem;
  box-shadow: var(--elev-1);
}
.quote-step-title {
  margin: 0 0 0.35rem;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
}
.quote-form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 0.85rem 1rem;
  margin-bottom: 0.75rem;
}
/*
 * Step dot: number by default, swaps to a check icon once done. The "ahead"
 * state is the unstyled baseline (white dot, muted number/label); done and
 * current below are the only states that need explicit overrides.
 */
.quote-wizard-step-dot {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--white);
  border: 2px solid var(--line);
  color: var(--muted);
  font-size: var(--hub-fs-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease;
}
.quote-wizard-step-dot .ph {
  font-size: 0.8rem;
}
.quote-wizard-pill:hover .quote-wizard-step-dot {
  border-color: var(--blue);
}
.quote-wizard-pill-form.is-done .quote-wizard-step-dot {
  background: var(--navy-tint);
  border-color: var(--blue);
  color: var(--blue);
}
.quote-wizard-pill-form.is-current .quote-wizard-step-dot {
  background: var(--navy);
  border-color: var(--navy);
  color: var(--white);
  /* Ring at --blue (73,141,190) 18% alpha; no rgb-triplet token exists for it. */
  box-shadow: 0 0 0 4px rgba(73, 141, 190, 0.18);
}
.quote-wizard-pill-label {
  font-size: var(--hub-fs-sm);
  color: var(--muted);
  font-weight: 500;
  text-align: center;
}
.quote-wizard-pill-form.is-current .quote-wizard-pill-label {
  color: var(--ink);
  font-weight: 700;
}
.quote-wizard-pill-chip {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.15rem 0.4rem;
  border-radius: 4px;
  background: var(--line-soft, #EFF1F4);
  color: var(--muted);
}
.quote-wizard-pill-chip.is-ready { background: var(--ok-bg); color: var(--ok-fg); }
.quote-wizard-pill-chip.is-incomplete { background: var(--warn-bg); color: var(--warn-fg); }
.quote-wizard-pill-chip.is-na { background: var(--line-soft); color: var(--muted); }
.quote-wizard-pill-chip.is-blocked { background: var(--bad-bg); color: var(--bad-fg); }

.quote-money-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem 1.75rem;
  align-items: baseline;
  margin-bottom: 1rem;
}
.quote-money-strip.panel {
  box-shadow: var(--elev-1);
}
.quote-money-label {
  display: block;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.quote-money-value { font-weight: 600; color: var(--ink); }
/* Subtotal / IVA / Total read at one equal size; Total is distinguished by
   navy color + weight only, never by size. */
.quote-money-total .quote-money-value { color: var(--navy); font-weight: 700; }
/* TRM reference stat: smaller and muted, deliberately not part of the three
   equal-size money values above. */
.quote-money-trm .quote-money-value {
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  color: var(--muted);
}
/* Single readiness verdict pill, appended at the end of the strip. */
.quote-verdict {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  align-self: center;
  margin-left: auto;
  padding: 0.42rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--white);
  color: var(--ink);
  font-size: var(--hub-fs-sm);
  font-weight: 600;
  white-space: nowrap;
}
.quote-verdict .ph {
  font-size: 1rem;
}
.quote-verdict.is-ok { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok-line); }
.quote-verdict.is-warn { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-line); }
.quote-verdict.is-bad { background: var(--bad-bg); color: var(--bad-fg); border-color: var(--bad-line); }

.quote-group-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.65rem;
}
.quote-kit-controls {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-left: auto;
}
.quote-kit-quantity-form {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0;
}
.quote-kit-quantity-form label {
  margin: 0;
  color: var(--muted);
  font-size: var(--hub-fs-xs);
  font-weight: 600;
}
.quote-kit-quantity-form .form-control { width: 4.25rem; }
.quote-kit-note {
  margin: -0.25rem 0 0.7rem;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
}
.quote-derived-values {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.6rem;
  margin-top: 0.25rem;
  color: var(--muted);
  font-family: var(--font-mono, monospace);
  font-size: var(--hub-fs-xs);
}
.quote-doc-check {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  padding: 0.45rem 0;
  font-size: var(--hub-fs-sm);
}
.quote-doc-check.is-ok { color: var(--ok-fg); }
.quote-doc-check.is-off { color: var(--bad-fg); }
/* TRM provenance chip (LIVE / STALE / FALLBACK) on draft wizard masthead */
.quote-trm-status {
  display: inline-block;
  margin-left: 0.25rem;
  padding: 0.05rem 0.35rem;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: 4px;
  vertical-align: middle;
}
.quote-trm-status.is-live { background: var(--ok-bg); color: var(--ok-fg); }
.quote-trm-status.is-stale { background: var(--warn-bg); color: var(--warn-fg); }
.quote-trm-status.is-fallback { background: var(--bad-bg); color: var(--bad-fg); }
.cot-dot { margin: 0 0.25rem; color: var(--muted); }

/* ── Company create (one screen) ─ */
.company-create-panel {
  max-width: 40rem;
}
/* Spreadsheet plantilla / export / import (master data) */
.spreadsheet-io-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.65rem 1rem;
  margin-bottom: 1rem;
  padding: 0.65rem 0.9rem;
}
.spreadsheet-io-bar-label {
  margin: 0;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.spreadsheet-io {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.4rem;
}
.spreadsheet-io-switcher {
  align-items: center;
}
.spreadsheet-io-resource-field {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.spreadsheet-io-resource-label {
  font-size: var(--hub-fs-sm);
  font-weight: 600;
  color: var(--ink);
}
.spreadsheet-io-resource-select {
  width: auto;
  min-width: 9.5rem;
}
.spreadsheet-io-import {
  position: relative;
}
.spreadsheet-io-import > summary {
  list-style: none;
  cursor: pointer;
}
.spreadsheet-io-import > summary::-webkit-details-marker {
  display: none;
}
.spreadsheet-io-form {
  position: absolute;
  right: 0;
  top: calc(100% + 0.35rem);
  z-index: 20;
  min-width: 18rem;
  padding: 0.85rem 1rem;
  box-shadow: var(--elev-1, 0 4px 18px rgba(15, 23, 42, 0.08));
}
.spreadsheet-io-help {
  position: relative;
}
.spreadsheet-io-help > summary {
  list-style: none;
  cursor: pointer;
}
.spreadsheet-io-help > summary::-webkit-details-marker {
  display: none;
}
.spreadsheet-io-help-body {
  flex-basis: 100%;
  margin-top: 0.5rem;
  padding: 0.85rem 1rem;
}
.spreadsheet-io-help-rules {
  margin: 0.5rem 0 0;
  padding-left: 1.15rem;
  font-size: var(--hub-fs-sm);
}
.spreadsheet-io-errors {
  margin-bottom: 1rem;
}
.spreadsheet-io-error-list {
  margin: 0.5rem 0 0;
  padding-left: 1.15rem;
  font-size: var(--hub-fs-sm);
}
.spreadsheet-io-error-list li {
  margin-bottom: 0.25rem;
}

@media (max-width: 575.98px) {
  .spreadsheet-io-switcher {
    align-items: stretch;
  }
  .spreadsheet-io-resource-field {
    width: 100%;
  }
  .spreadsheet-io-resource-select {
    flex: 1 1 auto;
  }
}
.hub-perm-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}
.hub-admin-access-form {
  max-width: 66rem;
}
.hub-admin-access-panel {
  max-width: none;
}
.hub-admin-identities-table {
  max-width: 100%;
}
.hub-admin-meta {
  padding: 0.65rem 0.75rem;
  background: var(--paper, #f4f4f5);
  border-radius: 0.35rem;
}
.panel-form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 1.5rem;
  padding: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
}

/* ── RUES (company form + registry card) ───────────────────────────────── */
.rues-nit-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.rues-nit-row .form-control { flex: 1 1 12rem; min-width: 10rem; }
.rues-dv-label {
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.rues-dv-value {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-weight: 600;
  min-width: 1.25rem;
  text-align: center;
}
/* The client-side lookup widget (rues-lookup.js / fragments/rues-lookup.html): a button plus an
   inline status line, never a separate indicator element — the status text itself says
   "Consultando…" while the request is in flight. */
.rues-lookup {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.rues-lookup-status {
  font-size: var(--hub-fs-sm);
  color: var(--muted);
}
.rues-box {
  display: flex;
  gap: 0.65rem;
  align-items: flex-start;
  padding: 0.75rem 0.9rem;
  border-radius: 8px;
  margin-bottom: 0.65rem;
  font-size: var(--hub-fs-sm);
}
.info-box.rues-box {
  background: var(--navy-tint, #eef2f7);
  color: var(--ink);
}
.warning-box.rues-box {
  background: #fff6e5;
  color: #5c4200;
  border: 1px solid #f0d9a0;
}
.rues-box .box-icon { font-size: 1.15rem; flex-shrink: 0; margin-top: 0.1rem; }
.rues-box .box-title { font-weight: 600; margin-bottom: 0.25rem; }
.rues-card {
  display: grid;
  gap: 0.35rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--line-soft, #e5e7eb);
  border-radius: 8px;
  background: #fff;
  margin-bottom: 0.65rem;
}
.rues-card .meta-row {
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: 0.5rem;
  font-size: var(--hub-fs-sm);
}
.rues-card .field-label {
  color: var(--muted);
  font-weight: 500;
}
/* ── Shared typeahead (company / catalog party) ───────────────────────────── */
.hub-typeahead {
  position: relative;
}
.hub-typeahead-menu {
  position: absolute;
  z-index: 40;
  left: 0;
  right: 0;
  top: calc(100% + 2px);
  max-height: 16rem;
  overflow-y: auto;
  margin: 0;
  padding: 0.25rem 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--white);
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.1);
}
.hub-typeahead-opt {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  width: 100%;
  margin: 0;
  padding: 0.45rem 0.75rem;
  border: 0;
  background: transparent;
  text-align: left;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}
.hub-typeahead-opt:hover,
.hub-typeahead-opt.is-active {
  background: var(--navy-tint, #E7EDF4);
}
.hub-typeahead-opt-label {
  font-weight: 500;
}
.hub-typeahead-opt-meta {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}
.hub-typeahead-empty {
  margin: 0;
  padding: 0.55rem 0.75rem;
  font-size: var(--hub-fs-sm);
  color: var(--muted);
}
/* Empty-state "Crear «nombre»" affordance (companyCreatable only — opt-in per usage) and the
   inline create panel it reveals. */
.hub-typeahead-create {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  margin: 0;
  padding: 0.5rem 0.75rem;
  border: 0;
  border-top: 1px solid var(--line-soft, #e5e7eb);
  background: transparent;
  text-align: left;
  font: inherit;
  font-weight: 600;
  color: var(--navy);
  cursor: pointer;
}
.hub-typeahead-create:hover {
  background: var(--navy-tint, #E7EDF4);
}
.hub-company-create {
  margin-top: 0.5rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-soft, #e5e7eb);
  border-radius: 8px;
  background: var(--white);
}
.hub-company-create-lead {
  margin: 0 0 0.65rem;
  font-size: var(--hub-fs-sm);
  color: var(--muted);
}
.hub-company-create .field:last-of-type {
  margin-bottom: 0;
}

/* ── Consultant quote wizard bodies ───────────────────────────────────────── */
.quote-step-title {
  margin: 0 0 0.35rem;
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--ink);
  font-family: var(--font-sans);
}
.quote-step-lead {
  margin: 0 0 1.25rem;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
  max-width: 42rem;
  line-height: 1.5;
}
.quote-section-label {
  margin: 1.25rem 0 0.65rem;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.quote-section-label:first-child { margin-top: 0; }
.quote-empty {
  padding: 1.5rem 0;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
}

/* Segmented control (zero-JS): styled radios for two/three exclusive choices. */
.hub-seg {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--line-soft);
  border-radius: 11px;
}
.hub-seg label { margin: 0; cursor: pointer; }
.hub-seg input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.hub-seg span {
  display: block;
  padding: 0.34rem 0.85rem;
  border-radius: 8px;
  font-size: var(--hub-fs-sm);
  color: var(--body);
  font-weight: 500;
  transition: background 0.12s ease, color 0.12s ease;
}
.hub-seg input:checked + span {
  background: var(--white);
  color: var(--navy);
  font-weight: 600;
  box-shadow: 0 1px 0 rgba(14, 42, 69, 0.06);
}
.hub-seg input:focus-visible + span { outline: 2px solid var(--blue); outline-offset: 2px; }

/* Segmented control, multi-select variant: same pill track/tokens as .hub-seg, but each
   option is an independent checkbox so more than one can be active at once. */
.hub-seg-multi {
  display: flex;
  flex-wrap: wrap;
  width: fit-content;
  max-width: 100%;
  padding: 3px;
  gap: 2px;
  background: var(--line-soft);
  border-radius: 11px;
}
.hub-seg-multi label { margin: 0; cursor: pointer; }
.hub-seg-multi input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.hub-seg-multi span {
  display: block;
  padding: 0.34rem 0.85rem;
  border-radius: 8px;
  font-size: var(--hub-fs-sm);
  color: var(--body);
  font-weight: 500;
  transition: background 0.12s ease, color 0.12s ease;
}
.hub-seg-multi input:checked + span {
  background: var(--white);
  color: var(--navy);
  font-weight: 600;
  box-shadow: 0 1px 0 rgba(14, 42, 69, 0.06);
}
.hub-seg-multi input:focus-visible + span { outline: 2px solid var(--blue); outline-offset: 2px; }

/* Bound-company card at the top of Encabezado. */
.quote-company-card {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.85rem 1rem;
  margin-bottom: 1.25rem;
  border: 1px solid var(--line-soft);
  border-radius: 12px;
  background: var(--surface-soft, #fafbfc);
}
.quote-company-card .qc-ico {
  width: 38px;
  height: 38px;
  border-radius: 10px;
  background: var(--navy-tint);
  color: var(--navy);
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}
.quote-company-card .qc-ico .ph { font-size: 1.2rem; }
.quote-company-card .qc-info { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.quote-company-card .qc-label {
  font-size: var(--hub-fs-xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
.quote-company-card .qc-name { font-size: 1rem; font-weight: 600; color: var(--ink); }
.quote-company-card .qc-meta {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  display: flex;
  gap: 0.45rem;
  flex-wrap: wrap;
  align-items: center;
}
.quote-company-card .qc-meta .sep { color: var(--line); }
.quote-company-card .qc-actions { margin-left: auto; }

/* Header step: two columns (encuadre | condiciones) */
.quote-header-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem 1.5rem;
  margin-bottom: 0.25rem;
}
@media (max-width: 900px) {
  .quote-header-grid { grid-template-columns: 1fr; }
}
.quote-header-panel {
  margin: 0;
  padding: 1.1rem 1.15rem 1.2rem;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--surface-soft, #fafbfc);
}
.quote-header-panel-title {
  margin: 0 0 1rem;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink);
}
.quote-header-panel .field { margin-bottom: 0.9rem; }
.quote-header-panel .field:last-child { margin-bottom: 0; }
.quote-delivery-range {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.5rem;
}
.quote-delivery-range .form-control {
  width: 5.5rem;
  max-width: 5.5rem;
  flex: 0 0 auto;
}
.quote-delivery-sep {
  color: var(--muted);
  font-size: var(--hub-fs-sm);
}
.quote-delivery-unit {
  color: var(--muted);
  font-size: var(--hub-fs-sm);
}
.quote-milestones-block {
  margin-top: 1.25rem;
  padding-top: 0.15rem;
}
.quote-milestone-rows {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  margin-bottom: 0.75rem;
}
.quote-milestone-row {
  display: grid;
  grid-template-columns: 7rem 1fr auto;
  gap: 0.5rem;
  align-items: center;
}
.quote-milestone-row .hub-percent-input {
  max-width: none;
  width: 100%;
}
.quote-ms-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
}
.quote-ms-head-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.4rem;
}
.quote-ms-sum {
  font-size: var(--hub-fs-sm);
  font-weight: 600;
  color: var(--muted);
}
.quote-ms-sum.is-ok { color: var(--ok-fg); }
.quote-ms-del {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.quote-ms-del:hover { color: var(--bad-fg); background: var(--bad-bg); }
.quote-ms-add {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.32rem 0.7rem;
  border: 1px dashed var(--line);
  border-radius: 8px;
  background: var(--paper);
  color: var(--blue-deep);
  font: inherit;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  cursor: pointer;
}
.quote-ms-add:hover { border-color: var(--blue-soft); }

/* Artículos editable line: labeled fields wrap in one row so each input is obvious. */
.quote-line-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.35rem 0.7rem;
}
.quote-line-fields .quote-field {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin: 0;
}
.quote-line-fields .quote-field > span {
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  color: var(--muted);
  letter-spacing: 0.02em;
}
.quote-field-desc { flex: 1 1 13rem; }
.quote-field-num .form-control { width: 5.5rem; }
.quote-recur-controls {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  height: calc(1.5em + 0.5rem + 2px); /* match form-control-sm height so the row aligns */
}
.quote-recur-controls .form-control { width: 4.5rem; }
.quote-line-save { align-self: flex-end; }
.quote-basis-cell { min-width: 7rem; }
.quote-price-hints {
  margin-top: 0.3rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.quote-price-hint {
  margin: 0;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  white-space: nowrap;
}
.quote-price-hint > span:first-child { margin-right: 0.25rem; }

/* Revisión: read-only content preview (grouped lines + totals + forma de pago + condiciones). */
.quote-review-group-title {
  font-size: var(--hub-fs-sm);
  font-weight: 600;
  color: var(--ink);
  margin: 1rem 0 0.4rem;
}
.quote-review-group:first-of-type .quote-review-group-title { margin-top: 0; }
.quote-review-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem 2.5rem;
  margin-top: 1.25rem;
}
.quote-review-block { flex: 1 1 12rem; min-width: 0; }
.quote-review-total { font-weight: 600; color: var(--navy); }
.quote-review-notes {
  margin-top: 0.5rem;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
  white-space: pre-line;
}

.quote-chip {
  display: inline-block;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.12rem 0.4rem;
  border-radius: 4px;
  background: var(--navy-tint, #E7EDF4);
  color: var(--navy);
}
/*
 * A chip standing beside buttons rather than inside a table cell.
 *
 * .quote-chip is sized for a dense row, where 10px uppercase is right. Dropped into a page header
 * next to full-height buttons it reads as a leftover fragment: too small to be a peer, too dark to
 * ignore. This keeps the same colour and case and gives it the buttons' own height and padding.
 */
.page-header-actions .quote-chip,
.action-row .quote-chip {
  align-self: center;
  padding: 0.45rem 0.75rem;
  font-size: 0.75rem;
  border-radius: 6px;
}

.quote-chip-muted {
  background: var(--line-soft);
  color: var(--muted);
}
.quote-chip-warn {
  background: var(--warn-bg);
  color: var(--warn-fg);
}
.quote-chip-ok {
  background: var(--ok-bg);
  color: var(--ok-fg);
}

/* Status pills sitting next to buttons in a page-header action row (e.g. diagnostic detail):
   .action-row stretches its flex children to a common height by default, which distorts a
   .quote-chip's small fixed padding. This wrapper centers the pills on the button row and keeps
   a clear gap before the buttons that follow. */
.hub-status-pills {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-right: 0.5rem;
}

/* Two date inputs plus a separator, sitting in a page-header action row (e.g. the Registro
   table's Cuándo range) — the one column whose funnel is a range instead of the shared
   Excel-like "contiene" popover, so it lives beside the Excel export button instead of on the
   column header. */
.hub-daterange {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.hub-daterange input[type="date"] {
  width: auto;
}
.hub-daterange-sep {
  color: var(--ink-soft, #6b7280);
}

/* Unified add bar: one kind switch + one row of fields */
.quote-addbar {
  margin: 0 0 1.5rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--surface-soft, #fafbfc);
}
.quote-addbar .eyebrow { margin-bottom: 0.65rem; }
.quote-addbar-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.55rem 0.65rem;
}
.quote-addbar-kind-field {
  flex: 0 0 auto;
  min-width: 8.5rem;
  margin: 0;
}
.quote-addbar-kind-field label {
  display: block;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.3rem;
}
.quote-addbar-kind-field .form-select {
  width: auto;
  min-width: 8.5rem;
}
.quote-addbar-toolbar .field {
  margin: 0;
  flex: 1 1 10rem;
  min-width: 7rem;
  max-width: 18rem;
}
.quote-addbar-toolbar .field-qty {
  flex: 0 0 5.5rem;
  max-width: 5.5rem;
}
.quote-addbar-toolbar .field-price {
  flex: 0 0 7.5rem;
  max-width: 8rem;
}
.quote-addbar-toolbar .field-currency,
.quote-addbar-toolbar .field-class {
  flex: 0 0 6.5rem;
  max-width: 7.5rem;
}
.quote-addbar-toolbar .field-grow {
  flex: 2 1 14rem;
  max-width: 22rem;
}
.quote-addbar-toolbar .field label {
  display: block;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.3rem;
}
.quote-addbar-toolbar .form-select,
.quote-addbar-toolbar .form-control {
  width: 100%;
  max-width: 100%;
}
.quote-addbar-toolbar .quote-addbar-submit {
  flex: 0 0 auto;
  align-self: flex-end;
}
.quote-addbar-hint {
  margin: 0.55rem 0 0;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  line-height: 1.4;
}

.catalog-variant-picker {
  display: grid;
  grid-template-columns: minmax(9rem, 13rem) minmax(14rem, 1fr);
  gap: 0.45rem;
}
.catalog-variant-picker .hub-typeahead { min-width: 0; }
@media (max-width: 640px) {
  .catalog-variant-picker { grid-template-columns: 1fr; }
}

/* Wizard help disclosure ("¿Cómo funciona el cotizador?") */
.quote-help {
  margin: 0 0 1rem;
}
.quote-help > summary {
  list-style: none;
  cursor: pointer;
}
.quote-help > summary::-webkit-details-marker {
  display: none;
}
.quote-help-body {
  margin-top: 0.5rem;
  padding: 0.85rem 1rem;
}
.quote-help-heading {
  margin: 0.9rem 0 0.35rem;
  font-size: var(--hub-fs-sm);
}
.quote-help-heading:first-of-type { margin-top: 0.35rem; }
.quote-help-list {
  margin: 0 0 0.5rem;
  padding-left: 1.15rem;
  font-size: var(--hub-fs-sm);
  color: var(--ink-mid, #4a5568);
}
.quote-help-list li { margin-bottom: 0.25rem; }
.quote-help-footnote {
  margin: 0.75rem 0 0;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}

/* Costing step table */
.quote-costing-note {
  margin: 0 0 1rem;
  padding: 0.75rem 0.9rem;
  border-radius: 8px;
  background: var(--surface-soft, #fafbfc);
  border: 1px solid var(--line-soft);
  font-size: var(--hub-fs-sm);
  color: var(--ink-mid, #4a5568);
  line-height: 1.45;
}
.quote-costing-note strong { color: var(--ink); font-weight: 600; }
.quote-group-block { margin-bottom: 1.5rem; }
.quote-group-title {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink);
}
.quote-lines-table thead th {
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  border-bottom-color: var(--line-soft);
  background: transparent;
}
/* Wizard step tables carry a lot of columns — keep the body text compact. */
.quote-wizard-body table.table { font-size: var(--hub-fs-sm); }
.quote-wizard-body table.table thead th { font-size: var(--hub-fs-xs); }
.quote-line-edit { margin: 0; }
.quote-dials {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: 1rem 1.25rem;
  margin-bottom: 0.5rem;
}
.quote-dial label {
  display: block;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.35rem;
}
.quote-dial .field-hint {
  margin-top: 0.35rem;
}
.quote-dial .hub-percent-input {
  max-width: 10rem;
}
/*
 * Pricing step, Origen column: the margin actually in force, beside the chip naming where it comes
 * from. The per-line box is blank unless that line carries its own adjustment, so this is the only
 * place the consultant reads the figure that will price the row.
 */
.quote-margin-effective {
  margin-left: 0.4rem;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}
.quote-shipment {
  margin-bottom: 1.75rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--line-soft);
}
.quote-shipment:last-child { border-bottom: 0; }
.quote-shipment-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: 0.85rem;
}
.quote-shipment-title {
  margin: 0.15rem 0 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}
.quote-shipment-fee { margin-bottom: 0.75rem; }
.field-action .btn { width: 100%; }
.quote-inline-form { margin: 0; display: inline-flex; }
.quote-health {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem 1.75rem;
  align-items: flex-start;
}
.quote-health-compact { margin-bottom: 1rem; }
.quote-health-score {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-width: 5.5rem;
  min-height: 5.5rem;
  padding: 0.75rem;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--white);
}
.quote-health-score-num {
  font-size: 1.65rem;
  font-weight: 700;
  line-height: 1;
  color: var(--ink);
}
.quote-health-score-label {
  margin-top: 0.35rem;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.quote-health-score.is-ok { border-color: var(--ok-line); background: var(--ok-bg); }
.quote-health-score.is-ok .quote-health-score-num { color: var(--ok-fg); }
.quote-health-score.is-risk { border-color: var(--warn-line); background: var(--warn-bg); }
.quote-health-score.is-risk .quote-health-score-num { color: var(--warn-fg); }
.quote-health-score.is-blocker { border-color: var(--bad-line); background: var(--bad-bg); }
.quote-health-score.is-blocker .quote-health-score-num { color: var(--bad-fg); }
.quote-health-score.is-watch { border-color: var(--line); }
.quote-health-issues {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 16rem;
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}
.quote-health-issue {
  display: flex;
  gap: 0.65rem;
  padding: 0.75rem 0.85rem;
  border-radius: 8px;
  border: 1px solid var(--line-soft);
  background: var(--white);
  font-size: var(--hub-fs-sm);
}
.quote-health-issue i { font-size: 1.15rem; margin-top: 0.1rem; color: var(--muted); }
.quote-health-issue.is-blocker i { color: #a32020; }
.quote-health-issue.is-risk i { color: #9a6b00; }
.quote-health-action {
  margin-top: 0.25rem;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}
.quote-cierre-actions { margin: 1rem 0 1.5rem; }
.quote-cierre-meta {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 1.5rem;
}
.quote-doc-checks { display: flex; flex-direction: column; gap: 0.45rem; }
.quote-doc-check {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-size: var(--hub-fs-sm);
  padding: 0.55rem 0.7rem;
  border-radius: 8px;
  border: 1px solid var(--line-soft);
}
.quote-doc-check.is-ok { background: var(--ok-bg); border-color: var(--ok-line); }
.quote-doc-check.is-off { background: var(--bad-bg); border-color: var(--bad-line); }

/*
 * A quote with no linked opportunity, on the Cierre step: a fact worth surfacing but not a
 * blocker (emitting is still allowed) and not a success either, so neither .quote-doc-check
 * modifier fits — both carry a status color, and a valid state reads neutral (frontend-design
 * rules, "status color is never alarm color"). Shares .quote-doc-check's shape, one new tone.
 */
.quote-emit-notice {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin-bottom: 1rem;
  padding: 0.55rem 0.7rem;
  border-radius: 8px;
  border: 1px solid var(--line-soft);
  background: var(--paper);
  color: var(--muted);
  font-size: var(--hub-fs-sm);
}

/*
 * "¿Qué es esta versión?" on the emission screen (fragments/offer-emission.html): the .hub-check
 * radios reused as-is, this class only sets the block off from the "Envío por Hub" button below
 * it, since the fragment's own .quote-section-label already carries the top margin a section
 * heading needs but nothing supplies the bottom one a form control after it needs.
 */
.quote-version-question {
  margin-bottom: 0.75rem;
}

/*
 * "¿Quién envía?" (fragments/offer-emission.html), as one .mail-fact-row like every other fact on
 * that panel. The <form> has to wrap the value and the verb so the radios reach the submit, and
 * display:contents lets it do that without becoming a grid cell of its own and collapsing the
 * row's three columns into one.
 *
 * The two hints used to sit side by side under two competing buttons and read as a single run-on
 * sentence (Matt, 2026-09-09). Now the control is the site's own .hub-seg and exactly one hint is
 * shown, the one belonging to the selected mode. Zero JS: :has() on the checked radio, the same
 * mechanism .hub-flash-stack and .hub-open-row already use here.
 */
.emit-mode-cell {
  display: contents;
}

.emit-mode-hints {
  margin-top: 0.45rem;
}

.emit-hint-manual {
  display: none;
}

.emit-mode-cell:has(.emit-mode input[value="MANUAL"]:checked) .emit-hint-hub {
  display: none;
}

.emit-mode-cell:has(.emit-mode input[value="MANUAL"]:checked) .emit-hint-manual {
  display: block;
}

/* Gestión manual leaves no cadence and no letter, so the question about restarting one and the
   mark on the letter's own place in the day both have nothing to mean. */
.emit-mode-cell:has(.emit-mode input[value="MANUAL"]:checked) .emit-version,
.emit-mode-cell:has(.emit-mode input[value="MANUAL"]:checked) .emit-priority {
  display: none;
}

.emit-version,
.emit-priority {
  margin-top: 0.85rem;
  margin-bottom: 0;
}

/* State A: the notice has nothing to align to, so its verb sits under it rather than in a column. */
.emit-verb-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* Opportunity media: Dropzone owns selection/progress while this layer keeps it inside Hub's calm card system. */
.hub-media-pane { display:grid; gap:1.25rem; }
.hub-media-section-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; margin-bottom:1.25rem; }
.hub-media-section-title { margin:0; color:var(--ink); font-size:var(--hub-fs-xl); }
.hub-media-section-copy { margin:.35rem 0 0; color:var(--muted); font-size:var(--hub-fs-sm); }
.hub-media-count { flex:0 0 auto; padding:.3rem .55rem; border:1px solid var(--line-soft); border-radius:999px; background:var(--surface-soft, var(--paper)); color:var(--muted); font-size:var(--hub-fs-xs); }
.hub-media-upload-layout { display:grid; grid-template-columns:minmax(15rem, .9fr) minmax(18rem, 1.1fr); gap:1rem; align-items:stretch; }
.hub-media-drop { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.35rem; min-height:13rem; padding:1.5rem; border:2px dashed var(--line); border-radius:10px; text-align:center; background:var(--white); cursor:pointer; }
.hub-media-drop:hover, .hub-media-drop.dz-drag-hover { border-color:var(--blue); background:var(--navy-tint); }
.hub-media-drop > i { font-size:2rem; color:var(--blue-deep); }
.hub-media-drop > strong { color:var(--ink); }
.hub-media-drop > span, .hub-media-drop > small { color:var(--muted); font-size:var(--hub-fs-sm); }
.hub-media-drop .btn { margin-top:.35rem; pointer-events:none; }
.hub-media-fallback-input { max-width:18rem; }
/* A column, explicitly. It was display:flex with no direction, so items laid out in a ROW:
   each queued file declares width:100% but a flex item in a row still shrinks, so twelve
   files became twelve slivers and overflow-wrap:anywhere broke every name one character
   per line. Latent while a queue held one or two files, obvious the moment a meeting
   bundle arrived. The cap keeps a long queue from pushing the form off the screen. */
.hub-media-queue { display:flex; flex-direction:column; align-items:stretch; gap:.5rem; min-height:13rem; max-height:22rem; overflow-y:auto; padding:.8rem; border:1px solid var(--line-soft); border-radius:10px; background:var(--surface-soft, var(--paper)); }
.hub-media-queue-empty { align-self:center; margin:auto; max-width:18rem; color:var(--muted); font-size:var(--hub-fs-sm); text-align:center; }
.hub-media-queued-file { display:flex; align-items:flex-start; gap:.7rem; width:100%; margin:0; padding:.75rem; border:1px solid var(--line-soft); border-radius:8px; background:var(--white); }
.hub-media-queued-icon { display:grid; place-items:center; flex:0 0 2.25rem; width:2.25rem; height:2.25rem; border-radius:7px; color:var(--blue-deep); background:var(--navy-tint); }
.hub-media-queued-details { flex:1 1 auto; min-width:0; }
/* The name is one line with an ellipsis (the full name is in the title attribute); the
   description below it may wrap, since that is prose worth reading. */
.hub-media-queued-details strong { display:block; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.hub-media-queued-details > span { display:block; overflow-wrap:anywhere; }
.hub-media-queued-details > span { margin-top:.15rem; color:var(--muted); font-size:var(--hub-fs-xs); }
.hub-media-queued-progress { height:.35rem; margin-top:.7rem; overflow:hidden; border-radius:999px; background:var(--line-soft); }
.hub-media-queued-progress .dz-upload { display:block; width:0; height:100%; background:var(--blue); transition:width .15s ease; }
.hub-media-queued-file .dz-error-message { display:none; margin:.45rem 0 0; color:var(--bad-fg); font-size:var(--hub-fs-xs); }
.hub-media-queued-file.dz-error .dz-error-message { display:block; }
.hub-media-remove { flex:0 0 auto; padding:.15rem; border:0; background:transparent; color:var(--muted); cursor:pointer; }
.hub-media-remove:hover, .hub-media-remove:focus-visible { color:var(--bad-fg); }
.hub-media-status { display:flex; align-items:flex-start; gap:.5rem; padding:.7rem .8rem; border:1px solid var(--line-soft); border-radius:8px; background:var(--surface-soft, var(--paper)); color:var(--muted); font-size:var(--hub-fs-sm); }
.hub-media-status i { flex:0 0 auto; font-size:1.1rem; }
.hub-media-status.is-ready { border-color:var(--ok-line); background:var(--ok-bg); color:var(--ok-fg); }
.hub-media-status.is-uploading { border-color:var(--blue-soft); background:var(--navy-tint); color:var(--navy); }
.hub-media-status.is-success { border-color:var(--ok-line); background:var(--ok-bg); color:var(--ok-fg); }
.hub-media-status.is-error { border-color:var(--bad-line); background:var(--bad-bg); color:var(--bad-fg); }
.hub-media-progress { display:block; width:100%; height:.65rem; accent-color:var(--blue); }
.hub-media-submit-tooltip { position:relative; display:inline-flex; }
.hub-media-submit-tooltip:focus-visible { outline:2px solid var(--blue); outline-offset:3px; border-radius:6px; }
.hub-media-submit-tooltip:has(button:disabled)::after { position:absolute; z-index:4; bottom:calc(100% + .45rem); left:0; width:max-content; max-width:19rem; padding:.45rem .6rem; border-radius:6px; background:var(--navy); color:var(--white); content:attr(data-tooltip); font-size:var(--hub-fs-xs); line-height:1.35; opacity:0; pointer-events:none; transform:translateY(.2rem); transition:opacity .15s ease, transform .15s ease; }
.hub-media-submit-tooltip:has(button:disabled):hover::after, .hub-media-submit-tooltip:has(button:disabled):focus-within::after, .hub-media-submit-tooltip:has(button:disabled):focus-visible::after { opacity:1; transform:translateY(0); }
.hub-media-submit-tooltip .btn:disabled { border-color:var(--line); background:var(--line-soft); color:var(--muted); opacity:1; cursor:not-allowed; }
@media (max-width: 767.98px) { .hub-media-upload-layout { grid-template-columns:1fr; } .hub-media-drop, .hub-media-queue { min-height:10rem; } .hub-media-section-heading { flex-direction:column; } }
.hub-media-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(250px,1fr)); gap:1rem; }
.hub-media-card { display:flex; flex-direction:column; min-width:0; border:1px solid var(--line-soft); border-radius:10px; background:var(--white); overflow:hidden; }
.hub-media-preview { position:relative; display:grid; place-items:center; min-height:150px; background:var(--surface-soft, var(--paper)); text-decoration:none; overflow:hidden; }
.hub-media-preview img { width:100%; height:150px; object-fit:cover; transition:transform .18s ease; }
.hub-media-preview > span { position:absolute; right:.6rem; bottom:.6rem; padding:.25rem .45rem; border-radius:5px; background:color-mix(in srgb, var(--navy) 88%, transparent); color:var(--white); font-size:var(--hub-fs-xs); opacity:0; transform:translateY(.2rem); transition:opacity .15s ease, transform .15s ease; }
.hub-media-preview:hover img { transform:scale(1.02); }
.hub-media-preview:hover > span, .hub-media-preview:focus-visible > span { opacity:1; transform:translateY(0); }
.hub-media-card-body { display:flex; flex:1 1 auto; flex-direction:column; padding:.85rem; }
.hub-media-card p { margin:.35rem 0; color:var(--muted); font-size:var(--hub-fs-xs); overflow-wrap:anywhere; }

/* ── Staging (/staging) ───────────────────────────────────────────────────
   Promoted from the agreed design mock (recepcion-mock.local.html /
   recepcion-conversacion.local.html), translated onto existing tokens, --bad-fg/--warn-fg/--ok-* (status tokens above) rather than the mock's
   raw hexes, and named staging-* for the module's English name. */
.staging-table th, .staging-table td { white-space: nowrap; }
.staging-table td.staging-ask { white-space: normal; }
.staging-ref {
  font-family: var(--font-mono);
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  letter-spacing: .02em;
}

/* T. hábil carries the colour because it is the number anyone is answerable for; T. real stays
   neutral — it explains, it does not accuse. */
.staging-real { font-variant-numeric: tabular-nums; color: var(--body); }
.staging-business { font-variant-numeric: tabular-nums; font-weight: 600; }
.staging-business-late { color: var(--bad-fg); }
.staging-business-soon { color: var(--warn-fg); }
.staging-business-ok { color: var(--body); font-weight: 500; }

.staging-when { color: var(--ink); }
.staging-when small { display: block; font-size: var(--hub-fs-xs); color: var(--muted); font-weight: 400; }
.staging-sub { display: block; font-size: var(--hub-fs-xs); color: var(--muted); font-weight: 400; }
.staging-who-name { font-weight: 600; color: var(--ink); }
.staging-ask { color: var(--body); }
/* A real inbound message runs to several hundred words; the full text lives one click away in the
   detail panel (.staging-said, never clamped), so truncating the list cell here loses nothing while
   keeping every row the same calm height. */
.staging-ask-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 32ch;
}
.staging-channel { color: var(--body); font-size: 1.15rem; }

/*
 * The detail row of a staging item: a slot that is always in the markup and empty until the arrow
 * asks for it (2026-09-07). An empty slot must not leave a bordered blank row behind, which is what
 * :has does here — the row is only a row once it holds something. The border and background it
 * shares with Salidas' own open row live in one shared rule, .hub-open-row below (not .hub-row-open,
 * already the name of the unrelated "whole row is a link" component a few hundred lines up).
 */
.hub-open-row:has(.staging-detail-slot:empty) {
  display: none;
}

/*
 * Cerrar sits over the panel, never in it. As a plain child of the grid below it was a grid item:
 * it took the whole first cell, stretched to its full width and height, and pushed every real
 * section one cell along, so the drawer opened with a button the size of the message beside it and
 * the reading column squeezed into the narrow half. float did nothing about that, a grid item
 * ignores it. Absolute against the panel gives the two columns back to the content they were sized
 * for (Matt, 2026-09-10).
 */
.staging-detail-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  z-index: 1;
}

.staging-detail {
  position: relative;
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 2rem;
  padding: 1.25rem 1rem 1.5rem;
  white-space: normal;
}
.staging-said { font-size: 1.0625rem; line-height: 1.55; color: var(--ink); margin: 0 0 .75rem; }
.staging-said-mark { border-left: 3px solid var(--blue-soft); padding-left: .9rem; }
.staging-meta { font-size: var(--hub-fs-xs); color: var(--muted); line-height: 1.6; margin: 0 0 .75rem; }
.staging-uuid { font-family: var(--font-mono); font-size: var(--hub-fs-xs); color: var(--muted); }

.staging-fields { margin: .5rem 0 0; }
.staging-fields summary {
  font-size: var(--hub-fs-xs);
  color: var(--blue-deep);
  font-weight: 500;
  cursor: pointer;
}
.staging-fields dl { margin: .6rem 0 0; }
.staging-fields dt { font-size: var(--hub-fs-xs); color: var(--muted); font-weight: 500; margin-top: .4rem; }
.staging-fields dd { font-size: var(--hub-fs-sm); color: var(--ink); margin: 0; }

.staging-known dt { font-size: var(--hub-fs-xs); color: var(--muted); font-weight: 500; margin-top: .5rem; }
.staging-known dd { font-size: var(--hub-fs-sm); color: var(--ink); margin: 0; }

.staging-actions { display: flex; gap: .5rem; align-items: center; margin-top: 1.25rem; flex-wrap: wrap; }
.staging-actions .staging-spacer { flex: 1; }

/* Procesar: three rows, one shape (fact-sheet pattern shared with .mail-fact-* — mail.html and the
   Configuración tab), so reused directly rather than reinvented. */
.staging-process { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line-soft); }
.staging-process > .eyebrow { margin-bottom: .5rem; }

/* Vincular's picker sits above the confirmation it produces (HTMX, GET /staging/{id}/link-preview,
   innerHTML into an empty target — reference_htmx_outerswap_rebind). Left border reads as "this
   came from the field above", the same idea as .staging-said-mark. */
.staging-link-preview { margin-top: .6rem; border-left: 2px solid var(--blue-soft); padding: .05rem 0 .05rem .8rem; }
.staging-link-preview .eyebrow { margin-bottom: .2rem; }
.staging-link-preview-name { margin: 0 0 .15rem; font-weight: 600; color: var(--ink); }
.staging-link-preview-sub { margin: 0; color: var(--muted); font-size: var(--hub-fs-sm); }

/* Desvincular is quiet on purpose: one wrong bind is not a mistake worth a loud button, it is a
   correction any consultant should feel safe pressing. */
.staging-unlink-form { display: inline-block; }

/* Descartar carries a motivo. The discard pile is a dataset, so the reason travels with the
   decision instead of being reconstructed later from who happened to remember. */
.staging-discard { display: flex; gap: .4rem; align-items: center; }
.staging-discard-label {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  font-weight: 500;
  white-space: nowrap;
}
.staging-discard-note { width: 14rem; }

/* The decision, read back: state, date, author, and what they wrote. */
.staging-decision { margin-top: 1.25rem; }
.staging-decision-line { font-size: var(--hub-fs-sm); color: var(--ink); margin: 0 0 .5rem; }

/* Métricas in network terms: this firm sells traceability to plant engineers, and RTT / TTL /
   Hops / Idle are the words they already use for exactly these quantities. Only what is actually
   measured is shown — see StagingService / staging/index.html. */
.staging-metrics { margin-top: 1.35rem; padding-top: 1rem; border-top: 1px solid var(--line-soft); }
.staging-metrics table { width: 100%; font-variant-numeric: tabular-nums; }
.staging-metrics th {
  font-weight: 600;
  font-size: var(--hub-fs-sm);
  color: var(--ink);
  padding: .3rem 0;
  width: 4.5rem;
  text-align: left;
}
.staging-metrics td { font-size: var(--hub-fs-sm); color: var(--ink); padding: .3rem 0; }
.staging-metrics td.staging-metric-note { color: var(--muted); font-size: var(--hub-fs-xs); text-align: right; }
.staging-metric-none { color: var(--muted); }
.staging-metric-bad { color: var(--bad-fg); font-weight: 600; }
.hub-media-card-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:.5rem; margin-bottom:.15rem; }
.hub-media-card-heading strong { min-width:0; overflow-wrap:anywhere; }
.hub-media-pdf { display:block; padding:1.5rem 0; color:var(--bad-fg); font-size:5rem; text-align:center; }
.hub-media-rename { margin-top:.6rem; color:var(--muted); font-size:var(--hub-fs-sm); }
.hub-media-rename summary { width:max-content; color:var(--blue-deep); cursor:pointer; }
.hub-media-rename form { margin-top:.55rem; }
.hub-media-rename-controls { display:flex; gap:.45rem; }
.hub-media-rename-controls .form-control { min-width:0; }
.hub-media-card-actions { display:flex; justify-content:space-between; gap:.5rem; margin-top:auto; padding-top:.85rem; }

/* Installed iOS web-apps need their content clear of the sensor/home-indicator areas. Desktop keeps its normal spacing. */
@supports (padding: max(0px)) {
  @media (max-width: 991.98px) {
    .hub-content-topbar { padding-top:max(.45rem, env(safe-area-inset-top)); padding-left:max(1.25rem, env(safe-area-inset-left)); padding-right:max(1.25rem, env(safe-area-inset-right)); }
    .hub-main { padding-bottom:max(1.5rem, env(safe-area-inset-bottom)); padding-left:max(1.25rem, env(safe-area-inset-left)); padding-right:max(1.25rem, env(safe-area-inset-right)); }
  }
}

/*
 * Gestión log (opportunity detail, Gestiones tab): a calm timeline — hairline
 * separators between entries, no per-row card boxes (see frontend-design.md).
 */
.hub-gestion-entry {
  padding: 1rem 0;
  border-bottom: 1px solid var(--line-soft);
}
.hub-gestion-entry:last-child {
  border-bottom: none;
  padding-bottom: 0;
}
.hub-gestion-entry-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.6rem;
  margin-bottom: 0.35rem;
}
.hub-gestion-entry-when {
  font-size: var(--hub-fs-sm);
  color: var(--muted);
}
.hub-gestion-entry-actions {
  margin-left: auto;
}
.hub-gestion-entry-note {
  margin: 0;
  color: var(--ink);
}

/* The .hub-ball / .hub-ball-late pair ("who owes the next move, under a pending commitment") and
   .hub-registro-grid (the Gestión + Compromiso two-column console) lived here. Both describe the
   compromiso model V27 replaced with the espera, and no template has carried either class since.
   Deleted 2026-08-04 with the .hub-entry* family, for the same reason. */

/* ── Proposal detail: generate requirements checklist ──────────────────────
   Reuses the shared ok/warn status tokens (see :root above) rather than a
   new palette: a satisfied requirement reads as --ok, a pending one as
   --warn (never alarm red — nothing is broken, it just is not ready yet). */
.hub-gate-done {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  color: var(--ok-fg);
  font-weight: 600;
}
.hub-gate-done .ph { font-size: 1.1rem; }

.hub-gatelist {
  list-style: none;
  margin: 0 0 0.6rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.hub-gate {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--hub-fs-sm);
}

.hub-gate .ph { font-size: 1rem; }

.hub-gate-ok { color: var(--ok-fg); }
.hub-gate-ok .ph { color: var(--ok-fg); }

.hub-gate-pending { color: var(--ink); }
.hub-gate-pending .ph { color: var(--warn-fg); }

/* A pending requirement is a jump link to the control that fixes it, with a one-line instruction
   underneath. Satisfied requirements stay plain text -- there is nothing to go and do. */
.hub-gate-link {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dashed var(--line);
}
.hub-gate-link:hover,
.hub-gate-link:focus-visible {
  color: var(--blue);
  border-bottom-color: currentColor;
}
.hub-gate-hint {
  font-size: var(--hub-fs-xs);
  color: var(--muted);
  line-height: 1.35;
}

/* Jumped-to sections must clear the sticky header instead of hiding under it. */
[id^="sec-"],
#route,
#coverTitle {
  scroll-margin-top: 5rem;
}

.hub-gate-summary {
  margin: 0;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  background: var(--warn-bg);
  color: var(--warn-fg);
  font-size: var(--hub-fs-sm);
  font-weight: 500;
}

/* Compact read-only panels: tighter padding + kv rhythm than a working-form panel */
.panel-compact {
  padding: 0.7rem 1.05rem;
}

/* ── Propuesta Custom: ISO/IEC/IEEE 29148 section guidance ──────────────────
   Shown on the authoring page in place of an empty optional section (scope
   statement, actors, assumptions, constraints, references). Reuses .eyebrow
   for the clause label and .field-hint for the rationale, so this is layout
   glue only, not a new typography or color statement. Consultant-only copy:
   never rendered into the PDF. */
.hub-ieee-guidance {
  margin-top: 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.hub-ieee-guidance .eyebrow {
  margin-bottom: 0;
}

.hub-ieee-optional {
  font-style: italic;
}

/*
 * Inline add-form row followed by its own result (a table or an empty-state notice), inside the
 * same panel: Alcance, Roles, Glosario, and the ISO/IEC/IEEE 29148 item lists all share this exact
 * shape. A wider column-gap than the canonical .hub-form-inline rhythm gives the submit button room
 * to read as an action, not a fifth field; the bottom margin is the same 0.85rem the app already
 * uses between two stacked panels (see ".panel + .panel"), so the form and what it produces read as
 * two things, not one crowded block.
 */
.hub-add-form-row {
  gap: 1rem 1.5rem;
  margin: 0 0 0.85rem;
}

.hub-kv-tight {
  gap: 0.5rem 1.25rem;
}

/*
 * One row of fields (canonical definition — do not redeclare this selector elsewhere).
 *
 * align-items is flex-start, not flex-end: a field's hint (`.field-hint`) always follows its input,
 * so it cannot move that field's own input, but flex-end used to bottom-align the whole row, and a
 * field with a two-line hint made every plainer sibling in the row taller than its own content,
 * pushing THEIR inputs down to match, out of line with the tall field's input above its hint (a
 * field with a hint next to one without landed at two different input heights). Every field now
 * grows from the row's top, so a hint's length only changes how far down that one field's OWN box
 * reaches, never its neighbors'.
 *
 * A field-less action — `.form-actions` or a bare submit button — has no label above it, so
 * top-aligning it the way a labelled field aligns would float it above the inputs it acts on
 * instead of beside them; the rule below opts it back into the row's bottom edge on purpose.
 */
.hub-form-inline {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  flex-wrap: wrap;
}

.hub-form-inline .field { margin-bottom: 0; flex: 1 1 16rem; }

/*
 * The pipeline's filter row, narrowed. Same component, same markup, one width: a 16rem basis is
 * right for a form of text inputs and wrong for four short selects, which wrapped onto a second
 * line and made the toolbar taller than the first card underneath it. Vertical space is the board's
 * scarcest resource, so this row spends as little of it as the controls allow.
 */
.pipeline-filters {
  gap: 0.55rem 0.75rem;
  padding-block: 0.7rem;
}

.pipeline-filters .field {
  flex: 1 1 8rem;
  max-width: 13rem;
}

.pipeline-filters .field:first-child {
  flex: 2 1 12rem;
  max-width: 22rem;
}

.hub-form-inline .form-actions,
.hub-form-inline > .btn {
  align-self: flex-end;
}

/*
 * One row that stays one row: a short form (a field or two and its verb) inside a box that takes its
 * width from its content, a bulk bar or a fact sheet's verb column. A wrapping flex row in such a box
 * is measured by its widest single child, so the box got the field's width and the button dropped
 * under the field (Staging's bulk bar, Correo › Comportamiento, the company's "Pausar hasta…",
 * 2026-09-13/14). Here the row never wraps; the fields shrink instead, down to a width a date still
 * fits in.
 */
.hub-form-inline.is-one-row { flex-wrap: nowrap; gap: .5rem; align-items: flex-end; }
.hub-form-inline.is-one-row .field { flex: 0 1 12rem; min-width: 7.5rem; }

/* Small muted labels over each input: a fact sheet's own verb column, folded or not (a folded
   form still opens into this same quiet shape). */
.hub-form-inline.is-compact { gap: .5rem; }
.hub-form-inline.is-compact label {
  display: block;
  margin-bottom: .25rem;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}

/* Propuesta Custom detail: acceptance-criteria list inside the per-requirement modal
   (templates/custom/detail.html). One line per criterion, its "Quitar" button on the same row. */
.hub-criteria-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.hub-criteria-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.35rem;
  border-bottom: 1px solid var(--hairline, #e5e7eb);
}

.hub-criteria-list li:last-child {
  border-bottom: none;
}

/* Diagnostic detail "Enlace" card: the copy field + toggle/vigencia controls on the left, the QR
   code on the right, sharing the full card width instead of stacking in a narrow column. */
.diagnostic-link-row {
  display: flex;
  gap: 1.75rem;
  flex-wrap: wrap;
}

.diagnostic-link-main {
  flex: 1 1 22rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.diagnostic-link-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.5rem;
  align-items: flex-end;
}

/* The date picker and both buttons on one baseline. .hub-form-inline lets its field grow to fill
   the row, which pushed "Actualizar vigencia" away from the date it acts on. */
.diagnostic-link-actions { margin: 0; }
.diagnostic-link-actions .field { flex: 0 0 auto; }
.diagnostic-link-actions .form-control:disabled { background: var(--paper); cursor: not-allowed; }

.diagnostic-link-qr {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
}

.diagnostic-link-qr img {
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.5rem;
  background: var(--white);
}

.diagnostic-link-qr-caption {
  margin: 0;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}

.hub-page-title-sm {
  font-size: var(--hub-fs-title);
  margin: 0 0 0.9rem;
}
/* Proposal authoring keeps the common path compact and isolates customer-specific prose by section. */
.proposal-authoring-tabs {
  margin-bottom: 1.25rem;
}

.proposal-authoring-layout {
  display: grid;
  grid-template-columns: minmax(14rem, 18rem) minmax(0, 1fr);
  gap: 1.25rem;
  align-items: start;
}

.proposal-narrative-toc {
  position: sticky;
  top: 1rem;
}

.proposal-section-nav {
  display: grid;
  gap: 0.25rem;
  margin-top: 1rem;
}

.proposal-section-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius-btn);
  color: var(--muted);
  text-decoration: none;
  line-height: 1.3;
}

.proposal-section-link:hover {
  background: var(--paper);
  color: var(--ink);
}

.proposal-section-link.is-active {
  background: var(--navy-tint);
  color: var(--navy);
  font-weight: 650;
}

.proposal-section-count {
  min-width: 1.55rem;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  background: var(--white);
  color: var(--navy);
  font-size: var(--hub-fs-xs);
  text-align: center;
}

.proposal-narrative-editor-header,
.proposal-narrative-field-heading,
.proposal-token-toolbar {
  display: flex;
  align-items: center;
  gap: 0.65rem;
}

.proposal-narrative-editor-header {
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 1rem;
}

.proposal-token-toolbar {
  flex-wrap: wrap;
  padding: 0.75rem;
  border-radius: var(--radius-btn);
  background: var(--paper);
  margin-bottom: 1.25rem;
}

.proposal-token-toolbar-label {
  margin-right: 0.25rem;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
  font-weight: 600;
}

.proposal-narrative-field {
  margin-block: 1.35rem;
}

.proposal-narrative-field-heading {
  min-height: 2rem;
  margin-bottom: 0.45rem;
}

.proposal-narrative-field-heading label {
  margin: 0 auto 0 0;
}

.proposal-narrative-actions {
  position: sticky;
  bottom: 0;
  z-index: 2;
  padding-block: 0.8rem;
  background: var(--white);
}

@media (max-width: 991.98px) {
  .proposal-authoring-layout {
    grid-template-columns: 1fr;
  }

  .proposal-narrative-toc {
    position: static;
  }

  .proposal-section-nav {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 575.98px) {
  .proposal-section-nav {
    grid-template-columns: 1fr;
  }

  .proposal-narrative-editor-header,
  .proposal-narrative-field-heading {
    align-items: flex-start;
    flex-direction: column;
  }

  .proposal-narrative-field-heading label {
    margin-right: 0;
  }
}

/* =====================================================================
   Diagnostic results: the customer's own photographs
   ===================================================================== */

/*
 * A contact sheet, not a gallery. The consultant is reading answers and these are one of them, so
 * the thumbnails sit inline at a size you can judge a doorway from and open full size in a tab.
 */
.diagnostic-result-photos {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin-top: 0.5rem;
}

.diagnostic-result-photos figure {
  margin: 0;
  width: 11rem;
}

.diagnostic-result-photos img {
  display: block;
  width: 100%;
  height: 8rem;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--paper);
}

.diagnostic-result-photos a:hover img { border-color: var(--blue); }

.diagnostic-result-photos figcaption {
  margin-top: 0.35rem;
  font-size: var(--hub-fs-sm);
  line-height: 1.4;
  color: var(--ink);
}

.diagnostic-result-photo-name {
  display: block;
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* Requirements the resolver derived, and the inferences it could not settle from the answers
   alone ("he said he has metal but X, so he needs Z") — the one thing on this page that is not
   plain evidence, so it earns one calm, distinct callout rather than blending into a paragraph. */
.diagnostic-hint-callout {
  margin: 0.9rem 0 0;
  padding: 0.85rem 1rem;
  border-radius: 10px;
  background: var(--warn-bg);
  color: var(--warn-fg);
}

.diagnostic-hint-callout-title {
  margin: 0 0 0.4rem;
  font-weight: 600;
  font-family: var(--font-sans);
}

.diagnostic-hint-callout ul {
  margin: 0;
  padding-left: 1.1rem;
}

.diagnostic-hint-callout li + li {
  margin-top: 0.3rem;
}

/* The operating baseline. Numbers carry the hierarchy here, so there is no new colour and no box:
   the working is muted and the result is not, which is the whole visual idea. */
.hub-unit {
  font-size: var(--hub-fs-sm);
  font-weight: 400;
  color: var(--muted);
}

/* An unanswered figure is an absence, not a fault. Muted and lowercase, never alarm colour: red is
   reserved for destructive actions, and a customer who skipped a question has done nothing wrong. */
.hub-value-missing {
  font-size: var(--hub-fs-sm);
  font-weight: 400;
  font-style: italic;
  color: var(--muted);
}

/* The working, shown so it can be checked. Quieter than the result it produces. */
.diagnostic-baseline-arithmetic {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  font-weight: 400;
}

.diagnostic-baseline-result {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* =====================================================================
   Diagnostic results (dfv-25): full-width, answer-first (Síntesis leads,
   the evidence follows). No right column, no peso valuation — see
   templates/diagnostic/results.html and V88's migration header.
   ===================================================================== */

/* Síntesis: the one panel with a visible accent, so the reader's eye lands here first. */
.diagnostic-synthesis {
  border-left: 3px solid var(--blue-deep);
}

.diagnostic-synthesis-line {
  margin: 0 0 0.9rem;
  font-size: 1.05rem;
  line-height: 1.55;
  color: var(--ink);
  max-width: 66ch;
}

.diagnostic-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin: 0 0 1rem;
}

.diagnostic-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  border-radius: 999px;
  padding: 0.28rem 0.85rem;
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  background: var(--navy-tint);
  color: var(--blue-deep);
}

.diagnostic-chip-why {
  font-weight: 400;
  color: var(--teal, var(--blue-deep));
  font-size: 0.78rem;
}

/* The support-fit gap and any other flag worth a consultant's attention: warm, not alarm. */
.diagnostic-chip.is-flag {
  background: var(--warn-bg);
  color: var(--warn-fg);
}

.diagnostic-chip.is-flag .diagnostic-chip-why {
  color: var(--warn-fg);
  opacity: 0.85;
}

/* An unresolved fact (nivel Mixto/estiba, mecanismo de integración por definir): dashed, quiet. */
.diagnostic-chip.is-open {
  background: var(--line-soft);
  color: var(--body);
  border: 1px dashed var(--line);
}

.diagnostic-drivers {
  border-top: 1px solid var(--line-soft);
  padding-top: 0.7rem;
}

.diagnostic-drivers-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
  font-weight: 600;
  margin: 0 0 0.4rem;
}

.diagnostic-drivers p {
  margin: 0;
  font-size: var(--hub-fs-sm);
  color: var(--body);
  max-width: 68ch;
}

.diagnostic-drivers em {
  color: var(--ink);
  font-style: italic;
}

/* A sub-heading inside "El proyecto en cifras" (El universo / Las etiquetas / La lectura /
   Restricciones), the same small-caps rhythm the rest of Hub already uses for grouped lists. */
.diagnostic-group-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
  font-weight: 600;
  margin: 0.9rem 0 0.15rem;
}

.diagnostic-group-label:first-of-type {
  margin-top: 0;
}

/* An unresolved figure ("se define en la revisión"): the same warm, non-alarm treatment as the
   integration-mechanism gap, never plain muted text — a gap the consultant must act on reads
   differently from an answer nobody gave. */
.diagnostic-pending {
  color: var(--warn-fg);
  background: var(--warn-bg);
  border-radius: 6px;
  padding: 0.05rem 0.5rem;
  font-size: var(--hub-fs-sm);
}

/* En sus palabras: the customer's own text, quoted rather than tabulated. */
.diagnostic-quote-block {
  border-left: 3px solid var(--line);
  padding: 0.1rem 0 0.1rem 0.85rem;
  margin: 0 0 0.85rem;
}

.diagnostic-quote-block:last-child {
  margin-bottom: 0;
}

.diagnostic-quote-source {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  font-weight: 600;
  margin: 0 0 0.2rem;
}

.diagnostic-quote-text {
  margin: 0;
  font-size: var(--hub-fs-sm, 0.9375rem);
  color: var(--ink);
  font-style: italic;
  max-width: 64ch;
}

.diagnostic-pain-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.3rem;
}

.diagnostic-pain-chips span {
  background: var(--line-soft);
  color: var(--body);
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  font-size: var(--hub-fs-sm);
}

/* Respuestas completas: the exact questions and the exact answers, sitting open right under
   the metrics. It was a <details> drawer at the foot of the page until 2026-09-07; the five
   rules that dressed its summary went with it, since every panel below is derived from this
   and the source material should not be the one thing a reader has to open. */
.diagnostic-full-answers-section {
  margin-top: 0.9rem;
}

/* =====================================================================
   Sistema: solicitudes de capacidad del agente
   ===================================================================== */

/* Goal/missing/context are free prose an agent wrote, not a short label — line breaks the agent
   typed should survive, the same reasoning behind .quote-review-notes, so a multi-sentence report
   reads as the paragraphs it was written as rather than one run-on line. */
.agent-capability-text {
  white-space: pre-line;
  min-width: 12rem;
}

/*
 * Contratos › Días: how long until this one has to be renewed, signed. Past due reads as calm
 * amber, never red — red is for destructive actions, and a contract running out is neither
 * destructive nor anybody's fault. The Estado chip already carries the word "Vencida"; this only
 * says by how much.
 */
.contract-days-overdue { color: var(--warn-fg); font-weight: 600; }

/* ── Outbound (Salidas): Staging's sibling — what leaves rather than what arrives ────────── */
/*
 * The two repair panels ("Sin contacto principal", "Rebotó"): one opportunity per line, its lane
 * chip, and the verb that clears it. The class was on the markup with nothing behind it until
 * 2026-09-07, so the rows ran together as a bare bullet list.
 */
.links-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.links-list > li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.35rem 0;
}
.links-list > li + li { border-top: 1px solid var(--line); }
/* The remedy, kept quiet: the opportunity's own name is the louder link on the row. */
.outbound-needs-verb {
  margin-left: auto;
  font-size: var(--hub-fs-sm);
  color: var(--muted);
  white-space: nowrap;
}
.outbound-needs-verb:hover { color: var(--navy); }

.outbound-who-name { font-weight: 600; color: var(--ink); }
.outbound-who-sub { display: block; font-size: var(--hub-fs-xs); color: var(--muted); font-weight: 400; }
.outbound-tabular { font-variant-numeric: tabular-nums; color: var(--body); }
/* Overdue reads as calm amber, never red: red stays for destructive actions only. */
.outbound-last { color: var(--muted); font-size: var(--hub-fs-sm); white-space: nowrap; }
.outbound-ask { color: var(--body); }

/*
 * Shared bulk bar (js/hub/hub-bulk-select.js): pick rows, then act on all of them at once. Used by
 * Salidas and by Sistema › Agente › Solicitudes; it lived under an .outbound- name until the second
 * screen needed it. Hidden until something is checked, so a table nobody has touched stays quiet.
 */
.hub-bulk-bar {
  display: none;
  align-items: center;
  gap: .5rem;
  padding: .65rem 1rem;
  margin: 0 0 1rem;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: 10px;
  flex-wrap: wrap;
}
.hub-bulk-bar.is-visible { display: flex; }
.hub-bulk-count { font-weight: 600; color: var(--ink); margin-right: auto; }
/* The bar's one optional field (Solicitudes' review note, Staging's motivo) gets more room than a
   folded form's field. The row itself is .hub-form-inline.is-one-row; the bar still wraps, so on a
   narrow screen the whole form moves to its own line with its verbs beside the field. */
.hub-bulk-bar .is-one-row .field { flex-basis: 18rem; }
.hub-bulk-check-col { width: 2.25rem; }
/*
 * Siempre visible en Salidas (Matt, 2026-09-15: "put all functions in an organized drop down
 * list"), a diferencia de .hub-bulk-bar.is-visible arriba: Recrear borradores actúa sobre todo Hoy
 * y no necesita selección, así que la barra no puede depender de tener algo marcado para aparecer.
 * Los verbos que sí la necesitan se deshabilitan en vez de ocultarse (data-hub-bulk-needs-selection).
 * Antes .outbound-toolbar, su propio componente; unificado el 2026-09-16 en .hub-bulk-bar, el mismo
 * que usan Empresas, Staging y Solicitudes, con este único modificador.
 */
.hub-bulk-bar.is-always-visible { display: flex; }
.hub-bulk-bar .dropdown-menu { min-width: 16rem; }
.hub-bulk-bar .dropdown-header {
  font-size: var(--hub-fs-xs);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.hub-bulk-bar .dropdown-item:disabled { color: var(--muted); }
.hub-bulk-bar .dropdown-item.is-danger { color: var(--bs-danger, #b42318); }
/*
 * Hoy: one table, one thead (Matt, 2026-09-16, it used to be one table per lane). The lane heading
 * that used to sit above its own table is now a row inside the shared tbody, same eyebrow type as
 * .quote-section-label but inline, so it never claims a hover it is not (see the neutralizing rule
 * next to .hub-open-row below, which this row shares).
 */
.outbound-lane-row td {
  padding-top: 1.1rem;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  border: none;
}
.outbound-lane-row:first-child td { padding-top: .35rem; }
.table-hover > tbody > tr.outbound-lane-row:hover > * { background: transparent; box-shadow: none; }
/*
 * A folded form's toggle reads as a disclosure, not a button (Matt, 2026-09-14: "the drawer is
 * redundant [on a one-line form]... make it one liner for all, clean"). The forms that DO stay
 * folded, because a checkbox list or a typeahead cannot fit one line, still get a toggle that looks
 * like what it is, never a lookalike `.btn`). Text plus a caret that turns on open; used for Copias
 * and the note in the company's Correo tab, the two contact pickers in Emitir's "A quién" panel, and
 * the two verbs that fold in Salidas' action bar (No enviar, Pausar la cuenta).
 */
.hub-disclosure { margin-top: .75rem; }
.hub-disclosure > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  color: var(--muted);
  font-size: var(--hub-fs-sm);
}
.hub-disclosure > summary::-webkit-details-marker { display: none; }
.hub-disclosure > summary .ph-caret-down { transition: transform .15s ease; }
.hub-disclosure[open] > summary { color: var(--navy); }
.hub-disclosure[open] > summary .ph-caret-down { transform: rotate(180deg); }
.hub-disclosure > form { margin-top: .5rem; }

/*
 * The row that opened the detail below, and the detail row itself: one shared marker for Salidas
 * and Staging, both of which used to drive their own copy (unified 2026-09-16). Staging carried a
 * 2px blue top border, a second signal next to the calmer 1px line Salidas already used; the
 * neutral one won and the duplicate is gone. Named .hub-open-row, not .hub-row-open: that name
 * already belongs to the unrelated "the whole row is a link" component a few hundred lines up.
 */
.hub-open-row,
.hub-open-row-summary { background: var(--white); }
/* The open row and its drawer are one object, so the drawer never lights up on hover: a hover
   highlight says "clickable", and the drawer is a surface you read, not a thing you press. Both
   resets fire together: --bs-table-bg-state is the layer Bootstrap's own .table-hover rule sets on
   the hovered cells, and background/box-shadow catch what falls through it. */
.hub-open-row { border-top: 1px solid var(--line); }
.table-hover > tbody > tr.hub-open-row:hover > *,
.table-hover > tbody > tr.hub-open-row-summary:hover > * {
  --bs-table-bg-state: initial;
  --bs-table-color-state: initial;
  background: var(--white);
  box-shadow: none;
}
tr.hub-open-row > td { box-shadow: none !important; }

/* Inset past the checkbox column so the open row and its drawer read as one piece. Two columns is
   the default, for the reply/question screens: history reads first, narrower, then the reply and
   its question, wider (HUB.md §31). */
.outbound-detail {
  display: grid;
  grid-template-columns: 1fr 1.4fr;
  gap: 2rem;
  padding: 1.5rem 1.5rem 1.5rem 3.25rem;
}
/* The letter itself (PREPARAR/ESPERAR/ENVIADA, no question open) reads top to bottom instead: one
   calmer column, capped so a line of prose stays readable, drawer rebuild 2026-09-04. */
.outbound-detail-letter { grid-template-columns: 1fr; max-width: 46rem; }
.outbound-letter { display: flex; flex-direction: column; gap: 1rem; }
.outbound-subject { margin: 0; font-weight: 600; color: var(--ink); }
.outbound-message-body {
  white-space: pre-wrap;
  font-size: 1rem;
  line-height: 1.55;
  color: var(--ink);
  background: var(--line-soft);
  border-radius: 8px;
  padding: .9rem 1rem;
  margin: 0;
}

/* The action bar: the decision verb first, No enviar beside it, a spacer, then Ver en Gmail and
   Pausar la cuenta as quiet grey verbs on the right (HUB.md §31). */
.outbound-action-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.outbound-action-bar form { margin: 0; }
.outbound-bar-spacer { flex: 1 1 auto; }
.outbound-action-bar .hub-disclosure { margin-top: 0; }

.outbound-decision-options { display: flex; flex-direction: column; gap: .6rem; align-items: flex-start; }

/* The question component (fragments/outbound.html :: question / answered), one question at a time
   for a reply and for the third letter alike. */
.outbound-reply-box {
  font-family: var(--font-mono, monospace);
  font-size: var(--hub-fs-sm);
  line-height: 1.55;
  color: var(--ink);
  background: var(--line-soft);
  border-radius: 8px;
  padding: .75rem .9rem;
  margin: 0 0 .9rem;
  max-height: 9rem;
  overflow-y: auto;
}
/* white-space lives on the paragraph itself, not the box: the box stays plain (normal) so the
   template's own indentation between its tags is never rendered as visible blank lines, and only
   the reply's own line breaks, carried by th:text with nothing around it, show through. */
.outbound-reply-box p {
  margin: 0;
  white-space: pre-line;
}
.outbound-answer-trail {
  margin: 0 0 .9rem;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  font-size: var(--hub-fs-xs);
  color: var(--muted);
}
.outbound-answer-trail li:not(:last-child)::after { content: "›"; margin-left: .35rem; }
/* The same serif .hub-login-title's headline uses; no new token, only a size that fits a question
   rather than a page title. */
.outbound-question {
  font-family: var(--font-serif);
  font-size: 1.3rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0 0 .85rem;
}
.outbound-decision-options form { display: flex; flex-direction: column; align-items: flex-start; gap: .4rem; }
.outbound-answer-bullets {
  margin: 0 0 1rem;
  padding-left: 1.1rem;
  font-size: var(--hub-fs-sm);
  color: var(--body);
}
.outbound-answer-bullets li { padding: .15rem 0; }

.outbound-empresas-table td { vertical-align: middle; }

/*
 * The one colour on a mail settings fact sheet (HUB.md §31): a 7px dot beside "Sí" / "No" /
 * "Pausada". Red is reserved for destructive actions, so "off" reads muted rather than alarmed, * a company not receiving automatic mail is a deliberate choice, not a fault.
 */
.mail-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-right: 0.4rem;
  vertical-align: middle;
}
.mail-dot.is-on { background: var(--ok-fg); }
.mail-dot.is-off { background: var(--muted); }

/*
 * The firm switch (admin/mail.html): four pills and the consequence of the one that is selected.
 * Same shape as .emit-mode-cell above, and for the same reasons: display:contents lets the <form>
 * wrap the value and the verb without collapsing the row's three columns, and :has() on the
 * checked radio shows exactly one hint with zero JS. Four hints stacked would be four lines of
 * copy for one decision.
 */
.mail-level-cell {
  display: contents;
}

.mail-level-hints {
  margin-top: 0.45rem;
}

.mail-level-hints .form-hint {
  display: none;
}

.mail-level-cell:has(.mail-level input[value="OFF"]:checked) .mail-hint-off,
.mail-level-cell:has(.mail-level input[value="QUEUE"]:checked) .mail-hint-queue,
.mail-level-cell:has(.mail-level input[value="DRAFT"]:checked) .mail-hint-draft,
.mail-level-cell:has(.mail-level input[value="SEND"]:checked) .mail-hint-send {
  display: block;
}

/*
 * Sistema › Correo › Plantillas: the letter itself, in the editor. A monospace face and a wide
 * measure because what is being read is where the line breaks fall and where a {{campo}} sits, not
 * the prose rhythm; the paragraph breaks in a letter are load-bearing.
 */
.mail-template-body {
  font-family: var(--font-mono);
  font-size: var(--hub-fs-sm);
  line-height: 1.5;
}

/*
 * Plantillas as a list and one open letter (Matt, 2026-09-15): every variant used to render as a full
 * editor stacked one after another. The list groups by lane and letter; a variant is a plain link
 * (?carta=), so the open letter survives a reload and a Guardar comes back to it. Stacks under the
 * editor at phone width.
 */
.mail-templates {
  display: grid;
  grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
}

.mail-template-nav {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.mail-template-group-title {
  margin: 0 0 0.25rem;
  font-size: var(--hub-fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.mail-template-group ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.mail-template-link {
  display: block;
  padding: 0.3rem 0.6rem;
  border-left: 2px solid transparent;
  color: var(--ink);
  font-size: var(--hub-fs-sm);
  text-decoration: none;
}

.mail-template-link:hover {
  background: var(--paper);
}

.mail-template-link:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}

/* The same accent the tab bar underlines its open tab with, turned on its side. */
.mail-template-link[aria-current="true"] {
  border-left-color: var(--blue);
  background: var(--line-soft);
  color: var(--blue-deep);
  font-weight: 600;
}

@media (max-width: 767.98px) {
  .mail-templates {
    grid-template-columns: minmax(0, 1fr);
  }
}

.mail-lane-once {
  padding-top: 0.25rem;
}

/* ── The contact form's salutation preview (fragments/salutation.html) ───────────────────────
 * Four lines a consultant reads before a customer does. A definition list, because each line is
 * an answer to a named case, and the lines themselves are what the eye should land on: the case
 * label is small and muted, the rendered greeting is the body text.
 */
.hub-salutation-title {
  margin: 0 0 0.25rem;
  font-size: var(--hub-fs-sm);
  font-weight: 500;
  color: var(--ink);
}

.hub-salutation-preview {
  background: var(--canvas);
  border-radius: 8px;
  padding: 0.6rem 0.75rem;
}

.hub-salutation-list {
  margin: 0;
}

.hub-salutation-list dt {
  font-size: var(--hub-fs-xs);
  font-weight: 400;
  color: var(--muted);
}

.hub-salutation-list dd {
  margin: 0 0 0.5rem;
  font-size: var(--hub-fs);
  color: var(--ink);
}

.hub-salutation-list dd:last-of-type {
  margin-bottom: 0.35rem;
}
