/* Consentimiento Lead Verificado — estilos base. ui-designer es el dueño y los amplía.
   Dos contextos: .cv-panel (SaaS sobrio) y .cv-portal (consumidor, móvil-first). */

* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  font-family: var(--cv-font);
  color: var(--cv-text);
  background: var(--cv-bg);
  font-size: var(--fs-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 { font-family: var(--cv-font-head); letter-spacing: -.01em; line-height: var(--lh-tight); }
/* ESCALA EXPLÍCITA. Antes sólo se fijaba la familia: `h3` se quedaba con el 1.17em del
   user-agent (18.7px) y se pintaba MÁS GRANDE que el `h2` de su propia tarjeta (18px).
   Cuatro escalones reales, distinguidos por tamaño Y peso (Manrope 600/700/800). */
.cv-panel h1 { font-size: var(--fs-2xl); font-weight: var(--fw-black); }
.cv-panel h2 { font-size: var(--fs-xl);  font-weight: var(--fw-bold); }
.cv-panel h3 { font-size: var(--fs-base); font-weight: var(--fw-bold); }
.cv-panel h4 {
  font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  text-transform: uppercase; letter-spacing: .06em; color: var(--cv-text-muted);
}
a.cv-link, .cv-link { color: var(--cv-primary); font-weight: 500; }
a.cv-link:hover, .cv-link:hover { text-decoration: underline; }

/* Icono SVG inline alineado con el texto (helper cv_icon) */
.cv-ico { display: inline-block; vertical-align: middle; flex-shrink: 0; }

/* Foco siempre visible (accesibilidad AA / teclado) */
:focus-visible { outline: 3px solid var(--cv-focus); outline-offset: 2px; }
/* Dentro de una tabla con scroll el desplazamiento del anillo lo recorta contra el borde del
   contenedor: quien navega con teclado deja de ver dónde está justo en la última columna,
   que es donde viven las acciones. Dentro del marco, el anillo se pega al elemento. */
.cv-table-wrap :focus-visible { outline-offset: -1px; }

/* ================================================================
   PANEL (autenticado) — tema slate "LegalSafe"
   Sidebar oscura (slate), marca arriba, navegación con iconos, pie de usuario.
   ================================================================ */
.cv-shell { display: flex; min-height: 100vh; }

/* ----- Sidebar como <details> (toggle CSS-puro, sin JS). -----
   Desktop: columna fija siempre abierta. Móvil: barra superior (summary) + nav desplegable. */
.cv-sidebar-d {
  width: 248px;
  flex-shrink: 0;
}
/* El contenedor visual oscuro vive en .cv-sidebar-inner (el <nav> interno). */
.cv-sidebar-inner {
  background: var(--cv-sidebar-bg);
  color: var(--cv-sidebar-text);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  padding: var(--sp-5) var(--sp-4);
  min-height: 100vh;
}

/* Barra superior (summary): SOLO visible en móvil; oculta en desktop. */
.cv-topbar { display: none; }
.cv-topbar::-webkit-details-marker { display: none; }   /* sin triángulo nativo */
.cv-topbar::marker { content: ""; }
.cv-topbar-brand {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  color: var(--cv-sidebar-brand);
}
.cv-topbar-brand .cv-ico { color: var(--cv-sidebar-brand); }
.cv-burger {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--cv-radius-sm);
  color: var(--cv-sidebar-brand);
}
.cv-burger-close { display: none; }

/* Desktop (>900px): el <details> del sidebar debe mostrar SIEMPRE la nav, esté
   "abierto" o no (no hay hamburguesa en escritorio). Chrome 120+ oculta el contenido
   de un <details> cerrado vía ::details-content { content-visibility: hidden }, que el
   display:flex de .cv-sidebar-inner NO vence; lo forzamos visible. En navegadores sin
   ::details-content, el .cv-sidebar-inner ya queda visible por su display base. */
@media (min-width: 901px) {
  /* Chrome 120+ envuelve el contenido del <details> en ::details-content. Sin una altura
     definida en ese pseudo, el height:100% de .cv-sidebar-inner colapsa a "auto" y el fondo
     oscuro no llega hasta abajo. Le damos height:100% para completar la cadena de alturas
     .cv-sidebar-d (100vh) -> ::details-content -> .cv-sidebar-inner. En navegadores sin este
     pseudo (Firefox), la regla es inocua y la cadena ya funciona por herencia directa. */
  .cv-sidebar-d::details-content { content-visibility: visible; height: 100%; }
  .cv-sidebar-d > .cv-sidebar-inner { display: flex; }

  /* Sidebar fijada al hacer scroll: sticky (no fixed) para no sacarla del flex ni
     necesitar margin-left en .cv-main. align-self:flex-start evita que el flex la
     estire (sticky requiere que la caja no ocupe ya toda la altura). */
  .cv-sidebar-d {
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100vh;
    /* Salvaguarda: el fondo oscuro también en la columna (que sí tiene 100vh definidos), por
       si el height del pseudo ::details-content fallara en algún navegador. Evita el hueco. */
    background: var(--cv-sidebar-bg);
  }
  /* El fondo oscuro (.cv-sidebar-inner) cubre toda la ventana; su min-height:100vh base
     se anula para permitir scroll interno del PROPIO menú si no cabe (viewports cortos:
     el pie "Salir" no se recorta). */
  .cv-sidebar-d > .cv-sidebar-inner {
    min-height: 0;
    height: 100%;
    overflow-y: auto;
  }
}

/* Marca: escudo + "LegalSafe" + subtítulo */
.cv-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-2);
  color: var(--cv-sidebar-brand);
  text-decoration: none;
}
.cv-brand .cv-ico { color: var(--cv-sidebar-brand); }
.cv-brand-name {
  font-family: var(--cv-font-head);
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: -.01em;
  line-height: 1.1;
}
.cv-brand-sub {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--cv-sidebar-muted);
  margin-top: 2px;
}

.cv-nav { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--sp-1); }
.cv-nav a {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-3);
  border-radius: var(--cv-radius-sm);
  color: var(--cv-sidebar-text);
  text-decoration: none;
  font-size: var(--fs-sm);
  font-weight: 600;
  min-height: 44px;
}
.cv-nav a .cv-ico { color: currentColor; opacity: .9; }
.cv-nav a:hover { background: var(--cv-sidebar-hover); color: #fff; }
.cv-nav a.is-active,
.cv-nav a[aria-current="page"] {
  background: var(--cv-primary);
  color: var(--cv-primary-text);
}
.cv-nav a.is-active .cv-ico,
.cv-nav a[aria-current="page"] .cv-ico { opacity: 1; }

/* Pie de la sidebar (usuario + salir), empujado al fondo */
.cv-sidebar-foot {
  margin-top: auto;
  padding-top: var(--sp-4);
  border-top: 1px solid var(--cv-sidebar-divider);
  display: grid;
  gap: var(--sp-3);
}
.cv-user { display: flex; align-items: center; gap: var(--sp-3); padding: 0 var(--sp-2); }
.cv-user-avatar {
  width: 36px; height: 36px; border-radius: var(--cv-radius-pill);
  background: var(--cv-primary); color: var(--cv-primary-text);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: var(--fs-sm); flex-shrink: 0;
}
.cv-user-meta { display: flex; flex-direction: column; min-width: 0; }
.cv-user-name { font-size: var(--fs-sm); font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cv-user-role { font-size: var(--fs-xs); color: var(--cv-sidebar-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.cv-main { flex: 1; padding: var(--sp-6); max-width: 1180px; min-width: 0; }
.cv-main h1 { margin: 0 0 var(--sp-2); }
/* RITMO ÚNICO DE PÁGINA. Antes cada componente decidía su propio margen (.cv-alert sp-3,
   .cv-card sp-4, .cv-stat-grid sp-4, .cv-form-section sp-5, .cv-section sp-6) y encima se
   parcheaba con .cv-mt-*: seis cadencias distintas que se sumaban y colapsaban a capricho.
   Ahora la separación entre bloques la fija el CONTENEDOR, una sola vez. */
.cv-main > * { margin-top: 0; margin-bottom: 0; }
.cv-main > * + * { margin-top: var(--stack-lg); }

/* ---------- Cabecera de página ----------
   Estaba en TODAS las pantallas y era un h1 con un párrafo gris debajo, sin nada que la
   anclara: la página empezaba flotando. Ahora es un componente con tres piezas —tesela de
   icono (la misma familia SVG del menú lateral, que es la superficie que sí gusta), título
   y subtítulo— cerrado por una línea de un píxel que separa la cabecera del contenido.
   La línea es lo que hace que el resto de la página se lea como "dentro de" algo. */
.cv-page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-4) var(--sp-5);
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid var(--cv-border-soft);
  margin-bottom: var(--sp-6);
}
/* El bloque de título no se estira: el subtítulo respeta la medida de lectura aunque el
   lienzo tenga 1180px. Un párrafo de 1180px de ancho no se lee, se recorre. */
.cv-page-head > div:first-child { min-width: 0; max-width: var(--measure); }
/* El h1 de página sube al 3.er escalón (34px): entre él y el h2 de sección (22px) ya hay
   distancia suficiente para no tener que subrayar nada. */
.cv-page-head h1 { margin: 0 0 var(--sp-2); font-size: var(--fs-3xl); letter-spacing: -.02em; }
.cv-page-head .cv-muted { margin: 0; }
.cv-page-actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }

/* Título con tesela de icono. El icono NO es decoración: identifica la sección con el mismo
   glifo del menú, de modo que "dónde estoy" se resuelve sin leer. */
.cv-page-title { display: flex; align-items: flex-start; gap: var(--sp-4); min-width: 0; }
.cv-page-title > div { min-width: 0; }
.cv-page-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  margin-top: 3px;
  border-radius: var(--cv-radius);
  border: 1px solid var(--cv-border-soft);
  background: var(--cv-primary-soft);
  color: var(--cv-primary);
}
/* Meta de cabecera: dato de contexto (versión activa, cuenta, recuento) junto a las acciones. */
.cv-page-meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--cv-text-muted);
}

/* Botones del panel (densos, no 44px): heredan .cv-btn pero compactos y a --fs-sm,
   uniformes con el resto del panel. El portal (.cv-portal .cv-btn) mantiene --fs-base. */
.cv-panel .cv-btn { min-height: 40px; padding: var(--sp-2) var(--sp-5); font-size: var(--fs-sm); }
/* Pero la densidad de escritorio no puede heredarse al táctil: por debajo del corte de
   tablet los botones del panel recuperan el mínimo de 44px y el cuerpo de 16px. */
@media (max-width: 900px) {
  .cv-panel .cv-btn { min-height: 44px; font-size: var(--fs-base); }
}
/* Acciones de cabecera: en móvil se apilan a ancho completo en vez de quedar apretadas. */
@media (max-width: 640px) {
  .cv-page-head { align-items: stretch; padding-bottom: var(--sp-4); margin-bottom: var(--sp-5); }
  .cv-page-head h1 { font-size: var(--fs-2xl); }
  /* La tesela encoge, no desaparece: sigue orientando y deja de robar ancho al título. */
  .cv-page-icon { width: 36px; height: 36px; margin-top: 2px; }
  .cv-page-title { gap: var(--sp-3); }
  .cv-page-actions { width: 100%; }
  .cv-page-actions > .cv-btn { flex: 1 1 100%; }
}

/* Utilidades de espaciado vertical (tokens, no valores a ojo) */
.cv-mt-4 { margin-top: var(--sp-4); }
.cv-mt-5 { margin-top: var(--sp-5); }

/* ----- Menú hamburguesa en móvil (≤900px): toggle con <details>, sin JS ----- */
@media (max-width: 900px) {
  .cv-shell { flex-direction: column; }
  .cv-sidebar-d { width: 100%; }

  /* Barra superior fija: marca + hamburguesa. El <summary> es el botón accesible. */
  .cv-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    min-height: 56px;
    background: var(--cv-sidebar-bg);
    color: var(--cv-sidebar-brand);
    cursor: pointer;
    list-style: none;          /* Firefox: oculta el marcador del summary */
    position: sticky;
    top: 0;
    z-index: 30;
  }
  .cv-topbar .cv-brand-name { font-family: var(--cv-font-head); font-size: var(--fs-lg); font-weight: 800; }
  .cv-topbar:focus-visible { outline: 3px solid #fff; outline-offset: -3px; }

  /* Nav OCULTA por defecto en móvil (details cerrado): solo se ve la barra superior.
     (Se fuerza display:none porque la base define display:flex, que vencería al UA shadow.) */
  .cv-sidebar-inner {
    display: none;
    min-height: 0;
    gap: var(--sp-4);
    border-top: 1px solid var(--cv-sidebar-divider);
  }
  .cv-sidebar-d[open] .cv-sidebar-inner { display: flex; }
  /* details cerrado => el navegador ya oculta el contenido tras el summary.
     Al abrir, alterna los iconos hamburguesa/X. */
  .cv-sidebar-d[open] .cv-burger-open  { display: none; }
  .cv-sidebar-d[open] .cv-burger-close { display: inline-flex; }

  /* La marca grande interna (con subtítulo) es redundante con la barra: ocúltala en móvil. */
  .cv-sidebar-inner > .cv-brand { display: none; }

  /* Items y pie del menú móvil: a ancho completo, targets grandes. */
  .cv-sidebar-foot { margin-top: var(--sp-2); }
}

/* ---------- Alerta / aviso reutilizable (panel + portal) ---------- */
.cv-alert {
  /* Sin margen propio: lo separa el ritmo de su contenedor (.cv-main, .cv-card,
     .cv-form o .cv-portal-card). Antes cada componente traía el suyo y se acumulaban. */
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--cv-border);
  border-left: 4px solid var(--cv-text-muted);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface);
  color: var(--cv-text);
  font-size: var(--fs-sm);
}
.cv-alert--error {
  border-left-color: var(--cv-rechazado);
  color: var(--cv-rechazado);
}
.cv-alert--success {
  border-left-color: var(--cv-aceptado);
  color: var(--cv-aceptado);
}
/* Informativo (acción disponible, no aviso): acento azul sobrio; texto legible en --cv-text. */
.cv-alert--info {
  border-left-color: var(--cv-primary);
  background: var(--cv-enviado-tint);
  color: var(--cv-text);
}
.cv-alert--info strong { color: var(--cv-primary); }

/* Mensaje de error multilínea (el informe del linter de campaña llega con saltos reales).
   Sustituye a un `style="white-space:pre-wrap"` inline que la CSP `style-src 'self'` ignoraba,
   así que el informe se leía como un párrafo continuo. */
.cv-alert--pre { white-space: pre-wrap; }

/* <details class="cv-card"> cuyo <summary> es la cabecera plegable de la tarjeta.
   El h2 iba con `style="display:inline"` (ignorado por la CSP) para no romper línea junto al
   triángulo: se resuelve con un summary flex, que además le da área de clic y foco visibles. */
.cv-card-toggle {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  list-style: none;
  min-height: 44px;
  border-radius: var(--cv-radius-sm);
}
.cv-card-toggle::-webkit-details-marker { display: none; }
.cv-card-toggle::marker { content: ""; }
.cv-card-toggle > h2 { margin: 0; font-size: var(--fs-lg); }
/* Chevron propio, para no depender del triángulo del user-agent (que varía por navegador). */
.cv-card-toggle::after {
  content: "";
  margin-left: auto;
  width: 9px; height: 9px;
  border-right: 2px solid var(--cv-text-muted);
  border-bottom: 2px solid var(--cv-text-muted);
  transform: rotate(45deg);
  transition: transform .15s ease;
}
details[open] > .cv-card-toggle::after { transform: rotate(225deg); }
.cv-card-toggle:hover > h2 { color: var(--cv-primary); }
/* Lista de información legible (p.ej. efectos de la baja, art. 7.3). Reutilizable. */
.cv-info-list {
  margin: 0;
  padding-left: var(--sp-5);
  display: grid;
  gap: var(--sp-3);
  color: var(--cv-text);
}
.cv-info-list li { line-height: 1.5; }

/* Formulario en línea dentro de una celda (acción activar/desactivar): no rompe la fila */
.cv-inline-form { display: inline; margin: 0; }

/* ---------- Badge (rol, metadato, activo/inactivo) ----------
   Comparte caja con el chip para que dos indicadores contiguos no se lean como dos sistemas
   distintos; se diferencia en que NO va en mayúsculas: el badge nombra una cosa (un rol, una
   versión), el chip califica un estado. Los de estado llevan además su punto de color. */
.cv-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 3px var(--sp-3);
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius-pill);
  background: var(--cv-surface-alt);
  color: var(--cv-text);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  line-height: 1.5;
  white-space: nowrap;
}
/* Indicador activo/inactivo: color + FORMA (punto) + texto, nunca solo color. */
.cv-badge--on,
.cv-badge--off,
.cv-badge--activa,
.cv-badge--sustituida,
.cv-badge--archivada,
.cv-badge--no_valida { border-color: transparent; }
.cv-badge--on::before,
.cv-badge--off::before,
.cv-badge--activa::before,
.cv-badge--sustituida::before,
.cv-badge--archivada::before,
.cv-badge--no_valida::before {
  content: "";
  width: 7px;
  height: 7px;
  flex-shrink: 0;
  border-radius: var(--cv-radius-pill);
  background: currentColor;
}
.cv-badge--on  { color: var(--cv-aceptado-ink);  background: var(--cv-aceptado-tint); }
.cv-badge--off { color: var(--cv-pendiente-ink); background: var(--cv-pendiente-tint); }
/* Estados de texto legal (color + forma + texto), con el mismo par tinte/tinta del chip. */
.cv-badge--activa     { color: var(--cv-aceptado-ink);  background: var(--cv-aceptado-tint); }
.cv-badge--sustituida { color: var(--cv-caducado-ink);  background: var(--cv-caducado-tint); }
.cv-badge--archivada  { color: var(--cv-pendiente-ink); background: var(--cv-pendiente-tint); }
.cv-badge--no_valida  { color: var(--cv-enviado-ink);   background: var(--cv-enviado-tint); }

/* ---------- Botón pequeño (acciones densas en celda de tabla) ---------- */
.cv-btn--sm {
  padding: var(--sp-1) var(--sp-3);
  min-height: 32px;
  font-size: var(--fs-sm);
}

/* ---------- Celda de acciones múltiples: separación uniforme entre botones/forms ---------- */
.cv-actions-cell {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}
.cv-actions-cell .cv-inline-form { display: inline-flex; }

/* ---------- Rejilla de formulario largo: 2 columnas en desktop, 1 en móvil ---------- */
.cv-form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
}
.cv-form-grid .cv-field-full { grid-column: 1 / -1; }
@media (max-width: 900px) {
  .cv-form-grid { grid-template-columns: 1fr; }
}
.cv-form-section { margin-top: var(--sp-5); }
.cv-form-section > h3 {
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--cv-text-muted);
  margin: 0 0 var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--cv-border);
}
/* Variante en tarjeta: cada sección del formulario es una tarjeta sobria (maqueta nueva solicitud) */
.cv-form-card {
  margin-top: var(--sp-5);
  background: var(--cv-surface);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  box-shadow: var(--cv-shadow);
  padding: var(--sp-5);
}
.cv-form-card > h3 {
  font-family: var(--cv-font-head);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--cv-text);
  margin: 0 0 var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--cv-border-soft);
}

/* ---------- Lista de definición (vista de solo lectura) ---------- */
.cv-deflist { margin: 0; display: grid; gap: var(--sp-4); }
.cv-deflist > div {
  display: grid;
  gap: var(--sp-1);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--cv-border);
}
.cv-deflist > div:last-child { border-bottom: 0; padding-bottom: 0; }
.cv-deflist dt {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--cv-text-muted);
  font-weight: 600;
}
.cv-deflist dd { margin: 0; color: var(--cv-text); }

/* ---------- Tarjeta ancha (formulario largo a 2 columnas) ---------- */
.cv-card--wide { max-width: 880px; }

/* ---------- Tarjeta sobria del panel (confirmación de alta, etc.) ---------- */
.cv-card {
  max-width: 640px;
  background: var(--cv-surface);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  box-shadow: var(--cv-shadow);
  padding: var(--sp-5);
}
/* Ritmo INTERNO de la tarjeta: un solo gap, como en .cv-portal-card. */
.cv-card > * { margin-top: 0; margin-bottom: 0; }
.cv-card > * + * { margin-top: var(--stack); }
/* El h2 de tarjeta es el 3.er escalón (18px), por debajo del h2 de sección (22px). */
.cv-card > h2 { font-size: var(--fs-lg); }
.cv-card > h2 + p { margin-top: var(--sp-2); }

/* TARJETA DE DATOS. `.cv-card` hacía dos trabajos incompatibles: contenedor de lectura
   (640px es la medida correcta para prosa y formularios) y contenedor de tabla. Había 8
   sitios donde una tarjeta envolvía un .cv-table-wrap: borde dentro de borde, sombra sobre
   sombra, y una tabla de 10 columnas estrangulada a 640px con scroll sobre un lienzo de
   1180px vacío a la derecha. Si la tarjeta contiene una tabla, se ensancha y cede el marco. */
.cv-card:has(> .cv-table-wrap),
.cv-card--full { max-width: none; }
.cv-card > .cv-table-wrap {
  border: 0;
  box-shadow: none;
  border-radius: 0;
  /* La tabla sangra hasta el borde de la tarjeta: el marco ya lo pone la tarjeta. */
  margin-left: calc(var(--sp-5) * -1);
  margin-right: calc(var(--sp-5) * -1);
  width: auto;
}
.cv-card > .cv-table-wrap:first-child { margin-top: calc(var(--sp-5) * -1); }
.cv-card > .cv-table-wrap:last-child  { margin-bottom: calc(var(--sp-5) * -1); }

/* ---------- Fieldset de canales (panel) ---------- */
.cv-fieldset {
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  padding: var(--sp-3) var(--sp-4);
}
.cv-fieldset legend {
  font-weight: 600;
  padding: 0 var(--sp-2);
  color: var(--cv-text);
}

/* ================================================================
   KPI DEL PANEL — el dashboard deja de ser "cifras sueltas"
   La rejilla existía y pintaba ocho cajas idénticas con el número a la derecha del rótulo,
   en línea: ocho datos del mismo peso, sin ninguno que mande. Un panel de control se lee de
   arriba abajo —rótulo, cifra— y necesita UN dato principal por encima del reparto.
   De ahí las dos piezas: `.cv-kpi` (el total, en el slate de la marca) y `.cv-stat` (el
   reparto por estado, en columna y con la cifra al tamaño de un dato, no de una etiqueta).
   ================================================================ */

/* ---------- Cifra principal de la página ----------
   En el slate del menú lateral a propósito: es la única superficie del panel que el cliente
   dio por buena, y repetirla aquí ata el dashboard con la navegación en vez de dejarlo
   flotando sobre el fondo gris. */
.cv-kpi {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3) var(--sp-6);
  padding: var(--sp-5) var(--sp-6);
  border-radius: var(--cv-radius);
  background: var(--cv-sidebar-bg);
  color: var(--cv-sidebar-brand);
  box-shadow: var(--cv-shadow);
}
.cv-kpi-figure { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.cv-kpi-label {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--cv-sidebar-muted);
}
.cv-kpi-value {
  font-family: var(--cv-font-head);
  font-size: var(--fs-4xl);
  font-weight: var(--fw-black);
  line-height: 1;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.cv-kpi-hint {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--cv-sidebar-text);
  max-width: var(--measure-short);
}
@media (max-width: 640px) {
  .cv-kpi { padding: var(--sp-4) var(--sp-5); }
  .cv-kpi-value { font-size: var(--fs-3xl); }
}

/* ---------- Reparto por estado ---------- */
.cv-stat-grid {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
}
@media (max-width: 1200px) { .cv-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .cv-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); } }
.cv-stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 104px;
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  background: var(--cv-surface);
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius);
  box-shadow: var(--cv-shadow);
  transition: box-shadow 140ms ease, border-color 140ms ease;
}
.cv-stat:hover { box-shadow: var(--cv-shadow-md); border-color: var(--cv-border); }
.cv-stat-label { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
/* Sin modificador el punto es neutro (un total no tiene estado), pero SIGUE ocupando su sitio:
   así todos los rótulos de la rejilla arrancan en la misma vertical. */
.cv-stat-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--cv-radius-pill);
  flex-shrink: 0;
  background: var(--cv-outline);
}
.cv-stat-name {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}
/* La cifra ES el contenido de la tarjeta: al tamaño de un dato, no al de una etiqueta. */
.cv-stat-count {
  font-family: var(--cv-font-head);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-black);
  line-height: 1;
  color: var(--cv-text);
  font-variant-numeric: tabular-nums;
}
/* Un cero no es un dato: es la ausencia de dato. Que no pese como los que sí lo son. */
.cv-stat-count--cero { color: var(--cv-outline); font-weight: var(--fw-bold); }
/* Punto de color por estado (reusa los tokens semánticos) */
.cv-stat-dot--pendiente { background: var(--cv-pendiente); }
.cv-stat-dot--enviado   { background: var(--cv-enviado); }
.cv-stat-dot--abierto   { background: var(--cv-abierto); }
.cv-stat-dot--aceptado  { background: var(--cv-aceptado); }
.cv-stat-dot--rechazado { background: var(--cv-rechazado); }
.cv-stat-dot--caducado  { background: var(--cv-caducado); }
.cv-stat-dot--revocado  { background: var(--cv-revocado); }
.cv-stat-dot--exportado { background: var(--cv-exportado); }

/* ---------- Sección con cabecera (tarjeta de tabla con título) ---------- */
.cv-section { margin-top: 0; }
.cv-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-4);
}
.cv-section-head h2 { font-size: var(--fs-lg); margin: 0; }
/* Enlace "Ver todas": el escalón menor de la cabecera, con flecha implícita por el chevron. */
.cv-section-head .cv-link { font-size: var(--fs-sm); white-space: nowrap; }

/* ---------- Barra de búsqueda / filtros (presentación; sin lógica aún) ---------- */
.cv-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}
.cv-search { position: relative; flex: 1; min-width: 220px; max-width: 420px; }
.cv-search .cv-ico {
  position: absolute; left: var(--sp-3); top: 50%; transform: translateY(-50%);
  color: var(--cv-outline); pointer-events: none;
}
.cv-search input {
  width: 100%;
  height: 40px;
  padding: 0 var(--sp-3) 0 var(--sp-7);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface);
  font: inherit;
  font-size: var(--fs-sm);
}
.cv-search input:focus { border-color: var(--cv-primary); }
.cv-toolbar select {
  height: 40px;
  padding: 0 var(--sp-4) 0 var(--sp-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface);
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--cv-text);
  cursor: pointer;
}

/* ================================================================
   TABLA DE DATOS (panel) — la pantalla donde se pasa el día
   Las reglas ya existían y por eso no se tocaron en la primera pasada: funcionaban, pero
   se veían planas. Lo que cambia aquí no es "más adorno", es CALIBRADO:
     · el divisor de fila baja a --cv-border-soft (el borde fuerte enmarca, no separa);
     · la cabecera se apoya en su propia superficie (--cv-th-bg) y cierra con borde fuerte;
     · el hover tiene tono propio y transición corta: la fila responde al puntero;
     · las cifras van a la derecha con cifras tabulares (columnas que se pueden comparar);
     · los identificadores se tratan como código (mono, tonal), no como prosa.
   ================================================================ */
.cv-table-wrap {
  width: 100%;
  overflow-x: auto;
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
  box-shadow: var(--cv-shadow);
}
.cv-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.cv-table th,
.cv-table td {
  padding: var(--sp-3) var(--sp-4);
  text-align: left;
  vertical-align: middle;
}
.cv-table thead th {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--cv-text-muted);
  background: var(--cv-th-bg);
  border-bottom: 1px solid var(--cv-border);
  white-space: nowrap;
}
/* Divisor TENUE entre filas: con el borde fuerte, veinte filas eran veinte rayas y la tabla
   pesaba más que sus datos. El marco lo pone el contenedor; dentro basta con una insinuación. */
.cv-table tbody td { border-bottom: 1px solid var(--cv-border-soft); }
.cv-table tbody tr:last-child td { border-bottom: 0; }
.cv-table tbody tr { transition: background-color 120ms ease; }
.cv-table tbody tr:hover { background: var(--cv-row-hover); }
.cv-table .cv-num { text-align: right; font-variant-numeric: tabular-nums; }
.cv-table .cv-nowrap { white-space: nowrap; }
/* Cifras tabulares también fuera de tabla (KPI, recuentos, resúmenes). */
.cv-num { font-variant-numeric: tabular-nums; }
/* COLUMNA-ESPINA: la primera celda es el identificador de la fila (código, nombre). Darle
   peso es lo que convierte una cuadrícula de texto plano en una tabla que se lee de un vistazo. */
.cv-table tbody td:first-child { font-weight: var(--fw-medium); color: var(--cv-text); }
/* Cabecera pegajosa: en listados largos (solicitudes, formularios) se pierde el contexto
   de columna al hacer scroll. El fondo opaco es imprescindible o el contenido se transparenta. */
.cv-table thead th { position: sticky; top: 0; z-index: 1; }
/* Hashes, tokens y user-agents no pueden ensanchar la tabla hasta romper la página. Llevan
   fondo tonal para leerse como dato-máquina, y siguen partiendo por cualquier carácter. */
.cv-table td code {
  font-family: var(--cv-font-mono);
  font-size: var(--fs-xs);
  overflow-wrap: anywhere;
  color: var(--cv-text-muted);
  background: var(--cv-surface-alt);
  border-radius: var(--cv-radius-sm);
  padding: 1px 5px;
}
.cv-table td { overflow-wrap: break-word; }
/* Celda de texto libre (finalidad, campaña): sin tope, una finalidad larga descuadra la fila. */
.cv-table td:not(.cv-nowrap):not(.cv-num) { max-width: 34ch; }
/* La última columna suele ser la acción ("Expediente", "Abrir"): a la derecha, sin envolver. */
.cv-table td:last-child > .cv-link { white-space: nowrap; }

/* ---------- Identificador (`CV-…`, `LINK-…`, `COL-001`) ----------
   Un código no es una palabra: se dicta por teléfono, se compara carácter a carácter y se
   pega en un buscador. Mono + cifras tabulares + fondo tonal lo separan del nombre propio
   que tiene al lado, que es exactamente el error de lectura que se cometía antes. */
.cv-code {
  display: inline-block;
  font-family: var(--cv-font-mono);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
  color: var(--cv-text);
  background: var(--cv-surface-alt);
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius-sm);
  padding: 2px 6px;
  white-space: nowrap;
}
a.cv-code { color: var(--cv-primary); text-decoration: none; }
a.cv-code:hover { background: var(--cv-primary-soft); border-color: var(--cv-border); }
/* Dentro de la columna-espina el código YA es el peso de la fila: no lo dobla. */
.cv-table tbody td:first-child > .cv-code { font-weight: var(--fw-medium); }

/* ---------- Celda de dos alturas (dato principal + secundario) ----------
   `colaboradores` metía razón social + nombre comercial + código en una celda separados por
   <br>: tres líneas del mismo tamaño y color, es decir, tres datos que parecen uno. */
.cv-cell-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.cv-cell-main { font-weight: var(--fw-semibold); color: var(--cv-text); }
.cv-cell-sub {
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: var(--cv-text-muted);
}

/* ================================================================
   CHIP DE ESTADO — la información más consultada de la aplicación
   Antes: pastilla maciza de color saturado con texto blanco. Una tabla con veinte chips era
   un semáforo, y el color competía con el dato en vez de calificarlo. Ahora cada estado es un
   par TINTE (fondo) + TINTA (texto del mismo tono, AA >= 4.9:1) y un punto de color dibujado
   con `currentColor`: color + FORMA + texto, los tres a la vez, como exige el sistema.
   El resultado pesa menos en tinta y más en jerarquía: se ve el estado sin dejar de leer la fila.
   ================================================================ */
.cv-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 3px 10px 3px 9px;
  border-radius: var(--cv-radius-pill);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: .05em;
  line-height: 1.5;
  text-transform: uppercase;
  white-space: nowrap;
  /* Neutro por defecto: un chip sin modificador (o con un estado nuevo aún sin token) se
     lee igual de bien. Antes salía texto blanco sobre nada, es decir, invisible. */
  background: var(--cv-pendiente-tint);
  color: var(--cv-pendiente-ink);
}
.cv-chip::before {
  content: "";
  width: 7px;
  height: 7px;
  flex-shrink: 0;
  border-radius: var(--cv-radius-pill);
  background: currentColor;
}
.cv-chip--pendiente { background: var(--cv-pendiente-tint); color: var(--cv-pendiente-ink); }
.cv-chip--enviado   { background: var(--cv-enviado-tint);   color: var(--cv-enviado-ink); }
.cv-chip--abierto   { background: var(--cv-abierto-tint);   color: var(--cv-abierto-ink); }
.cv-chip--aceptado  { background: var(--cv-aceptado-tint);  color: var(--cv-aceptado-ink); }
.cv-chip--rechazado { background: var(--cv-rechazado-tint); color: var(--cv-rechazado-ink); }
.cv-chip--caducado  { background: var(--cv-caducado-tint);  color: var(--cv-caducado-ink); }
.cv-chip--revocado  { background: var(--cv-revocado-tint);  color: var(--cv-revocado-ink); }
.cv-chip--exportado { background: var(--cv-exportado-tint); color: var(--cv-exportado-ink); }

/* Estados del FORMULARIO (migración 016). Reutilizan los tokens de estado existentes en vez de
   introducir colores nuevos: `cumplimentado` es el final feliz, igual que `aceptado`; `anulado`
   es una retirada, como `revocado`. Un estado nuevo con color propio solo añadiría ruido a una
   paleta que ya distingue lo que hay que distinguir. */
.cv-chip--cumplimentado { background: var(--cv-aceptado-tint); color: var(--cv-aceptado-ink); }
.cv-chip--anulado       { background: var(--cv-revocado-tint); color: var(--cv-revocado-ink); }

/* Estados de una versión de condiciones. Reutilizan los tokens existentes: `activa` es lo
   vigente (como `aceptado`), `borrador` está a la espera (como `pendiente`), y `sustituida` es
   histórico (como `caducado`). Ningún color nuevo para tres estados que ya tienen equivalente. */
.cv-chip--borrador   { background: var(--cv-pendiente-tint); color: var(--cv-pendiente-ink); }
.cv-chip--activa     { background: var(--cv-aceptado-tint);  color: var(--cv-aceptado-ink); }
.cv-chip--sustituida { background: var(--cv-caducado-tint);  color: var(--cv-caducado-ink); }
/* Estados de la campaña (borrador / publicada / pausada / archivada): mismo criterio, cero
   colores nuevos. `publicada` es lo vigente, `pausada` está detenida, `archivada` es histórico. */
.cv-chip--publicada  { background: var(--cv-aceptado-tint);  color: var(--cv-aceptado-ink); }
.cv-chip--pausada    { background: var(--cv-caducado-tint);  color: var(--cv-caducado-ink); }
.cv-chip--archivada  { background: var(--cv-pendiente-tint); color: var(--cv-pendiente-ink); }
/* Estado del enlace del colaborador: activo / usado / caducado / anulado. */
.cv-chip--activo     { background: var(--cv-aceptado-tint);  color: var(--cv-aceptado-ink); }
.cv-chip--usado      { background: var(--cv-exportado-tint); color: var(--cv-exportado-ink); }
/* Estado del LEAD (registrado / entregado / revocado). `registrado` es la autorización viva;
   `entregado` es que ya salió hacia el CRM, que es un tránsito, no un final feliz distinto. */
.cv-chip--registrado { background: var(--cv-aceptado-tint);  color: var(--cv-aceptado-ink); }
.cv-chip--entregado  { background: var(--cv-exportado-tint); color: var(--cv-exportado-ink); }

/* ---------- Recuento por estado sobre el listado ----------
   No es decoración: es el resumen que se lee ANTES que la tabla. Se presenta como una tira
   de pastillas con la cifra destacada en tinta plena, separada del rótulo por un divisor. */
.ls-recuento {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.ls-recuento .cv-chip { padding: 4px 10px 4px 9px; }
.ls-recuento .cv-chip strong {
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  margin-left: var(--sp-1);
  padding-left: var(--sp-2);
  border-left: 1px solid currentColor;
}

/* ================================================================
   TABLA EN MÓVIL — de rejilla ilegible a lista de fichas
   Hasta ahora la única respuesta al móvil era el scroll horizontal. Una tabla de diez
   columnas dentro de una ventana de 360px con scroll lateral no es una tabla: es un
   laberinto en el que hay que memorizar la cabecera para saber qué se está mirando.
   Con `.cv-table--cards` cada FILA pasa a ser una ficha y cada CELDA una pareja
   rótulo/valor; el rótulo sale de `data-label`, que es presentación pura y no toca ni un
   dato. Por encima de 640px la tabla vuelve a ser una tabla, sin duplicar markup ni JS.
   ================================================================ */
@media (max-width: 640px) {
  /* El marco lo pone cada ficha: el contenedor deja de enmarcar y de scrollear. */
  .cv-table-wrap:has(> .cv-table--cards) {
    border: 0;
    box-shadow: none;
    background: transparent;
    overflow: visible;
    border-radius: 0;
  }
  /* La tabla-tarjeta ya no sangra hasta el borde de la tarjeta: son fichas sueltas, y una
     ficha pegada al borde parecería recortada. Se anulan las cuatro sangrías negativas. */
  .cv-card > .cv-table-wrap:has(> .cv-table--cards),
  .cv-card > .cv-table-wrap:has(> .cv-table--cards):first-child,
  .cv-card > .cv-table-wrap:has(> .cv-table--cards):last-child { margin: 0; }
  .cv-table--cards,
  .cv-table--cards tbody,
  .cv-table--cards tr,
  .cv-table--cards td { display: block; width: auto; }
  /* La cabecera se oculta a la vista pero NO al lector de pantalla: sigue siendo la que
     asocia cada celda con su columna. */
  .cv-table--cards thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .cv-table--cards tr {
    margin-bottom: var(--sp-3);
    padding: var(--sp-2) var(--sp-4);
    background: var(--cv-surface);
    border: 1px solid var(--cv-border);
    border-radius: var(--cv-radius);
    box-shadow: var(--cv-shadow);
  }
  .cv-table--cards tr:last-child { margin-bottom: 0; }
  .cv-table--cards tbody tr:hover { background: var(--cv-surface); }
  .cv-table--cards td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-4);
    padding: var(--sp-2) 0;
    text-align: right;
    border-bottom: 1px solid var(--cv-border-soft);
    white-space: normal;
  }
  /* El tope de 34ch de la celda de texto libre es para una TABLA; dentro de una ficha de
     320px sobra. Se repite el selector completo porque el original tiene más peso que
     `.cv-table--cards td` y, si no, gana él. */
  .cv-table--cards td:not(.cv-nowrap):not(.cv-num) { max-width: none; }
  .cv-table--cards td:last-child { border-bottom: 0; }
  .cv-table--cards td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    text-align: left;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--cv-text-muted);
  }
  /* Celda sin rótulo (columna de acción): la acción ocupa la ficha entera, centrada.
     La primera se excluye: es el título de la ficha y va donde empieza a leerse. */
  .cv-table--cards td:not([data-label]):not(:first-child) { justify-content: center; text-align: center; }
  /* La celda-espina encabeza la ficha: sin rótulo repetido y con el peso de un título. */
  .cv-table--cards tbody td:first-child {
    justify-content: flex-start;
    text-align: left;
    font-size: var(--fs-base);
    font-weight: var(--fw-semibold);
    padding-top: var(--sp-3);
  }
  /* El dato secundario se alinea con su valor (a la derecha), salvo en la celda-espina. */
  .cv-table--cards .cv-cell-stack { align-items: flex-end; }
  .cv-table--cards tbody td:first-child .cv-cell-stack { align-items: flex-start; }
}

/* ---------- Veredicto de "¿Puedo llamar?" ----------
   Es la pantalla que alguien abre con el teléfono ya en la mano, y la respuesta era un
   párrafo de aviso del mismo tamaño que cualquier otro. Sí y No tienen que distinguirse a
   un metro de distancia: icono + color + palabra, los tres. La caja es idéntica en los dos
   casos —no se premia una respuesta sobre la otra—, cambian el tinte y el glifo. */
.cv-verdict {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-5);
  border-radius: var(--cv-radius);
  border: 1px solid transparent;
}
.cv-verdict-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  border-radius: var(--cv-radius-pill);
  background: var(--cv-surface);
  color: inherit;
}
.cv-verdict-text {
  font-family: var(--cv-font-head);
  font-size: var(--fs-xl);
  font-weight: var(--fw-black);
  line-height: var(--lh-tight);
  letter-spacing: -.01em;
}
.cv-verdict--si { background: var(--cv-aceptado-tint);  color: var(--cv-aceptado-ink);  border-color: var(--cv-aceptado-ink); }
.cv-verdict--no { background: var(--cv-rechazado-tint); color: var(--cv-rechazado-ink); border-color: var(--cv-rechazado-ink); }

/* ---------- Desplegable de acción secundaria dentro de una celda ----------
   `colaboradores` tenía un <details><summary> crudo dentro de una celda: el triángulo del
   navegador, texto gris y, al abrirlo, un formulario derramándose sobre la fila. */
.cv-disclosure { display: block; }
.cv-disclosure > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 32px;
  padding: var(--sp-1) 0;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--cv-primary);
  cursor: pointer;
  list-style: none;
}
.cv-disclosure > summary::-webkit-details-marker { display: none; }
.cv-disclosure > summary::marker { content: ""; }
/* Chevron propio (borde CSS): no se depende del triángulo del user-agent, que varía. */
.cv-disclosure > summary::after {
  content: "";
  width: 7px;
  height: 7px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transform-origin: center;
  margin-bottom: 3px;
  transition: transform 140ms ease;
}
.cv-disclosure[open] > summary::after { transform: rotate(225deg); margin-bottom: -2px; }
.cv-disclosure > summary:hover { text-decoration: underline; }
.cv-disclosure > form,
.cv-disclosure > div {
  margin-top: var(--sp-2);
  padding: var(--sp-3);
  background: var(--cv-surface-alt);
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius-sm);
}
@media (prefers-reduced-motion: reduce) {
  .cv-disclosure > summary::after,
  .cv-table tbody tr,
  .cv-stat { transition: none; }
}

/* ================================================================
   FORMULARIO DEL PANEL — `.cv-form`
   Esta clase se usaba en 11 sitios (los formularios más largos de la aplicación:
   configuracion 30 campos, campanas 24, textos 16, colaboradores 11) y NO TENÍA NI UNA
   REGLA. Como `.cv-field` tampoco lleva margen exterior, los campos se pintaban tocándose
   unos con otros y el botón pegado al último input. Era la razón principal de que el panel
   "pareciera hecho sin conocimiento de diseño": en un formulario, el aire ES el diseño.
   (Rastro de la deriva: existía `.cv-form--filtros .cv-field { margin: 0 }`, un reset de un
   margen que nunca existió.)
   ================================================================ */
.cv-form {
  display: grid;
  gap: var(--sp-5);
  align-content: start;
  max-width: var(--measure);
}
/* El botón de envío no es un campo más: se separa y se alinea a la izquierda. */
.cv-form > .cv-btn,
.cv-form > button[type="submit"],
.cv-form > .cv-actions {
  justify-self: start;
  margin-top: var(--sp-2);
}
/* Un <fieldset> dentro del formulario es un grupo, no una caja suelta del user-agent. */
.cv-form fieldset,
.cv-form .cv-field:is(fieldset) {
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius);
  background: var(--cv-bg);
  padding: var(--sp-4);
  margin: 0;
  display: grid;
  gap: var(--sp-3);
}
.cv-form fieldset > legend {
  font-family: var(--cv-font-head);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--cv-text);
  padding: 0 var(--sp-2);
  margin-left: calc(var(--sp-2) * -1);
}
/* Nota que cuelga de una casilla suelta: se pega a ella, no flota a media distancia. */
.cv-form > .cv-check + small.cv-muted { margin-top: calc(var(--sp-5) * -1 + var(--sp-2)); }
/* `--inline` y `--filtros` redefinen display/gap más abajo; aquí sólo se les quita el tope
   de medida, que en una barra de filtros no tiene sentido. */
.cv-form--inline, .cv-form--filtros { max-width: none; }

/* ---------- Pie de ayuda genérico (`.cv-help`) ----------
   Usada en el portal público vigente (ls_portal.php, j.php) SIN una sola regla: el texto de
   apoyo se pintaba con el mismo peso que el cuerpo legal, sin distinguirse de él. */
.cv-help {
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--cv-text-muted);
  max-width: var(--measure);
}
.cv-help code { overflow-wrap: anywhere; font-size: var(--fs-xs); }

/* ---------- Alias `.cv-alert--ok` ----------
   colaboradores.php, terminos.php y puedo_llamar.php marcan sus avisos de ÉXITO con
   `cv-alert--ok`, pero la clase definida se llamaba `--success`: tres confirmaciones se
   pintaban en gris neutro. Se resuelve con un alias, que es más barato que editar 3 ficheros
   y deja las dos grafías válidas. */
.cv-alert--ok {
  border-left-color: var(--cv-aceptado);
  color: var(--cv-aceptado);
}

/* ---------- Variantes de tarjeta que el markup pedía y no existían ----------
   `.cv-card--destacada` (mis_enlaces: el enlace recién generado) y `.cv-card--aviso`
   (campanas: la campaña no supera el linter y no puede publicarse). */
.cv-card--destacada {
  border-color: var(--cv-primary);
  border-left-width: 4px;
  background: var(--cv-primary-soft);
}
.cv-card--aviso {
  border-color: var(--cv-caducado);
  border-left-width: 4px;
  background: var(--cv-caducado-tint);
}
.cv-card--aviso > h2, .cv-card--aviso > h3 { color: var(--cv-caducado); }

/* ---------- Token de enlace de un solo uso (`.ls-token`) ----------
   "Cópialo ahora: es la única vez que se muestra". Se pintaba como un <code> crudo, sin
   ninguna señal de que es el dato importante de la pantalla. Caja monoespaciada, seleccionable
   de un golpe, y que rompe línea en móvil en vez de desbordar. */
.ls-token {
  display: block;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface-alt);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--cv-text);
  letter-spacing: .02em;
  overflow-wrap: anywhere;
  user-select: all;
}

/* ================================================================
   ESTADO VACÍO — `.cv-empty`
   Ocho pantallas tienen estado vacío y las ocho lo resolvían con una frase gris de 14px
   flotando donde debería ir una tabla. Un panel recién instalado —justo lo que ve un cliente
   evaluando el producto— está compuesto casi por entero de esos vacíos: la primera impresión
   del producto era su peor pantalla.
   ================================================================ */
.cv-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-5);
  text-align: center;
  border: 1px dashed var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
  color: var(--cv-text-muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
}
.cv-empty > * { margin: 0; max-width: var(--measure-short); }
/* Tesela de icono: el vacío deja de ser "falta algo" y pasa a ser un estado con forma.
   Neutra a propósito (outline sobre superficie tonal): no es un error ni una alerta. */
.cv-empty-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-bottom: var(--sp-1);
  border-radius: var(--cv-radius-pill);
  background: var(--cv-surface-alt);
  color: var(--cv-outline);
}
.cv-empty-title {
  font-family: var(--cv-font-head);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--cv-text);
}
.cv-empty .cv-btn { margin-top: var(--sp-3); }
/* Dentro de una tarjeta de datos el vacío ocupa el sitio de la tabla: sin doble marco. */
.cv-card > .cv-empty { border: 0; background: transparent; padding: var(--sp-6) var(--sp-4); }

/* ---------- Barra de filtros ----------
   Los campos fluyen en línea y se apilan solos cuando no caben, sin media queries.
   `min-width` evita que un <select> se estruje hasta ser inservible en móvil. Las etiquetas
   se tratan como RÓTULOS de control (micro, versal, tenues), no como títulos de campo: así
   la barra se lee de un vistazo como herramienta y no compite con el formulario de alta que
   suele tener debajo, que es donde estaban pesando lo mismo. */
.cv-form--filtros {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-3);
}
.cv-form--filtros .cv-field { flex: 1 1 180px; min-width: 160px; margin: 0; gap: var(--sp-1); }
.cv-form--filtros .cv-field > span {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
}
.cv-form--filtros .cv-btn { flex: 0 0 auto; }
/* La ayuda de un filtro es una nota al pie del control: no puede empujar la fila entera. */
.cv-form--filtros .cv-field small { margin-top: var(--sp-1); }

/* Formulario de una sola acción dentro de una celda o junto a un texto. */
.cv-form--inline {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.cv-form--inline .cv-field { margin: 0; }
/* Un <input> suelto dentro de un formulario en línea (la contraseña nueva de una cuenta
   colaboradora) no está envuelto en `.cv-field`: sin esto conserva la caja nativa del
   navegador y desentona con todos los demás controles de la aplicación. */
.cv-form--inline input:not([type="hidden"]) {
  font: inherit;
  font-size: var(--fs-sm);
  min-height: 36px;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface);
  color: var(--cv-text);
  min-width: 0;
  flex: 1 1 12rem;
}
.cv-form--inline input:not([type="hidden"]):focus { border-color: var(--cv-primary); }
/* En una celda de acciones el desplegable ocupa su propia línea: al abrirse no empuja al
   botón de al lado ni deforma la fila. */
.cv-actions-cell > .cv-disclosure { flex: 1 1 100%; }
@media (max-width: 900px) {
  .cv-form--inline input:not([type="hidden"]) { min-height: 44px; font-size: var(--fs-base); }
}
/* ================================================================
   PORTAL DEL CONSUMIDOR (público, móvil-first) — tema slate "LegalSafe"
   Mucho aire, lectura cómoda, trámite legal legítimo. Targets >=44px.
   ================================================================ */
.cv-portal {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
  padding: 0;
  font-size: var(--fs-lg);          /* portal: 18px efectivo, lectura cómoda en móvil */
  line-height: 1.6;
  overflow-x: hidden;               /* nada de scroll horizontal */
}
/* Texto largo (código CV-…, emails, teléfonos, URLs) hace wrap, nunca desborda a 360px. */
.cv-portal p,
.cv-portal li,
.cv-portal dd,
.cv-portal .cv-link,
.cv-portal .cv-code-value { overflow-wrap: anywhere; }

/* Cabecera de marca "LegalSafe" con escudo (centrada, separador tenue) */
.cv-portal-header {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-5) var(--sp-4);
  background: var(--cv-surface);
  border-bottom: 1px solid var(--cv-border-soft);
}
.cv-portal-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--cv-font-head);
  font-size: var(--fs-xl);
  font-weight: 700;
  color: var(--cv-primary);
  letter-spacing: -.01em;
}
.cv-portal-brand .cv-ico { color: var(--cv-primary); }

/* Logotipo de la empresa colaboradora en la cabecera del portal público.
   Alto fijo y ancho libre: los logos llegan en proporciones muy distintas y lo que tiene que
   mantenerse constante es la altura de la cabecera, no el ancho del logo. `object-fit: contain`
   evita que un logo apaisado se recorte. El tope de ancho impide que una imagen muy alargada
   empuje el nombre fuera de la pantalla en un móvil. */
/* Pantalla sin menú lateral: la de aceptar condiciones se muestra antes de que el colaborador
   tenga acceso a nada, así que no debe pintar una navegación que todavía no puede usar. */
.cv-main--solo {
  max-width: 52rem;
  margin: 0 auto;
  padding-left: var(--sp-5);
  padding-right: var(--sp-5);
}

/* ================================================================
   ACEPTACIÓN DE CONDICIONES DEL COLABORADOR (condiciones.php)
   2.900 palabras, 14 cláusulas, y quien las abre es un comercial con el móvil en la calle.
   Se pintaba con nl2br() sobre texto plano: párrafos simulados con saltos dobles, espaciado
   irregular y CERO diferencia visual entre el título de una cláusula y su contenido. 18.000
   caracteres de masa uniforme, que es la forma más eficaz de garantizar que nadie los lea.
   El cuerpo pasa a renderizarse con estructura real (h2/h3/p/li) — sólo presentación: no se
   altera ni se recorta el texto, y el hash se sigue calculando sobre el texto almacenado.
   ================================================================ */

/* ---------- Cabecera de identidad ----------
   La pantalla no tiene menú lateral (al colaborador que aún no ha aceptado no se le enseña una
   navegación que no puede usar), y eso la dejaba huérfana: sin cabecera de ningún tipo. Quien
   está aceptando un contrato tiene que saber con quién. */
.cv-doc-head {
  border-bottom: 1px solid var(--cv-border);
  background: var(--cv-surface);
}
.cv-doc-head-inner {
  max-width: 52rem;
  margin: 0 auto;
  padding: var(--sp-4) var(--sp-5);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-4);
}
.cv-doc-mark {
  width: 40px; height: 40px; flex-shrink: 0;
  border-radius: var(--cv-radius-sm);
  background: var(--cv-primary); color: var(--cv-primary-text);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--cv-font-head); font-weight: var(--fw-black); font-size: var(--fs-lg);
}
.cv-doc-id { display: flex; flex-direction: column; min-width: 0; margin-right: auto; }
.cv-doc-id > * { margin: 0; }
.cv-doc-org {
  font-family: var(--cv-font-head);
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  color: var(--cv-text);
  overflow-wrap: anywhere;
}
.cv-doc-kind {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
}
.cv-doc-version {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--cv-text-muted);
  white-space: nowrap;
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-pill);
}

/* ---------- El documento ---------- */
.ls-terminos {
  max-width: 66ch;
  line-height: var(--lh-relaxed);
  font-size: var(--fs-base);
  color: var(--cv-text);
}
.ls-terminos > :first-child { margin-top: 0; }
.ls-terminos > :last-child { margin-bottom: 0; }
.ls-terminos p { margin: 0 0 var(--sp-4); overflow-wrap: break-word; }
/* Título de PARTE / cláusula: el escalón que no existía. */
.ls-terminos h2 {
  font-size: var(--fs-xl);
  font-weight: var(--fw-black);
  line-height: var(--lh-tight);
  color: var(--cv-text);
  margin: var(--sp-7) 0 var(--sp-3);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--cv-border);
}
.ls-terminos h3 {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--cv-text);
  margin: var(--sp-6) 0 var(--sp-2);
}
.ls-terminos ul { margin: 0 0 var(--sp-4); padding-left: var(--sp-5); display: grid; gap: var(--sp-2); }
.ls-terminos li { overflow-wrap: break-word; }
/* La tarjeta del documento cede la medida al propio documento. */
.cv-terms-doc { max-width: none; }
.cv-terms-doc > .ls-terminos { margin-inline: auto; }

/* ---------- El bloque de aceptación ----------
   Dos exigencias que NO son estéticas y gobiernan este bloque:
     · la casilla se sirve vacía, siempre (ningún truco visual que sugiera lo contrario);
     · el rechazo pesa lo mismo que la aceptación — misma caja, misma altura, mismo ancho.
   Un botón grande de aceptar junto a un enlace gris no es una elección libre: es un embudo. */
.cv-terms-consent { max-width: none; }
.cv-terms-consent .cv-form { max-width: none; gap: var(--sp-4); }
.cv-terms-consent .cv-check { background: var(--cv-surface); }
/* Los dos botones comparten EXACTAMENTE la misma caja. El de rechazo no es "el otro": es
   la mitad de la decisión, y ocupa la mitad del espacio. */
.cv-terms-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
  margin: 0;
}
.cv-terms-actions .cv-btn { width: 100%; min-height: 52px; font-size: var(--fs-base); }
.cv-terms-nota { font-size: var(--fs-sm); color: var(--cv-text-muted); max-width: var(--measure); }

/* ---------- Móvil: la aceptación deja de estar al final del túnel ----------
   Eran varias pantallas de scroll hasta la casilla, así que RECHAZAR salía más caro que
   aceptar: había que recorrer el documento entero sólo para poder decir que no. El bloque de
   aceptación se ancla al pie de la ventana: la casilla (vacía) y los DOS botones están
   siempre a un toque, y el documento sigue íntegro y por delante. */
@media (max-width: 900px) {
  .cv-terms-consent {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 20;
    max-width: none;
    margin: 0;
    border: 0;
    border-top: 1px solid var(--cv-border);
    border-radius: 0;
    box-shadow: var(--cv-shadow-up);
    padding: var(--sp-3) var(--sp-4) calc(var(--sp-3) + env(safe-area-inset-bottom));
  }
  .cv-terms-consent .cv-form { gap: var(--sp-3); }
  .cv-terms-consent .cv-check { padding: var(--sp-2) var(--sp-3); min-height: 44px; }
  .cv-terms-consent .cv-check > span { font-size: var(--fs-sm); }
  .cv-terms-actions .cv-btn { min-height: 48px; }
  /* La nota ocupa demasiado en una barra fija; su contenido lo repite el propio botón. */
  .cv-terms-nota { display: none; }
  /* Hueco para que la barra no tape el final del documento. */
  .cv-terms { padding-bottom: 15rem; }
}
@media (max-width: 400px) {
  /* En pantallas muy estrechas los dos botones se apilan, pero SIGUEN siendo idénticos. */
  .cv-terms-actions { grid-template-columns: 1fr; }
}
.cv-portal-logo {
  height: 28px;
  width: auto;
  max-width: 160px;
  object-fit: contain;
  display: block;
}
/* Tarjeta principal del portal (autorización, justificante, baja).
   RITMO POR GAP: la tarjeta es un contenedor flex en columna; la separación entre bloques
   (remitente, info, casillas, confirmación, confianza…) la fija UN SOLO `gap` (--sp-5), no
   márgenes ad-hoc de cada bloque. Así la cadencia es uniforme en todas las pantallas del portal
   (autorización / justificante / baja) que comparten esta tarjeta. Réplica del mockup aprobado. */
.cv-portal-card {
  width: 100%;
  max-width: 640px;
  background: var(--cv-surface);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  box-shadow: var(--cv-shadow);
  padding: var(--sp-6);
  margin: var(--sp-6) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}
/* El gap gobierna el ritmo: los hijos directos NO aportan sus propios márgenes verticales
   (evita dobles espacios y aire irregular). Splash/summary/code-box mantienen su espaciado
   INTERNO porque sus elementos no son hijos directos de la tarjeta. */
.cv-portal-card > * { margin-top: 0; margin-bottom: 0; }
/* Excepción: el eyebrow se PEGA a su contenido (título / lista de info / casillas). Con gap
   uniforme, un margen inferior negativo deja la separación neta en --sp-2 (sp-5 + (sp-2 - sp-5)). */
.cv-portal-card > .cv-eyebrow { margin-bottom: calc(var(--sp-2) - var(--sp-5)); }
.cv-portal-card h1 {
  font-size: var(--fs-2xl);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -.02em;
  text-wrap: balance;
  color: var(--cv-text);
}
.cv-lead { font-size: var(--fs-lg); color: var(--cv-text); margin: 0 0 var(--sp-4); }
/* `.cv-muted` tiene 119 usos haciendo CINCO trabajos (subtítulo de página, ayuda de campo,
   estado vacío, dato secundario de celda, prosa explicativa) y los cinco se pintaban igual:
   14px gris. Ésa es la causa de que "todo pese lo mismo". En vez de partirla en cinco clases
   —y editar 119 sitios— se hace SENSIBLE AL CONTEXTO: misma clase, peso correcto en cada sitio. */
.cv-muted { color: var(--cv-text-muted); font-size: var(--fs-sm); line-height: 1.6; }
/* 1. Subtítulo de página: acompaña a un h1 de 28px, no puede ser una nota al pie. */
.cv-page-head .cv-muted { font-size: var(--fs-base); max-width: var(--measure); }
/* 2. Ayuda de campo: es una nota, y debe leerse como tal. */
.cv-field small.cv-muted,
.cv-field > small,
small.cv-muted { display: block; font-size: var(--fs-xs); line-height: 1.5; max-width: var(--measure); }
/* 3. Prosa explicativa dentro de tarjeta o sección: medida de lectura acotada. */
.cv-card > p.cv-muted,
.cv-section > p.cv-muted { max-width: var(--measure); }
/* 4. Dato secundario dentro de una celda: no compite con el dato principal. */
.cv-table .cv-muted { font-size: var(--fs-xs); line-height: 1.4; }

/* Enlaces dentro del portal: subrayados, target táctil cómodo sin romper el flujo del texto. */
.cv-portal .cv-link { text-decoration: underline; padding: 2px 1px; }
.cv-portal .cv-link:active { color: var(--cv-primary-hover); }
/* Enlace de acción independiente (no inline en un párrafo): bloque táctil >=44px. */
.cv-link-action {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 44px;
  padding: var(--sp-2) var(--sp-1);
  font-weight: 600;
}

/* Ritmo vertical del portal: los bloques son hijos directos de .cv-portal-card
   (contenedor en bloque), así que sus márgenes verticales COLAPSAN entre sí y no
   se acumulan. El eyebrow queda pegado a su contenido (etiqueta de la sección). */

/* Rótulo de sección (eyebrow): mayúsculas pequeñas, neutro (outline). */
.cv-portal .cv-eyebrow {
  font-size: var(--fs-xs);
  letter-spacing: .09em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--cv-outline);
  margin: 0 0 var(--sp-2);
}
/* Intro de UI (chrome neutro, no literal legal): párrafo introductorio tranquilo.
   Sin margen propio: el gap de la tarjeta fija su separación. */
.cv-intro {
  margin: 0;
  color: var(--cv-text-muted);
  font-size: var(--fs-lg);
  line-height: 1.6;
}

/* ---------- Tarjeta de remitente (Responsable del tratamiento) ----------
   Rótulo NEUTRO: sin verde, sin badge "verificado". La jerarquía es tipográfica.
   Fondo tonal tenue (primary-soft) = sobrio, transmite trámite, no "sello". */
.cv-sender {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  background: var(--cv-primary-soft);
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius);
  padding: var(--sp-4);
}
.cv-sender-mark {
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  border-radius: var(--cv-radius-sm);
  background: var(--cv-primary);
  color: var(--cv-primary-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--cv-font-head);
  font-weight: 800;
  font-size: var(--fs-lg);
  letter-spacing: .02em;
}
.cv-sender-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; margin: 0; }
.cv-sender-body > p { margin: 0; }
/* Rótulo del remitente: NEUTRO (text-muted), sin color de "verificado". */
.cv-sender-label {
  font-size: var(--fs-xs);
  letter-spacing: .06em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--cv-text-muted);
}
.cv-sender-name { font-weight: 700; font-size: var(--fs-base); color: var(--cv-text); }
.cv-sender-meta { font-size: var(--fs-sm); color: var(--cv-text-muted); }
.cv-sender-via { font-size: var(--fs-sm); color: var(--cv-text-muted); margin-top: var(--sp-2); }
.cv-sender-via b { color: var(--cv-text); font-weight: 600; }

/* ---------- Panel de información básica RGPD (en capas, visible) ----------
   <dl> con filas <div.cv-info-row><dt><dd>. Divisores tenues, bordes suaves. */
.cv-info {
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius);
  overflow: hidden;
}
.cv-info-row {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  border-top: 1px solid var(--cv-border-soft);
}
.cv-info-row:first-child { border-top: 0; }
.cv-info-row dt {
  color: var(--cv-outline);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  font-size: var(--fs-xs);
  padding-top: 2px;
}
.cv-info-row dd { margin: 0; color: var(--cv-text-muted); }
.cv-info-row dd b { color: var(--cv-text); font-weight: 600; }
/* Blob de información básica (info_basica_rgpd): fila a ancho completo, texto con saltos. */
.cv-info-note {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--cv-border-soft);
  font-size: var(--fs-sm);
  color: var(--cv-text-muted);
  line-height: 1.6;
}
.cv-info-foot {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--cv-border-soft);
  background: var(--cv-surface-alt);
  font-size: var(--fs-sm);
  color: var(--cv-text-muted);
  line-height: 1.6;
}

/* ---------- 2ª capa legal plegable (<details> nativo, sin JS, CSP-safe) ----------
   Texto legal completo (info_basica_rgpd) plegado. El <summary> es un control claro y clicable
   con chevron propio dibujado con borde CSS (sin icono externo). */
.cv-info-more {
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
}
.cv-info-more summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 44px;                 /* target táctil cómodo */
  padding: var(--sp-3) var(--sp-4);
  cursor: pointer;
  font-weight: 600;
  font-size: var(--fs-sm);
  color: var(--cv-primary);         /* se lee como control, no como texto muerto */
  list-style: none;                 /* Firefox: oculta el triángulo nativo */
  -webkit-tap-highlight-color: transparent;
}
.cv-info-more summary::-webkit-details-marker { display: none; } /* Safari/Chrome: sin triángulo */
/* Chevron propio (borde CSS): apunta hacia abajo cerrado, hacia arriba abierto. */
.cv-info-more summary::after {
  content: "";
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  margin-right: var(--sp-1);
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform .15s ease;
}
.cv-info-more[open] summary::after { transform: rotate(225deg); }
/* Foco de teclado bien visible dentro del borde del contenedor (no recortado). */
.cv-info-more summary:focus-visible {
  outline: 3px solid var(--cv-focus);
  outline-offset: -3px;
  border-radius: var(--cv-radius);
}
/* Cuerpo: texto legal denso, legible y separado del control. */
.cv-info-more-body {
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  border-top: 1px solid var(--cv-border-soft);
  font-size: var(--fs-sm);
  color: var(--cv-text-muted);
  line-height: 1.6;
}
@media (prefers-reduced-motion: reduce) {
  .cv-info-more summary::after { transition: none; }
}

/* ---------- Formulario de consentimiento ---------- */
/* Ritmo interno por gap (igual criterio que la tarjeta): eyebrow → casillas → confirmación →
   acciones separadas de forma uniforme. Sin margin-top: la separación con el bloque anterior
   la fija el gap de la tarjeta. */
.cv-consent-form { display: flex; flex-direction: column; gap: var(--sp-4); }
.cv-consent-form > * { margin-top: 0; margin-bottom: 0; }
/* El eyebrow "Qué nos autorizas" se pega a las casillas (neto --sp-2 con gap --sp-4). */
.cv-consent-form > .cv-eyebrow { margin-bottom: calc(var(--sp-2) - var(--sp-4)); }
.cv-field { display: grid; gap: var(--sp-2); font-size: var(--fs-base); }
/* Panel: etiquetas de campo a la escala de la tabla (--fs-sm) para una tipografía
   coherente en todo el SaaS. El portal (.cv-portal) NO se toca: su texto mayor es
   deliberado (legibilidad legal en móvil). */
.cv-panel .cv-field { font-size: var(--fs-sm); }
.cv-field textarea,
.cv-field input,
.cv-field select {
  padding: var(--sp-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface);
  font: inherit;
  /* >=16px SIEMPRE: evita el zoom involuntario de iOS al enfocar un campo. */
  font-size: max(16px, var(--fs-base));
  min-height: 48px;
  width: 100%;
}
.cv-field textarea { min-height: 88px; line-height: 1.5; }
/* Panel: el texto DENTRO de los controles también a --fs-sm (14px) para uniformidad total.
   Se acepta <16px porque el panel es escritorio (admin); el zoom de iOS no aplica aquí.
   El portal (.cv-portal) conserva el mínimo de 16px de arriba: es móvil y evita el zoom. */
.cv-panel .cv-field textarea,
.cv-panel .cv-field input,
.cv-panel .cv-field select { font-size: var(--fs-sm); }
.cv-field textarea:focus,
.cv-field input:focus,
.cv-field select:focus { border-color: var(--cv-primary); }

/* Casilla de consentimiento: grande, área de toque amplia, HONESTAMENTE desmarcada.
   Sin ningún "look" de activado por defecto (RGPD: acto afirmativo). Separación clara. */
.cv-check {
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: var(--sp-4);
  align-items: center;            /* checkbox alineado con la primera línea de texto */
  cursor: pointer;
  padding: var(--sp-4);
  min-height: 48px;               /* área de toque cómoda en toda la fila */
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
  -webkit-tap-highlight-color: transparent;
}
.cv-check:hover { background: var(--cv-bg); }
.cv-check:active { background: var(--cv-surface-alt); }
/* Foco por teclado en el checkbox => resalta toda la fila (la fila es el target). */
.cv-check:focus-within { border-color: var(--cv-primary); outline: 3px solid var(--cv-focus); outline-offset: 2px; }
.cv-check input[type="checkbox"] {
  width: 26px;
  height: 26px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--cv-primary);   /* color slate SOLO cuando el usuario marca activamente */
}
/* La fila ya muestra el foco (focus-within); evita anillo doble en el propio checkbox. */
.cv-check input[type="checkbox"]:focus-visible { outline: none; }
.cv-check > span { font-size: var(--fs-base); color: var(--cv-text); }
.cv-check em {
  display: inline-block;
  color: var(--cv-outline);
  font-style: normal;
  font-size: var(--fs-sm);
  font-weight: 500;
}

/* Grupo de casillas apiladas (canales del panel, etc.) */
.cv-check-group { display: grid; gap: var(--sp-3); }

/* ---------- Casillas del portal (cabecera de canal + literal legal de BD) ---------- */
/* Contenedor: separación clara entre opciones (no marcar la de al lado por error). */
.cv-checks { display: flex; flex-direction: column; gap: var(--sp-3); }

/* En el portal la casilla tiene cabecera (icono + píldora) y texto en varias líneas:
   alinea el checkbox ARRIBA, no centrado. */
.cv-portal .cv-check { align-items: start; }
.cv-portal .cv-check:hover { background: var(--cv-primary-soft); }

/* Checkbox del portal dibujado a medida para un contraste marcado/desmarcado INEQUÍVOCO:
   - Desmarcado = caja BLANCA con borde neutro (outline). Ni rastro de color de acento.
   - Marcado    = relleno slate (--cv-primary) + check blanco.
   El relleno es la ÚNICA señal de "activo" (RGPD art. 4.11/7: acto afirmativo honesto). */
.cv-portal .cv-check input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 26px;
  height: 26px;
  margin: 2px 0 0;
  border: 2px solid var(--cv-outline);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface);
  position: relative;
  cursor: pointer;
  flex-shrink: 0;
}
.cv-portal .cv-check input[type="checkbox"]:checked {
  background: var(--cv-primary);
  border-color: var(--cv-primary);
}
.cv-portal .cv-check input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 8px;
  top: 3px;
  width: 6px;
  height: 12px;
  border: solid var(--cv-primary-text);
  border-width: 0 3px 3px 0;
  transform: rotate(45deg);
}

/* Cuerpo de la casilla (span): cabecera + literal legal de BD. */
.cv-check-body { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.cv-check-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* Icono de canal DECORATIVO en muted (outline): NUNCA en --cv-primary; no debe
   sugerir un estado "activo". La señal de activo es solo el checkbox relleno. */
.cv-check-ico { color: var(--cv-outline); display: inline-flex; flex-shrink: 0; }
.cv-check-text { font-size: var(--fs-base); color: var(--cv-text); line-height: 1.5; }

/* Píldoras Obligatorio/Opcional: NEUTRAS, jamás en --cv-primary (evita apariencia de
   activado). Se distinguen por relleno/peso; ambas con contraste AA sobre su fondo.
   --req: chip claro con borde (texto oscuro). --opt: aún más tenue (sin relleno). */
.cv-pill {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
  padding: 2px var(--sp-2);
  border-radius: var(--cv-radius-pill);
  white-space: nowrap;
}
.cv-pill--req { color: var(--cv-text); background: var(--cv-surface-alt); border: 1px solid var(--cv-border); }
.cv-pill--opt { color: var(--cv-text-muted); background: transparent; border: 1px solid var(--cv-border-soft); }

/* Frase de confirmación: BANDA legible (no letra pequeña gris), justo encima del botón.
   El icono de la banda es decoración de refuerzo, no una casilla: puede usar el acento. */
.cv-confirm {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  margin: 0;
  padding: var(--sp-4);
  background: var(--cv-primary-soft);
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius);
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--cv-text);
  line-height: 1.5;
}
.cv-confirm-ico { color: var(--cv-primary); flex-shrink: 0; margin-top: 2px; display: inline-flex; }
.cv-confirm-text { min-width: 0; }

/* ---------- Botones del portal ---------- */
.cv-actions { display: flex; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-2); }
/* Un `.cv-btn` SIN modificador era invisible: `border: 1px solid transparent` y ningún
   `background`. Nueve botones reales se pintaban como texto suelto, entre ellos el
   "No acepto" de condiciones.php, lo que producía justo el embudo que ese fichero prohíbe.
   La base pasa a ser un SECUNDARIO legítimo: superficie, borde y texto de contraste AA. */
.cv-btn {
  font: inherit;
  font-weight: var(--fw-semibold);
  background: var(--cv-surface);
  border-color: var(--cv-border);
  color: var(--cv-text);
  font-size: var(--fs-base);
  padding: var(--sp-3) var(--sp-6);
  border-radius: var(--cv-radius-sm);
  /* El borde iba DESPUÉS de border-color, con lo que lo pisaba y lo dejaba
     transparente: todos los botones secundarios de la aplicación se pintaban sin
     contorno. En el tema claro se disimulaba porque la superficie contrasta con el
     fondo de página; en oscuro, la superficie del botón ES la de la tarjeta que lo
     contiene, así que el botón desaparecía y se leía como texto suelto. */
  border: 1px solid var(--cv-border);
  cursor: pointer;
  /* Un botón nunca se subraya, sea <button> o <a> (evita el underline heredado de enlace). */
  text-decoration: none;
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  -webkit-tap-highlight-color: transparent;
  transition: background-color .12s ease, transform .06s ease;
}
.cv-btn:hover { background: var(--cv-surface-alt); border-color: var(--cv-outline); }
.cv-btn:active { transform: scale(.985); }
.cv-btn:hover, .cv-btn:focus, .cv-btn:focus-visible { text-decoration: none; }
/* Deshabilitado: no había NINGUNA regla en todo el CSS, así que un botón `disabled`
   (p.ej. "no puedes desactivarte a ti mismo" en usuarios.php) se veía idéntico a uno activo,
   porque las variantes fuerzan `color` y anulan el gris del user-agent. */
.cv-btn:disabled, .cv-btn[disabled], .cv-btn[aria-disabled="true"] {
  background: var(--cv-surface-alt);
  border-color: var(--cv-border-soft);
  color: var(--cv-text-muted);
  box-shadow: none;
  opacity: .65;
  cursor: not-allowed;
  transform: none;
}
.cv-btn:disabled:hover, .cv-btn[disabled]:hover {
  background: var(--cv-surface-alt); border-color: var(--cv-border-soft);
}
.cv-btn--primary { background: var(--cv-primary); border-color: var(--cv-primary); color: var(--cv-primary-text); box-shadow: var(--cv-shadow); }
.cv-btn--primary:hover { background: var(--cv-primary-hover); }
.cv-btn--primary:active { background: var(--cv-primary-hover); }
/* "Rechazar": igual de accesible que aceptar (no oscurecido). Borde slate, texto slate. */
.cv-btn--ghost { background: transparent; border-color: var(--cv-outline); color: var(--cv-primary); }
.cv-btn--ghost:hover { background: var(--cv-bg); }
.cv-btn--ghost:active { background: var(--cv-surface-alt); }
/* Acción DESTRUCTIVA (Archivar, Desactivar, Pausar, Retirar autorización). Existía el token
   --cv-danger y ningún botón lo usaba: se veían igual que "Volver". Contorno, no relleno:
   avisa sin gritar y mantiene AA (rojo #ba1a1a sobre blanco = 6.5:1). */
.cv-btn--danger { background: var(--cv-surface); border-color: var(--cv-danger); color: var(--cv-danger); }
.cv-btn--danger:hover { background: var(--cv-danger-soft); border-color: var(--cv-danger); }
.cv-btn--block { width: 100%; }

/* <button class="cv-link"> (formularios.php "Anular"): sin esto conserva la caja gris
   nativa del navegador con el texto en azul dentro. Se normaliza a enlace de texto real. */
button.cv-link {
  background: none; border: 0; padding: 0;
  font: inherit; font-weight: var(--fw-medium);
  color: var(--cv-primary); cursor: pointer; text-decoration: underline;
}

/* Acciones del portal: SIEMPRE apiladas y a ancho completo (todas las anchuras).
   "Rechazar" (--ghost) conserva EXACTAMENTE la misma caja que "Quiero que me llamen":
   mismo ancho, mismo min-height (>=56px), borde visible. El rechazo fácil es requisito. */
/* Acciones apiladas; la separación con la banda de confirmación la fija el gap del formulario. */
.cv-portal .cv-actions { flex-direction: column; flex-wrap: nowrap; gap: var(--sp-3); margin-top: 0; }
.cv-portal .cv-actions .cv-btn { width: 100%; min-height: 56px; font-size: var(--fs-lg); }
.cv-portal .cv-btn--ghost { color: var(--cv-text); border-color: var(--cv-outline); }

/* ---------- Pantalla de ÉXITO (justificante / consentimiento registrado) ---------- */
.cv-splash { text-align: center; display: flex; flex-direction: column; align-items: center; }
.cv-splash-icon {
  width: 80px; height: 80px; border-radius: var(--cv-radius-pill);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto var(--sp-5);
}
.cv-splash-icon--ok    { background: var(--cv-primary-soft); color: var(--cv-primary); }
.cv-splash-icon--error { background: var(--cv-danger-bg);    color: var(--cv-danger); }
.cv-splash h1 { margin: 0 0 var(--sp-3); }
.cv-splash .cv-lead { color: var(--cv-text-muted); max-width: 42ch; }

/* Caja del código de justificante (sub-superficie tonal) */
.cv-code-box {
  width: 100%;
  background: var(--cv-surface-alt);
  border: 1px solid var(--cv-border-soft);
  border-radius: var(--cv-radius);
  padding: var(--sp-4);
  text-align: center;
}
.cv-code-box .cv-code-label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
  margin-bottom: var(--sp-1);
}
.cv-code-box .cv-code-value {
  display: block;
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: .05em;
  color: var(--cv-text);
}

/* Resumen de autorización (lista clave/valor con divisores tenues) */
.cv-summary { width: 100%; }
.cv-summary h2 {
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--cv-text);
  border-bottom: 1px solid var(--cv-border-soft);
  padding-bottom: var(--sp-2);
  margin: 0 0 var(--sp-3);
}
.cv-summary dl { margin: 0; }
.cv-summary dl > div {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--sp-4);
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--cv-border-soft);
  font-size: var(--fs-base);
}
.cv-summary dl > div:last-child { border-bottom: 0; }
.cv-summary dt { color: var(--cv-text-muted); margin: 0; }
.cv-summary dd { margin: 0; text-align: right; color: var(--cv-text); font-weight: 500; }
.cv-summary .cv-chan { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-1); }
.cv-summary .cv-chan span { display: inline-flex; align-items: center; gap: var(--sp-2); }

/* Chip de estado SUAVE para el justificante (color + icono + texto). */
.cv-chip-soft {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px var(--sp-2);
  border-radius: var(--cv-radius-sm);
  font-size: var(--fs-sm);
  font-weight: 700;
}
.cv-chip-soft--aceptado { background: var(--cv-aceptado-bg); color: var(--cv-aceptado-text); }
.cv-chip-soft--rechazado { background: var(--cv-danger-bg); color: var(--cv-rechazado); }

/* ---------- Pantalla de error/expirado (icono en círculo + acción "Volver") ---------- */
.cv-error-actions { width: 100%; margin-top: var(--sp-5); }

/* ---------- Login del panel (tarjeta centrada con marca LegalSafe) ---------- */
.cv-login { justify-content: center; }
.cv-login-card { max-width: 420px; }
.cv-login-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--cv-primary);
  margin-bottom: var(--sp-5);
}
.cv-login-brand .cv-ico { color: var(--cv-primary); }
.cv-login-brand .cv-brand-name {
  font-family: var(--cv-font-head);
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: -.01em;
  color: var(--cv-primary);
}

/* ---------- Pie de confianza (3 garantías) ----------
   Iconos DECORATIVOS en muted (outline): nada de verde de "verificado". */
.cv-trust {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-5);
  justify-content: center;
  padding-top: var(--sp-4);
  border-top: 1px solid var(--cv-border-soft);
}
.cv-trust-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--cv-text-muted);
}
.cv-trust-ico { color: var(--cv-outline); display: inline-flex; flex-shrink: 0; }

/* ================================================================
   PORTAL VIGENTE DEL MODELO A (`/l/<token>` -> ls_portal.php, `/j/<token>` -> j.php)
   Estas siete clases estaban EN EL MARKUP y no tenían una sola regla de CSS. Consecuencia:
   los dos <fieldset> del formulario que ve el consumidor se pintaban con el borde `groove`
   de 2px del user-agent y el <dl> del justificante con los <dd> sangrados 40px. La cara
   pública del producto tenía aspecto de formulario HTML de 1998.
   (`cv_form.php`, el portal del modelo B RETIRADO, sí estaba estilado; se quedó sin CSS
   justamente el que está vivo.)
   ================================================================ */

/* ---------- Bloque de texto legal (art. 13) ----------
   Documento largo que hay que poder leer: medida corta, interlineado holgado y jerarquía
   real entre el encabezado de cláusula y su cuerpo. */
.ls-legal {
  max-width: 66ch;
  line-height: var(--lh-relaxed);
  color: var(--cv-text);
}
.ls-legal > p { margin: 0 0 var(--sp-4); overflow-wrap: anywhere; }
.ls-legal > p:last-child { margin-bottom: 0; }
.ls-legal-h,
.ls-legal > h2 {
  font-family: var(--cv-font-head);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--cv-text);
  margin: var(--sp-6) 0 var(--sp-2);
}
.ls-legal > :first-child { margin-top: 0; }
/* En la ficha de lead del PANEL, `.ls-legal` cita texto sellado: se marca como cita literal
   para que no se confunda con la interfaz que lo rodea. */
.cv-panel .ls-legal {
  padding: var(--sp-3) var(--sp-4);
  border-left: 3px solid var(--cv-border);
  background: var(--cv-surface-alt);
  border-radius: 0 var(--cv-radius-sm) var(--cv-radius-sm) 0;
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

/* ---------- Los dos <fieldset> del formulario público ----------
   "Tus datos" y "Tu solicitud". Grupos claramente separados, sin borde de user-agent. */
.ls-campos,
.ls-permisos {
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
  padding: var(--sp-5) var(--sp-4);
  margin: 0 0 var(--sp-5);
  display: grid;
  gap: var(--sp-4);
  min-width: 0;   /* un fieldset no encoge por defecto: sin esto desborda en móvil */
}
.ls-campos > legend,
.ls-permisos > legend {
  font-family: var(--cv-font-head);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--cv-text);
  padding: 0 var(--sp-2);
  margin-left: calc(var(--sp-2) * -1);
}
/* "Tu solicitud" agrupa las casillas: fondo tonal para separarlo de la captura de datos,
   sin dar a ninguna casilla el menor aspecto de estar marcada. */
.ls-permisos { background: var(--cv-primary-soft); border-color: var(--cv-border-soft); }
.ls-permisos .cv-check { background: var(--cv-surface); }
.ls-campos > .cv-field { gap: var(--sp-2); }

/* ---------- Resumen del justificante (`.ls-resumen`) ----------
   <dl> de pares clave/valor. Se pintaba crudo, con el sangrado nativo de los <dd>.
   Usado en el portal (j.php) y en el panel (leads.php, mis_enlaces.php). */
.ls-resumen {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) 1fr;
  gap: 0;
  border-top: 1px solid var(--cv-border-soft);
}
.ls-resumen > dt {
  margin: 0;
  padding: var(--sp-3) var(--sp-4) var(--sp-3) 0;
  border-bottom: 1px solid var(--cv-border-soft);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--cv-text-muted);
}
.ls-resumen > dd {
  margin: 0;
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--cv-border-soft);
  color: var(--cv-text);
  min-width: 0;
  overflow-wrap: anywhere;
}
.ls-resumen > dd code { font-size: var(--fs-xs); color: var(--cv-text-muted); }
/* En móvil la clave va encima del valor: dos columnas rompen con hashes y teléfonos. */
@media (max-width: 640px) {
  .ls-resumen { grid-template-columns: 1fr; }
  .ls-resumen > dt { padding-bottom: 0; border-bottom: 0; }
  .ls-resumen > dd { padding-top: var(--sp-1); }
}

/* ---------- "Lo que autorizaste" del justificante ----------
   Cada permiso es una fila con su texto y su acción de retirada. La retirada tiene que estar
   VISIBLE y ser cómoda: revocar debe ser tan fácil como autorizar (art. 7.3). */
.ls-permisos-estado { display: grid; gap: var(--sp-3); }
.ls-permiso {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
}
.ls-permiso > p { margin: 0; min-width: 14rem; flex: 1 1 14rem; }
.ls-permiso > form { margin: 0; flex: 0 0 auto; }
.ls-permiso > .cv-help { flex: 0 0 auto; margin: 0; }
@media (max-width: 640px) {
  .ls-permiso > form, .ls-permiso > form .cv-btn { width: 100%; }
}
/* ================= Portal en MÓVIL (móvil-first refinado) ================= */
@media (max-width: 640px) {
  /* Aprovecha el ancho: tarjeta casi a pantalla completa con márgenes laterales de 16px. */
  .cv-portal-card {
    padding: var(--sp-5) var(--sp-4);
    margin: var(--sp-4) var(--sp-4) calc(var(--sp-6) + env(safe-area-inset-bottom, 0px));
    border-radius: var(--cv-radius);
  }
  .cv-portal-card h1 { font-size: var(--fs-xl); }

  /* Cabecera de marca compacta: no roba altura útil. */
  .cv-portal-header { padding: var(--sp-3) var(--sp-4); }
  .cv-portal-brand { font-size: var(--fs-lg); }

  /* Casillas: más separación entre ellas para no marcar la de al lado por error. */
  .cv-consent-form { gap: var(--sp-4); }
  .cv-check { gap: var(--sp-3); padding: var(--sp-4) var(--sp-3); }

  /* Info RGPD: clave encima del valor (evita aprietes y overflow a 360px). */
  .cv-info-row { grid-template-columns: 1fr; gap: 2px; }

  /* Pie de confianza: menos separación horizontal cuando envuelve en varias líneas. */
  .cv-trust { gap: var(--sp-3); }

  /* (Los botones del portal ya van apilados y a ancho completo vía .cv-portal .cv-actions.) */

  /* Resumen del justificante: clave encima del valor (evita aprietes y overflow). */
  .cv-summary dl > div { flex-direction: column; gap: var(--sp-1); }
  .cv-summary dd { text-align: left; }
  .cv-summary .cv-chan { align-items: flex-start; }

  /* Acciones de error/baja a ancho completo. */
  .cv-error-actions .cv-btn { width: 100%; min-height: 52px; }
}

/* ============================================================================
   TEMA CONSOLA — AJUSTES DE COMPONENTE
   ============================================================================
   El cambio de piel lo hace tokens.css: aquí solo va lo que la redefinición de
   colores no puede resolver sola, porque es ESTRUCTURA.
   ========================================================================= */

/* ---------- Texto solo para lectores de pantalla ----------
   Un número suelto («14») no dice nada en voz alta. Va acompañado de la palabra
   que lo explica, visible solo para quien no ve la pantalla. */
.cv-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- Menú lateral: grupos, avisos y marca de posición ---------- */

/* Encabezado de grupo. Diminuto y en versales: tiene que ordenar sin competir
   con los destinos, que son lo que se pulsa. */
.cv-nav-grupo {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--cv-sidebar-muted);
  padding: var(--sp-4) var(--sp-3) var(--sp-1);
  pointer-events: none;
}
.cv-nav-grupo:first-child { padding-top: 0; }

.cv-nav a { position: relative; }
.cv-nav-txt { flex-grow: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* EL ACTIVO NO SE PINTA ENTERO DE ÁMBAR.
   Con el acento nuevo, el `background: var(--cv-primary)` que traía la regla
   original convertía el destino actual en una barra ámbar maciza: gritaba más
   que cualquier dato de la pantalla y competía con el botón de acción, que es
   el otro sitio donde vive el ámbar. Aquí el activo se marca por elevación
   (fondo un peldaño más claro) y una pestaña de 2px al borde. Se reconoce
   igual de rápido y no hay dos ámbares peleando. */
.cv-nav a.is-active,
.cv-nav a[aria-current="page"] {
  background: var(--cv-sidebar-hover);
  color: var(--cv-sidebar-brand);
}
.cv-nav a.is-active::before,
.cv-nav a[aria-current="page"]::before {
  content: "";
  position: absolute;
  left: 0; top: 8px; bottom: 8px;
  width: 2px;
  border-radius: 0 2px 2px 0;
  background: var(--cv-primary);
}

/* Contador de pendientes. Mono y tabular: son cifras, y en el menú van a estar
   una debajo de otra. */
.cv-nav-aviso {
  flex-shrink: 0;
  font-family: var(--cv-font-mono);
  font-size: 10px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1.6;
  padding: 0 var(--sp-2);
  border-radius: var(--cv-radius-pill);
  background: var(--cv-primary-soft);
  color: var(--cv-primary);
}
.cv-nav a.is-active .cv-nav-aviso,
.cv-nav a[aria-current="page"] .cv-nav-aviso {
  background: var(--cv-primary);
  color: var(--cv-primary-text);
}

/* En el menú, la densidad de escritorio puede bajar de 44px: no es una diana
   táctil mientras haya ratón. En táctil vuelve a 44 (media query de abajo). */
@media (min-width: 901px) {
  .cv-nav a { min-height: 34px; padding: var(--sp-2) var(--sp-3); font-weight: 500; }
  .cv-nav { gap: 2px; }
}

/* ---------- Campos sobre fondo oscuro ----------
   En claro, un input con el fondo de la tarjeta funciona: el borde basta para
   delimitarlo. En oscuro no: `--cv-surface` es a la vez el fondo de la tarjeta
   y el del campo, así que el campo desaparece y solo queda un rectángulo de
   borde. La convención que funciona en interfaces oscuras es la contraria a la
   clara: el campo se HUNDE respecto a la tarjeta, no flota sobre ella. */
.cv-panel .cv-field input,
.cv-panel .cv-field select,
.cv-panel .cv-field textarea,
.cv-panel .cv-form input,
.cv-panel .cv-form select,
.cv-panel .cv-form textarea,
.cv-panel .cv-form--inline input {
  background: var(--cv-input-bg);
  color: var(--cv-text);
}
.cv-panel input::placeholder,
.cv-panel textarea::placeholder { color: var(--cv-outline); opacity: 1; }

/* El foco del campo: borde ámbar además del anillo. Sobre oscuro, un anillo
   suelto se pierde contra el borde de la tarjeta que tiene al lado. */
.cv-panel .cv-field input:focus,
.cv-panel .cv-field select:focus,
.cv-panel .cv-field textarea:focus { border-color: var(--cv-primary); }

/* ---------- «¿Puedo llamar?»: el veredicto ----------
   Es la pantalla que más veces se abre al día y la única cuya respuesta tiene
   consecuencia legal directa, así que la respuesta tiene que leerse desde lejos
   y sin ambigüedad. Sobre fondo oscuro había dos problemas: el círculo del
   icono usaba `--cv-surface`, que es EXACTAMENTE el fondo de la tarjeta que lo
   contiene —el icono flotaba sin caja—, y el veredicto pesaba lo mismo que
   cualquier otra tarjeta de la página.

   Aquí el bloque entero se tiñe del tono de la respuesta, el círculo pasa a
   color macizo con el glifo en oscuro, y la frase sube a tamaño de titular.
   El color NO va solo: hay icono, hay palabra y hay borde. */
.cv-panel .cv-verdict {
  border-color: var(--tono-borde);
  background: var(--tono-fondo);
  padding: var(--sp-5) var(--sp-5);
  align-items: flex-start;
}
.cv-panel .cv-verdict--si { --tono: var(--cv-aceptado); --tono-fondo: var(--cv-aceptado-tint); --tono-borde: var(--cv-aceptado); }
.cv-panel .cv-verdict--no { --tono: var(--cv-rechazado); --tono-fondo: var(--cv-rechazado-tint); --tono-borde: var(--cv-rechazado); }

.cv-panel .cv-verdict-ico {
  background: var(--tono);
  color: var(--cv-bg);
}
.cv-panel .cv-verdict-text {
  font-family: var(--cv-font-head);
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--tono);
}

/* El resumen de la prueba, pegado al veredicto en vez de suelto debajo. */
.cv-panel .cv-verdict + .ls-resumen {
  margin-top: 0;
  border: 1px solid var(--cv-border);
  border-top: 0;
  border-radius: 0 0 var(--cv-radius) var(--cv-radius);
  padding: var(--sp-4) var(--sp-5);
}
.cv-panel .cv-verdict:has(+ .ls-resumen) { border-radius: var(--cv-radius) var(--cv-radius) 0 0; }

/* ============================================================================
   PANEL DE INICIO (panel.php)
   ========================================================================= */

/* Dos columnas: el trabajo a la izquierda, lo que reclama atención a la
   derecha. El rail no es "información secundaria": es lo que hay que mirar
   aunque no lo estuvieras buscando, y por eso no se entierra al final de la
   página, donde nadie baja. */
.ls-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 330px;
  gap: var(--stack-lg);
  align-items: start;
}
.ls-panel-main { display: flex; flex-direction: column; gap: var(--stack); min-width: 0; }
.ls-panel-rail { display: flex; flex-direction: column; gap: var(--stack-lg); min-width: 0; }

@media (max-width: 1200px) {
  /* Por debajo de escritorio el rail pasa arriba, no abajo: si lo que reclama
     atención queda detrás de dos tablas largas, deja de reclamar nada. */
  .ls-panel { grid-template-columns: minmax(0, 1fr); }
  .ls-panel-rail { order: -1; }
}

/* ---------- Cifras ---------- */
.ls-kpis {
  display: grid;
  grid-template-columns: 1.4fr repeat(3, 1fr);
  gap: var(--sp-3);
}
@media (max-width: 900px) { .ls-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 480px) { .ls-kpis { grid-template-columns: minmax(0, 1fr); } }

.ls-kpi {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-4) var(--sp-4) var(--sp-4);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
  box-shadow: var(--cv-shadow);
  min-width: 0;
}
/* La cifra que contesta la pregunta de la pantalla se tiñe del acento. Es la
   ÚNICA de las cuatro que lo hace: si se destacan todas, no se destaca ninguna. */
.ls-kpi--destacado {
  border-color: var(--cv-primary);
  background: var(--cv-primary-soft);
}
.ls-kpi-k {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
}
.ls-kpi--destacado .ls-kpi-k { color: var(--cv-primary); }
.ls-kpi-v {
  font-family: var(--cv-font-head);
  font-size: var(--fs-3xl);
  font-weight: var(--fw-bold);
  line-height: 1.1;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.ls-kpi--destacado .ls-kpi-v { font-size: var(--fs-4xl); color: var(--cv-primary); }
.ls-kpi-n { font-size: var(--fs-xs); color: var(--cv-text-muted); }

/* Un cero no es un dato: es la ausencia de dato. Atenuado, deja de competir
   con las cifras que sí dicen algo. */
.ls-kpi-v.is-cero  { color: var(--cv-outline); }
.ls-kpi-v.is-aviso { color: var(--cv-caducado); }

/* ---------- Conversión por colaborador ---------- */
.ls-tasa { font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); }
.ls-tasa.is-aviso { color: var(--cv-caducado); }
.ls-tasa.is-bien  { color: var(--cv-aceptado); }

/* ---------- Rail: lo que reclama atención ---------- */
.ls-rail-bloque { display: flex; flex-direction: column; gap: var(--sp-3); }
.ls-rail-titulo {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  font-family: var(--cv-font-head);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
}

.ls-atencion {
  display: grid;
  grid-template-columns: 8px minmax(0, 1fr);
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid var(--cv-border);
  border-left: 3px solid var(--tono);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
}
.ls-atencion--grave { --tono: var(--cv-rechazado); }
.ls-atencion--aviso { --tono: var(--cv-caducado); }
.ls-atencion--ok    { --tono: var(--cv-aceptado); }

/* El punto acompaña al color: quien no distingue rojo de ámbar sigue viendo
   que hay tres avisos y dónde empieza cada uno. */
.ls-atencion-punto {
  width: 8px; height: 8px;
  border-radius: var(--cv-radius-pill);
  background: var(--tono);
  margin-top: 6px;
}
.ls-atencion-t { margin: 0 0 2px; font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
.ls-atencion-d {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--cv-text-muted);
  max-width: var(--measure-short);
}
.ls-atencion .cv-link { font-size: var(--fs-xs); }

/* ---------- Barra de 14 días ----------
   Las alturas van por clase y no por atributo `style`: la CSP es
   `style-src 'self'` y un estilo en línea se descarta sin avisar, dejando las
   catorce barras exactamente iguales. Once escalones bastan para una barra de
   62 px: la diferencia entre el 71 % y el 73 % no la ve nadie. */
.ls-spark {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 62px;
  padding: var(--sp-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
}
.ls-spark-b {
  flex-grow: 1;
  min-width: 0;
  border-radius: 2px 2px 0 0;
  background: var(--cv-border);
}
/* Los tres últimos días en acento: el ojo va a "cómo va la semana", no al
   histórico completo. */
.ls-spark-b.is-reciente { background: var(--cv-primary); }

.ls-spark-b--0  { height: 2px; }
.ls-spark-b--1  { height: 10%; }
.ls-spark-b--2  { height: 20%; }
.ls-spark-b--3  { height: 30%; }
.ls-spark-b--4  { height: 40%; }
.ls-spark-b--5  { height: 50%; }
.ls-spark-b--6  { height: 60%; }
.ls-spark-b--7  { height: 70%; }
.ls-spark-b--8  { height: 80%; }
.ls-spark-b--9  { height: 90%; }
.ls-spark-b--10 { height: 100%; }

.ls-spark-pie {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--cv-text-muted);
  font-variant-numeric: tabular-nums;
}

/* ---------- Atajos de filtro (Formularios) ----------
   Un filtro que hay que desplegar y rellenar no se usa. Estos son los cinco
   cortes que de verdad se piden, en un clic y con su recuento delante. */
.ls-atajos {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--stack);
}
.ls-atajo {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 34px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-pill);
  background: var(--cv-surface);
  color: var(--cv-text-muted);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  text-decoration: none;
  transition: border-color .12s ease, color .12s ease;
}
.ls-atajo:hover { color: var(--cv-text); border-color: var(--cv-outline); }
.ls-atajo-n {
  font-family: var(--cv-font-mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--cv-outline);
}
.ls-atajo.is-activo {
  border-color: var(--cv-text);
  background: var(--cv-surface-alt);
  color: var(--cv-text);
  font-weight: var(--fw-semibold);
}
.ls-atajo.is-activo .ls-atajo-n { color: var(--cv-text-muted); }

/* «Sin abrir» es el único atajo que delata algo, y por eso es el único teñido.
   Si se destacaran todos volveríamos a tener cinco pastillas iguales. */
.ls-atajo--delator {
  border-color: var(--cv-caducado);
  color: var(--cv-caducado);
}
.ls-atajo--delator .ls-atajo-n { color: var(--cv-caducado); }
.ls-atajo--delator.is-activo {
  background: var(--cv-caducado-tint);
  border-color: var(--cv-caducado);
  color: var(--cv-caducado-ink);
}
.ls-atajo--delator.is-activo .ls-atajo-n { color: var(--cv-caducado-ink); }

/* ---------- Origen declarado y filas que piden un vistazo ---------- */
.ls-origen { font-size: var(--fs-sm); color: var(--cv-text-muted); }
.ls-origen--ojo { color: var(--cv-caducado); font-weight: var(--fw-medium); }

/* La fila no se pinta de rojo: no es un error, es algo que merece mirarse. Una
   barra al borde y un fondo apenas teñido bastan para que el ojo caiga ahí sin
   que la tabla se convierta en un semáforo. */
.ls-fila-ojo > td:first-child { box-shadow: inset 2px 0 0 var(--cv-caducado); }
.ls-fila-ojo > td { background: var(--cv-caducado-tint); }
.cv-table tbody tr.ls-fila-ojo:hover > td { background: var(--cv-row-hover); }

/* ---------- Línea de tiempo del expediente ----------
   Tres columnas fijas: cuándo · marca · qué. La hora en columna propia y con
   cifras tabulares para que las horas se alineen verticalmente; en cuanto se
   desalinean, deja de leerse como una secuencia y pasa a ser una lista. */
.ls-linea {
  list-style: none;
  margin: 0 0 var(--stack);
  padding: 0;
  display: flex;
  flex-direction: column;
}
.ls-hito {
  display: grid;
  grid-template-columns: 132px 16px minmax(0, 1fr);
  gap: 0 var(--sp-3);
  align-items: start;
}
.ls-hito-ts {
  font-family: var(--cv-font-mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--cv-text-muted);
  padding-top: 3px;
  white-space: nowrap;
}
/* El punto y el hilo que lo une con el siguiente. El hilo se dibuja como borde
   izquierdo del propio contenido, así que crece con el texto y nunca queda
   corto ni sobresale. */
.ls-hito-marca {
  position: relative;
  width: 9px; height: 9px;
  margin: 5px 0 0 3px;
  border-radius: var(--cv-radius-pill);
  background: var(--cv-outline);
}
.ls-hito-txt {
  display: block;
  padding: 0 0 var(--sp-4) var(--sp-1);
  margin-left: -9px;
  border-left: 1px solid var(--cv-border-soft);
  padding-left: var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--cv-text);
}
.ls-hito.is-ultimo .ls-hito-txt { border-left-color: transparent; padding-bottom: 0; }

.ls-hito-quien {
  display: block;
  font-size: var(--fs-xs);
  color: var(--cv-text-muted);
}

/* UN solo hito destacado: la petición expresa. Es el hecho que se está
   probando; si se destacan cinco, no se destaca ninguno. */
.ls-hito--clave .ls-hito-marca { background: var(--cv-primary); box-shadow: 0 0 0 4px var(--cv-primary-soft); }
.ls-hito--clave .ls-hito-txt   { font-weight: var(--fw-semibold); }
.ls-hito--bien  .ls-hito-marca { background: var(--cv-aceptado); }
.ls-hito--aviso .ls-hito-marca { background: var(--cv-caducado); }
.ls-hito--grave .ls-hito-marca { background: var(--cv-rechazado); }
.ls-hito--grave .ls-hito-txt   { color: var(--cv-rechazado); font-weight: var(--fw-semibold); }

@media (max-width: 640px) {
  /* En móvil la hora se va encima: 132px de columna fija dejan el texto en un
     canal de dos palabras. */
  .ls-hito { grid-template-columns: 16px minmax(0, 1fr); }
  .ls-hito-ts { grid-column: 1 / -1; padding: 0 0 2px var(--sp-1); }
}

/* ---------- Detalle de campaña: trabajo a la izquierda, ficha a la derecha ----------
   La ficha son ocho datos que se consultan mucho y se cambian poco. Tenerlos
   dentro del formulario de edición obligaba a desplegar veinte campos solo
   para recordar a cuántos meses se conservan los datos: consultar y editar no
   son el mismo gesto y no deberían costar lo mismo. */
.ls-camp {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--stack-lg);
  align-items: start;
}
.ls-camp-main { display: flex; flex-direction: column; gap: var(--stack); min-width: 0; }
.ls-camp-rail { display: flex; flex-direction: column; gap: var(--stack-lg); min-width: 0; }

@media (max-width: 1200px) {
  .ls-camp { grid-template-columns: minmax(0, 1fr); }
  /* Aquí el rail va DEBAJO, al revés que en el panel: en una campaña el trabajo
     es la revisión jurídica, y la ficha solo se consulta. */
}

.ls-ficha {
  margin: 0;
  display: grid;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
}
.ls-ficha dt {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
}
.ls-ficha dd {
  margin: 2px 0 0;
  font-size: var(--fs-sm);
  color: var(--cv-text);
}
.ls-ficha dd + dt { padding-top: var(--sp-3); border-top: 1px solid var(--cv-border-soft); }
.ls-ficha-nota {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--cv-text-muted);
}

/* Un ajuste apagado que resta garantías no se dice en gris como si diera igual. */
.ls-si { color: var(--cv-aceptado); font-weight: var(--fw-semibold); }
.ls-no { color: var(--cv-caducado); font-weight: var(--fw-semibold); }

.ls-camp-cifras { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }

/* ============================================================================
   ESTRUCTURA: SE ACABA LA COLUMNA DE 640 px
   ============================================================================
   `.cv-card` nacía con `max-width: 640px` dentro de un contenido de 1180. En
   una pantalla de escritorio eso deja casi la mitad del ancho en negro, y la
   aplicación se lee como un formulario largo, no como un panel. El pase
   anterior lo parcheó con `--full`, pero solo para las tablas: el resultado
   fueron dos anchos distintos apilados en la misma columna, que es peor que
   uno malo.

   Aquí la tarjeta pasa a ocupar SU CELDA, y quien decide el ancho es la
   maqueta de la página. La medida de lectura no se pierde: sigue viviendo
   donde debe, en `.cv-form` (68ch) y en la prosa, que es lo que de verdad no
   puede ensancharse.

   Va acotado a `.cv-panel` por si algún día el portal usa la clase: allí una
   tarjeta a pantalla completa sí sería un error. */
.cv-panel .cv-card { max-width: none; }

/* ---------- Lista + alta: dos columnas ----------
   Casi todas las pantallas de administración son lo mismo: una tabla de lo que
   ya existe y un formulario para crear uno más. Apilados, el formulario —que
   es lo que MENOS se usa— ocupa la mitad de la página y empuja la tabla fuera
   de la vista. Al lado, cada uno tiene el ancho que le corresponde: la tabla
   todo el que necesita, el alta el de un formulario legible. */
.ls-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 400px;
  gap: var(--stack-lg);
  align-items: start;
}
.ls-cols-main { display: flex; flex-direction: column; gap: var(--stack); min-width: 0; }
.ls-cols-rail { display: flex; flex-direction: column; gap: var(--stack); min-width: 0; }

@media (max-width: 1100px) {
  .ls-cols { grid-template-columns: minmax(0, 1fr); }
}

/* El alta, al ser secundaria, se pliega. Abierta ocupa lo que necesite; cerrada
   deja la pantalla para lo que se consulta. */
.cv-card-toggle .cv-muted { margin-left: auto; font-size: var(--fs-sm); }

/* ---------- Filtros plegados ----------
   Seis campos y dos botones desplegados permanentemente encima de la tabla
   eran más superficie que los datos. Los cinco cortes que se piden de verdad
   ya están en las pastillas de arriba; esto es lo que queda para el caso raro. */
.cv-filtros[open] > .cv-card-toggle { margin-bottom: var(--sp-4); }
.cv-form-acciones {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: center;
  flex-basis: 100%;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--cv-border-soft);
}

/* ---------- Tablas: que la primera columna no se parta en cuatro líneas ----------
   «Naturnova Gestión Integral, S.L.» ocupaba cuatro renglones en una celda de
   90 px mientras la tabla se salía por la derecha. El nombre de una empresa es
   lo que identifica la fila: necesita sitio, y lo que puede apretarse son las
   fechas y los identificadores, que tienen ancho fijo y conocido. */
/* NADA DE  PARA APRETAR COLUMNAS.
   Se puso para que las fechas y los códigos no se partieran, y con once columnas
   ayudaba. Con siete sobra, y hace daño: forzar varios anchos mínimos a la vez
   deja alguna celda por debajo del ancho de su contenido y, como además llevan
   , el texto se sale y se pinta ENCIMA de la celda vecina. Eso es lo
   que se veía como datos cabalgados.
   El algoritmo de tabla reparte bien solo; basta con decirle qué no se parte. */
.cv-panel .cv-table .cv-code { white-space: nowrap; }

/* Un nombre largo se equilibra en dos renglones en vez de romperse en cuatro. */
.cv-cell-main, .cv-table td > strong { text-wrap: balance; }

/* ---------- Estado vacío de una tabla ----------
   Una tabla sin filas se pintaba como una cabecera flotando sobre nada, que se
   lee como "esto está roto" y no como "todavía no hay". */
.cv-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-5);
  text-align: center;
  color: var(--cv-text-muted);
}
.cv-empty-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border-radius: var(--cv-radius-pill);
  background: var(--cv-surface-alt);
  color: var(--cv-outline);
  margin-bottom: var(--sp-1);
}
.cv-empty-title {
  margin: 0;
  font-family: var(--cv-font-head);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--cv-text);
}
.cv-empty p { margin: 0; max-width: var(--measure-short); font-size: var(--fs-sm); }

/* ---------- Cabecera de página: menos grito, más orden ----------
   Un h1 de 34 px sobre un subtítulo que parte en dos renglones y un distintivo
   flotando en la esquina opuesta se lee como tres cosas sueltas, no como una
   cabecera. El titular baja un escalón —sigue mandando, porque lo que hay
   debajo es texto de 14—, el subtítulo se acota a una medida de lectura para
   que no se parta a capricho, y el meta se alinea con el titular en vez de con
   la línea inferior. */
.cv-panel .cv-page-head { align-items: flex-start; }
.cv-panel .cv-page-head h1 {
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  letter-spacing: -.02em;
  line-height: var(--lh-tight);
}
.cv-panel .cv-page-head .cv-muted {
  max-width: 62ch;
  font-size: var(--fs-sm);
}
.cv-panel .cv-page-actions { padding-top: 2px; }

/* Fechas e identificadores no rompen línea: tienen ancho fijo y conocido, y
   partirlos obliga a leer una fecha en dos renglones para ganar 20 px que la
   columna de al lado desperdicia. */
.cv-panel .cv-table td.cv-nowrap,
.cv-panel .cv-table th.cv-nowrap { white-space: nowrap; }

/* ---------- Una sola barra de filtros ----------
   Los atajos y el resto de filtros comparten fila. Cerrado, «Más filtros» es
   una pastilla más al final; abierto, se lleva la fila entera y el formulario
   cae debajo sin necesidad de posicionamiento absoluto ni de JavaScript. */
.ls-barra {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--stack);
}
.ls-barra .ls-atajos { flex: 1 1 auto; margin-bottom: 0; }
.cv-filtros { flex: 0 0 auto; margin-left: auto; }
/* El panel es HERMANO del desplegable, no hijo, y se muestra con el selector de
   hermano. Dentro, al abrirlo el bloque crecía y se llevaba la fila entera: el
   propio botón saltaba de sitio bajo el dedo que acababa de pulsarlo. */
.ls-barra > .cv-form--filtros { flex-basis: 100%; }
.cv-filtros:not([open]) ~ .cv-form--filtros { display: none; }

.ls-atajo--mas {
  cursor: pointer;
  list-style: none;
  gap: var(--sp-2);
}
.ls-atajo--mas::-webkit-details-marker { display: none; }
.ls-atajo--mas::marker { content: ""; }
.cv-filtros[open] > .ls-atajo--mas {
  border-color: var(--cv-text);
  color: var(--cv-text);
  margin-bottom: var(--sp-4);
}
.cv-filtros > .cv-form--filtros {
  padding: var(--sp-4);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
}

/* ---------- Densidad de tabla ----------
   Con siete columnas en vez de once, cada celda ya no pelea por el ancho y la
   fila puede volver a una sola línea de texto. */
.cv-panel .cv-table td { padding: var(--sp-3) var(--sp-4); }
.cv-panel .cv-table .cv-cell-sub {
  display: block;
  font-size: var(--fs-xs);
  color: var(--cv-text-muted);
  margin-top: 1px;
}
.cv-panel .cv-table .cv-cell-main { display: block; font-weight: var(--fw-semibold); }
/* La columna del cliente es la que se viene a buscar: se le deja crecer. */
.cv-panel .cv-table td[data-label="Cliente"] { width: auto; min-width: 12rem; }

/* ---------- Cabecera de página sin tesela ----------
   El cuadrado de color delante del titular venía del pase anterior, cuando la
   cabecera no tenía nada que la anclara. Ahora la ancla la línea inferior, y la
   tesela solo repite el icono que ya está marcado en el menú, a tres
   centímetros y en el mismo color que el botón de acción. */
.cv-panel .cv-page-icon { display: none; }
.cv-panel .cv-page-title { gap: 0; }

/* El nombre del responsable bajo la marca no es una etiqueta de producto: se
   escribe como un nombre propio, no en versales apretadas. */
.cv-panel .cv-brand-sub {
  text-transform: none;
  letter-spacing: 0;
  font-weight: var(--fw-normal);
  font-size: var(--fs-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 11rem;
}

/* ============================================================================
   ANCHO COMPLETO
   ============================================================================
   `.cv-main` estaba topado a 1180 px. En un monitor de 1900 eso deja 700 px de
   negro a la derecha mientras la tabla, siete columnas más allá, saca barra de
   desplazamiento horizontal: el espacio sobraba en un sitio y faltaba en el de
   al lado. El contenido pasa a ocupar la pantalla y son los componentes los que
   se reajustan.

   El tope de 2200 px no es un resto del anterior: es el punto a partir del cual
   una fila de tabla estirada empieza a tener más hueco que dato y el ojo pierde
   el renglón. Por debajo de eso —cualquier monitor normal— no se nota, porque
   nunca llega a aplicarse. Si en un ultrapanorámico se prefiere estirar hasta
   el borde, se quita esta línea y ya está.

   NO afecta a lo que se LEE: `.cv-main--solo` (la aceptación de condiciones)
   conserva su medida, y la prosa dentro de las tarjetas sigue acotada abajo. */
.cv-panel .cv-main {
  max-width: 2200px;
  margin-inline: auto;
  padding-inline: var(--sp-6);
}

@media (min-width: 1400px) {
  /* Con sitio de sobra, el aire lateral crece: el contenido no debe tocar el
     borde de la pantalla ni el del menú. */
  .cv-panel .cv-main { padding-inline: var(--sp-7); }
  /* Y los railes recuperan proporción: a 400 px fijos sobre 2000 de ancho se
     leían como una nota al margen, no como una columna. */
  .ls-cols  { grid-template-columns: minmax(0, 1fr) 460px; }
  .ls-panel { grid-template-columns: minmax(0, 1fr) 380px; }
  .ls-camp  { grid-template-columns: minmax(0, 1fr) 380px; }
}

/* LA PROSA NO SE ENSANCHA CON LA PÁGINA.
   Es la contrapartida obligatoria de quitar el tope: un párrafo de ayuda a 1900
   px son doscientos caracteres por línea, y a partir de unos setenta el ojo
   pierde el salto de renglón y hay que releer. Las tablas y las rejillas sí
   quieren todo el ancho; el texto seguido, no. */
.cv-panel .cv-card > p,
.cv-panel .cv-card > .cv-muted,
.cv-panel .cv-empty p,
.cv-panel .ls-cols-main > p,
.cv-panel .ls-panel-main > p { max-width: var(--measure); }

/* La versión instalada, al pie del menú. Tiene que poder leerse si se busca y
   desaparecer si no: es información de mantenimiento, no de producto. */
.cv-build {
  display: block;
  padding: 0 var(--sp-2) var(--sp-2);
  font-family: var(--cv-font-mono);
  font-size: 10px;
  color: var(--cv-sidebar-muted);
  opacity: .65;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ============================================================================
   ACCESO (login.php)
   ============================================================================
   Se entraba por una pantalla blanca y se aterrizaba en una consola oscura,
   como si fueran dos aplicaciones. Ahora la puerta lleva el mismo tema que hay
   detrás. Sigue siendo una sola caja centrada: en una pantalla de acceso, todo
   lo que no sea el formulario estorba. */
.cv-login { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: var(--sp-5); }
.cv-login-caja {
  width: 100%;
  max-width: 25rem;
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}

.cv-login-marca { display: flex; align-items: center; gap: var(--sp-3); justify-content: center; }
.cv-login-escudo {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border-radius: var(--cv-radius);
  background: var(--cv-primary);
  color: var(--cv-primary-text);
  flex-shrink: 0;
}
.cv-login-nombre {
  font-family: var(--cv-font-head);
  font-weight: var(--fw-bold);
  font-size: var(--fs-lg);
  letter-spacing: -.02em;
  line-height: 1.1;
}
.cv-login-nombre small {
  display: block;
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  letter-spacing: 0;
  color: var(--cv-text-muted);
  margin-top: 2px;
}

.cv-login-panel {
  background: var(--cv-surface);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  box-shadow: var(--cv-shadow-md);
  padding: var(--sp-6) var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
.cv-login-panel h1 { margin: 0; font-size: var(--fs-xl); letter-spacing: -.02em; }
.cv-login-panel > .cv-muted { margin: 0; font-size: var(--fs-sm); }
.cv-login-form { gap: var(--sp-4); max-width: none; }
/* El botón de entrar ocupa el ancho: es la única acción de la pantalla y en un
   móvil se pulsa con el pulgar. */
.cv-login-form .cv-btn { width: 100%; }

.cv-login-pie {
  margin: 0;
  text-align: center;
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--cv-text-muted);
}

/* ============================================================================
   MIS ENLACES — la tarjeta del enlace recién generado
   ============================================================================
   Es la pantalla más importante del colaborador y dura un solo pase: el enlace
   se enseña UNA vez porque en la base solo queda su huella. Si recarga sin
   copiarlo, ese formulario se pierde. Por eso la tarjeta se tiñe del acento, el
   aviso va arriba en lugar de en gris al pie, y copiar deja de ser «selecciona
   el texto con el dedo» para ser un botón. */
.ls-listo {
  border-color: var(--cv-primary);
  background: var(--cv-primary-soft);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
.ls-listo-cab { display: flex; align-items: flex-start; gap: var(--sp-3); }
.ls-listo-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex-shrink: 0;
  border-radius: var(--cv-radius-pill);
  background: var(--cv-primary);
  color: var(--cv-primary-text);
}
.ls-listo h2 { margin: 0; font-size: var(--fs-lg); }
.ls-listo-aviso {
  margin: 2px 0 0;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--cv-primary);
}

/* El enlace: monoespaciado, seleccionable de un golpe y partiendo por donde
   haga falta antes que desbordar en un móvil. */
.ls-listo .ls-token {
  display: block;
  font-family: var(--cv-font-mono);
  font-size: var(--fs-sm);
  line-height: 1.5;
  padding: var(--sp-3) var(--sp-4);
  border: 1px dashed var(--cv-primary);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-bg);
  color: var(--cv-text);
  overflow-wrap: anywhere;
  user-select: all;
}

.ls-listo-acciones { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
/* En móvil los dos botones a ancho completo: es donde se usa esta pantalla. */
@media (max-width: 640px) {
  .ls-listo-acciones { flex-direction: column; }
  .ls-listo-acciones > * { width: 100%; }
}

/* Confirmación del copiado. El botón dice lo que ha pasado en su propio texto,
   que es donde está mirando el dedo; un mensaje en otra esquina no se ve. */
.cv-btn.is-copiado {
  background: var(--cv-aceptado-tint);
  border-color: var(--cv-aceptado);
  color: var(--cv-aceptado-ink);
}
.cv-btn.is-fallo {
  background: var(--cv-rechazado-tint);
  border-color: var(--cv-rechazado);
  color: var(--cv-rechazado-ink);
}

/* ---------- Buscador de la cabecera de sección (Mis enlaces) ----------
   No hay más filtros que este, así que no se pliega: se abre la pantalla con
   una venta en la mano y lo primero que se hace es buscar su referencia. */
.ls-buscar { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.ls-buscar input {
  min-width: 14rem;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-input-bg, var(--cv-surface));
  color: var(--cv-text);
  font: inherit;
  /* >=16px: por debajo, iOS hace zoom al enfocar y descoloca la pantalla. */
  font-size: max(16px, var(--fs-sm));
  min-height: 38px;
}
.ls-buscar input:focus { border-color: var(--cv-primary); }
@media (max-width: 640px) {
  /* En el móvil el buscador se lleva su propia línea entera: compartirla con el
     título deja un campo de dos centímetros. */
  .ls-buscar { width: 100%; }
  .ls-buscar input { flex: 1 1 auto; min-width: 0; }
}

/* ============================================================================
   MIS ENLACES — dos columnas
   ============================================================================
   A la izquierda lo que se CONSULTA, a la derecha lo que se HACE. Apilados, el
   alta —diez campos con sus explicaciones— empujaba el listado fuera de la
   pantalla, y el listado es la mitad del trabajo: se entra tanto a generar un
   enlace como a buscar el de una venta que se va a registrar. */
.ls-enlaces {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 420px;
  gap: var(--stack-lg);
  align-items: start;
}
.ls-enlaces-main { display: flex; flex-direction: column; gap: var(--stack); min-width: 0; }
.ls-enlaces-rail { display: flex; flex-direction: column; gap: var(--stack); min-width: 0; }
.ls-enlaces-rail .cv-card { position: sticky; top: var(--sp-5); }

@media (max-width: 1100px) {
  .ls-enlaces { grid-template-columns: minmax(0, 1fr); }
  /* Y AQUÍ EL ALTA VA PRIMERO, al revés que en el panel del responsable: en la
     calle, con el cliente delante, esta pantalla se abre a generar un enlace,
     no a repasar el histórico. */
  .ls-enlaces-rail { order: -1; }
  .ls-enlaces-rail .cv-card { position: static; }
}

/* Cuatro cifras iguales: aquí ninguna manda sobre las otras, al contrario que
   en el panel del responsable, donde «solicitudes vivas» es la pregunta. */
.ls-kpis--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 900px) { .ls-kpis--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ---------- Opciones de origen ----------
   Es una DECLARACIÓN que queda registrada con nombre y fecha, así que merece el
   gesto de elegir. Toda la caja es pulsable —en un móvil, un punto de 12 px no
   es una diana— y la elegida se marca por borde y fondo, no solo por el punto. */
.ls-opciones { display: flex; flex-direction: column; gap: var(--sp-2); }
.ls-opcion {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 2px var(--sp-3);
  align-items: center;
  padding: var(--sp-3);
  min-height: 44px;
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-sm);
  background: var(--cv-surface);
  cursor: pointer;
  transition: border-color .12s ease, background-color .12s ease;
}
.ls-opcion:hover { border-color: var(--cv-outline); }
.ls-opcion input { margin: 0; grid-row: span 2; align-self: start; margin-top: 2px; }
.ls-opcion-txt { font-size: var(--fs-sm); line-height: 1.4; }
.ls-opcion-nota {
  grid-column: 2;
  font-size: var(--fs-xs);
  color: var(--cv-caducado);
}
/* :has() lo soportan todos los navegadores desde 2023; si algún día uno no lo
   entiende, la opción sigue funcionando y solo pierde el resalte. */
.ls-opcion:has(input:checked) {
  border-color: var(--cv-primary);
  background: var(--cv-primary-soft);
}
.ls-opcion:has(input:focus-visible) { outline: 2px solid var(--cv-focus); outline-offset: 2px; }

/* ---------- Ajustes pedidos sobre «Mis enlaces» ---------- */

/* El título del grupo de origen es una PREGUNTA al colaborador, no un rótulo de
   campo: pesa lo mismo que el resto del formulario y se saltaba. Sube a tamaño
   de subtítulo y se separa de las opciones. */
/* SOLO la pregunta de origen de «Mis enlaces». Esta regla llegó a alcanzar a
   TODOS los <legend> de la aplicación, y en un formulario largo como el de una
   campaña eso deja titulares de 18 px conviviendo con etiquetas de 14: es
   justo lo que se lee como «tamaños dispares». */
.ls-enlaces-rail fieldset > legend {
  font-family: var(--cv-font-head);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: -.01em;
  line-height: var(--lh-tight);
  color: var(--cv-text);
  padding: 0;
  margin-bottom: var(--sp-3);
}

/* El radio, centrado con su texto.
   Con la rejilla anterior el punto se anclaba arriba y, en las opciones de dos
   renglones, quedaba a la altura de la primera línea mientras el texto seguía
   bajando: se leía descolgado. En flex, el punto y el bloque de texto comparten
   eje y da igual cuántas líneas ocupe. */
.ls-opcion {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.ls-opcion input { margin: 0; flex: 0 0 auto; align-self: center; }
.ls-opcion > span { display: block; }
.ls-opcion-txt { flex: 1 1 auto; }
.ls-opcion-nota { display: block; margin-top: 2px; }
/* El texto y su nota van juntos en una columna, para que el punto se centre
   respecto al conjunto y no respecto a la primera línea. */
.ls-opcion-cuerpo {
  display: flex;
  flex-direction: column;
  gap: 0;
  flex: 1 1 auto;
  min-width: 0;
}

/* Buscador: el campo baja de tamaño y «Buscar» recupera forma de botón. Antes
   se leía como un enlace suelto al lado del campo —el borde del botón base
   estaba transparente, ver el arreglo de .cv-btn más arriba. */
.ls-buscar input { font-size: var(--fs-sm); min-height: 34px; min-width: 12rem; }
.ls-buscar .cv-btn { min-height: 34px; padding: 0 var(--sp-4); font-size: var(--fs-sm); }
@media (max-width: 900px) {
  /* En táctil el mínimo de 44 px manda sobre la densidad. */
  .ls-buscar input, .ls-buscar .cv-btn { min-height: 44px; }
}

/* ---------- Los radios y las casillas NO son campos de texto ----------
   `.cv-field input` da a TODO input de un campo el tratamiento de caja de
   texto: 48 px de alto, ancho completo, relleno y borde. Había excepción para
   `.cv-check input[type="checkbox"]`, pero no para los radios, así que
   cualquier radio dentro de un fieldset con clase `cv-field` se pintaba como
   un botón enorme y empujaba su etiqueta a una columna de una palabra por
   línea. Le pasaba también al selector de origen de la pantalla de campañas;
   no se había visto porque allí se abre poco.

   Se arregla en el sitio donde está el problema —la regla genérica— y no
   parcheando cada pantalla. */
.cv-field input[type="radio"],
.cv-field input[type="checkbox"],
.cv-form input[type="radio"],
.cv-form input[type="checkbox"] {
  width: auto;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  flex: 0 0 auto;
  /* El control nativo se pinta con el color del sistema, que sobre un panel
     oscuro llega en azul de Windows. Con esto adopta el acento y deja de ser
     el único elemento de la pantalla que no es de la aplicación. */
  accent-color: var(--cv-primary);
}

/* Tamaño de diana cómodo sin agrandar el control: la caja entera de
   `.ls-opcion` ya es pulsable, así que el punto solo tiene que verse. */
.ls-opcion input[type="radio"] { width: 18px; height: 18px; }

/* ---------- Notas al pie de una tarjeta ----------
   El párrafo suelto al final de la tabla se cortaba a media anchura y dejaba
   un hueco enorme a la derecha: la medida de lectura es correcta, pero sin
   nada que la enmarque se lee como texto truncado, no como una nota.

   La nota usa el ancho de la tarjeta: es un párrafo corto y al pie, y
   cortarlo a media anchura dejaba un hueco que se leía como un fallo de
   montaje. La medida de lectura se respeta donde de verdad importa —los textos
   largos y los formularios—, no en una nota de tres líneas. */
.ls-notas {
  display: grid;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--cv-border-soft);
}

.ls-notas p {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--cv-text-muted);
}
.ls-notas strong { color: var(--cv-text); font-weight: var(--fw-semibold); }

/* ============================================================================
   PORTAL DEL CONSUMIDOR — el mismo sistema, en claro
   ============================================================================
   El portal se queda CLARO a propósito: quien abre ese enlace no conoce a la
   empresa, va a entregar su teléfono desde el móvil y decide en tres segundos
   si esto le parece serio. Un formulario de consentimiento en negro no
   transmite eso.

   Lo que sí se unifica es todo lo demás — tipografías, escala, radios, anatomía
   de campos y botones—, para que las dos superficies se reconozcan como la
   misma aplicación sin compartir el fondo.

   NADA DE LO QUE HAY AQUÍ TOCA EL CONTENIDO: ni el texto legal, ni el literal
   del botón, ni la casilla, que sigue sirviéndose vacía. Solo presentación. */

body.cv-portal {
  /* Las mismas familias que el panel. Era lo que más separaba las dos
     superficies: se leían como dos productos de dos épocas. */
  --cv-font:      "Public Sans", "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --cv-font-head: "Public Sans", "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --cv-font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;

  /* El ámbar del panel entra SOLO como acento de foco y detalle. La acción
     principal sigue en slate: en una pantalla donde lo único que se puede
     pulsar es «Quiero que me llamen», el botón no necesita competir con nada,
     y el slate se lee más institucional que el ámbar. */
  --cv-focus: #b06f12;
}

/* ---------- Cabecera de marca ---------- */
.cv-portal-header {
  border-bottom: 1px solid var(--cv-border-soft);
  background: var(--cv-surface);
}
.cv-portal-brand {
  font-family: var(--cv-font-head);
  font-weight: var(--fw-bold);
  letter-spacing: -.01em;
}

/* ---------- La tarjeta ---------- */
.cv-portal-card h1 {
  font-family: var(--cv-font-head);
  font-weight: var(--fw-bold);
  letter-spacing: -.02em;
  text-wrap: balance;
}

/* ---------- Campos: la misma anatomía que el panel, con diana táctil ----------
   El portal se rellena con el pulgar, así que aquí la altura NO baja de 52 px
   ni el cuerpo de 16, que es lo que evita el zoom automático de iOS. */
.cv-portal .cv-field > span,
.cv-portal .cv-field > label { font-weight: var(--fw-semibold); font-size: var(--fs-sm); }
.cv-portal .cv-field input,
.cv-portal .cv-field select {
  min-height: 52px;
  border-radius: var(--cv-radius-sm);
  border-color: var(--cv-outline);
}
.cv-portal .cv-field input:focus,
.cv-portal .cv-field select:focus {
  border-color: var(--cv-primary);
  outline: 2px solid var(--cv-focus);
  outline-offset: 1px;
}

/* ---------- Identificadores, en monoespaciada ----------
   El código de la solicitud es lo que el interesado va a dictar por teléfono si
   alguna vez reclama. Con la mono se distinguen el 0 de la O y el 1 de la l. */
.cv-portal .ls-resumen dd strong,
.cv-portal .cv-code-value {
  font-family: var(--cv-font-mono);
  font-size: var(--fs-lg);
  letter-spacing: .02em;
}

/* ---------- Resumen del justificante ----------
   Era una lista de definición corrida. Cada dato en su fila, con la etiqueta
   pequeña encima: así se lee de arriba abajo en un móvil sin perder el par. */
.cv-portal .ls-resumen {
  margin: 0;
  display: grid;
  gap: 0;
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  overflow: hidden;
}
.cv-portal .ls-resumen dt {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
  padding: var(--sp-3) var(--sp-4) 0;
}
.cv-portal .ls-resumen dd {
  margin: 0;
  padding: 2px var(--sp-4) var(--sp-3);
  font-size: var(--fs-base);
  border-bottom: 1px solid var(--cv-border-soft);
}
.cv-portal .ls-resumen dd:last-child { border-bottom: 0; }

/* ---------- Confirmación del portal ----------
   Quien acaba de dar su teléfono a una empresa que no conoce necesita ver dos
   cosas: que se ha registrado, y con qué referencia puede reclamar. La marca de
   comprobado responde a la primera de un vistazo; el código en monoespaciada,
   a la segunda.

   Verde y no ámbar: aquí no se pide una acción, se confirma un hecho. */
.ls-ok {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
}
.ls-ok-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px; height: 52px;
  flex-shrink: 0;
  border-radius: var(--cv-radius-pill);
  background: var(--cv-aceptado-bg);
  color: var(--cv-aceptado-text);
}
.ls-ok h1 { margin: 0 0 var(--sp-2); }
.ls-ok .cv-lead { margin: 0; }

@media (max-width: 480px) {
  /* En el móvil, en columna: el icono al lado deja el titular en un canal de
     tres palabras. */
  .ls-ok { flex-direction: column; gap: var(--sp-3); }
}

/* ============================================================================
   PORTAL — ESCALA, FONDO Y ESTRUCTURA
   ============================================================================
   Diagnóstico, medido y no a ojo:

   · TODO estaba a 18 px. El titular a 28 y el cuerpo a 18 da una proporción de
     1,55; en una página con jerarquía esa proporción es 2 o más. Cuando todo
     mide casi lo mismo no hay diseño, hay letra grande.
   · La tarjeta era BLANCA sobre un fondo CASI BLANCO. Una tarjeta que no se
     despega de su fondo no se lee como tarjeta: se lee como página plana, y es
     lo que más delata a una pantalla sin trabajar.
   · La cabecera era una raya de 1 px con un icono. La marca no tenía escenario.
   · No había pie: la página terminaba de golpe.

   El fondo del formulario sigue claro —quien entrega su teléfono a una empresa
   que no conoce necesita leer, no admirar— pero el marco que lo rodea pasa a
   tener peso. */

/* ---------- Escala ----------
   El cuerpo baja a 16 px, que es el tamaño de lectura estándar y el mínimo que
   evita el zoom de iOS. Lo que sube es el TITULAR, hasta más del doble del
   cuerpo: la jerarquía se hace con la diferencia, no agrandándolo todo. */
body.cv-portal { font-size: var(--fs-base); line-height: 1.6; }
.cv-portal .cv-portal-card h1 { font-size: var(--fs-3xl); line-height: 1.12; }
.cv-portal .cv-lead { font-size: var(--fs-lg); color: var(--cv-text-muted); line-height: 1.55; }
.cv-portal .cv-help { font-size: var(--fs-sm); }

/* ---------- El fondo se aparta de la tarjeta ----------
   Un tono con sesgo hacia el slate de la marca, no un gris neutro: se nota que
   está elegido. Y la tarjeta gana una sombra de verdad para levantarse. */
body.cv-portal { background: #e9edf2; }
.cv-portal .cv-portal-card {
  box-shadow: 0 1px 2px rgba(29, 43, 62, .06), 0 12px 32px -12px rgba(29, 43, 62, .18);
  border-color: #d5dbe3;
  padding: var(--sp-6) var(--sp-6) var(--sp-5);
}

/* ---------- La marca, sobre banda oscura ----------
   Es el término medio: el parentesco con el panel se ve al instante en la
   cabecera, y el contenido —que es lo que hay que leer y rellenar— se queda en
   claro. Da además a la empresa un escenario donde antes tenía una raya. */
.cv-portal .cv-portal-header {
  width: 100%;
  background: var(--cv-sidebar-bg);
  border-bottom: 0;
  padding: var(--sp-4) var(--sp-5);
  display: flex;
  justify-content: center;
}
.cv-portal .cv-portal-brand {
  width: 100%;
  max-width: 640px;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  color: #fff;
  font-size: var(--fs-base);
}
.cv-portal .cv-portal-brand .cv-ico { color: #fff; }
.cv-portal .cv-portal-logo { max-height: 30px; width: auto; }

/* ---------- Pie de página ----------
   La página terminaba de golpe bajo la tarjeta. El pie cierra la composición y
   repite lo único que el interesado puede necesitar después: quién trata sus
   datos y dónde ejercer sus derechos. Sale de la ficha del responsable, que es
   la fuente única; no se escribe a mano en ningún sitio. */
.cv-portal-pie {
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  padding: var(--sp-5) var(--sp-4) var(--sp-7);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  line-height: 1.55;
  color: var(--cv-text-muted);
  text-align: center;
}
.cv-portal-pie strong { color: var(--cv-text); font-weight: var(--fw-semibold); }
.cv-portal-pie a { color: var(--cv-primary); }

/* Empuja el pie al fondo cuando la página es corta, para que no quede colgando
   a media pantalla. */
.cv-portal { justify-content: flex-start; }
.cv-portal .cv-portal-card { margin-bottom: 0; }

/* ---------- El panel de filtros, en rejilla ----------
   Eran ocho controles en flujo libre con anchos distintos y una nota al pie
   debajo de dos de ellos: cada fila rompía por un sitio distinto. En rejilla de
   columnas iguales todo se alinea solo, y las notas sobran — un filtro llamado
   «Apertura» con las opciones «Abiertos» y «Sin abrir» no necesita que le
   expliquen para qué sirve debajo. */
.ls-barra > .cv-form--filtros {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--sp-3) var(--sp-4);
  align-items: end;
  padding: var(--sp-5);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-surface);
  max-width: none;
}
.ls-barra > .cv-form--filtros .cv-field { margin: 0; gap: var(--sp-1); min-width: 0; }
.ls-barra > .cv-form--filtros .cv-field > span {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--cv-text-muted);
}
.ls-barra > .cv-form--filtros .cv-field input,
.ls-barra > .cv-form--filtros .cv-field select { min-height: 40px; font-size: var(--fs-sm); }
/* Las acciones a su propia fila, alineadas a la izquierda con los campos. */
.ls-barra > .cv-form--filtros .cv-form-acciones {
  grid-column: 1 / -1;
  border-top: 1px solid var(--cv-border-soft);
  margin-top: var(--sp-2);
}
@media (max-width: 900px) {
  .ls-barra > .cv-form--filtros .cv-field input,
  .ls-barra > .cv-form--filtros .cv-field select { min-height: 44px; }
}

/* ---------- Fechas en tabla ----------
   Llevaban la clase del chip de identificador, y eso hacía dos daños. El
   visible: una fecha no es un código, no necesita caja ni fondo, y dos cajas
   seguidas parecían dos columnas donde hay una.

   El invisible y peor: `.cv-code` es `display: inline-block`, y puesto sobre un
   `<td>` SACA LA CELDA DE LA TABLA. El navegador reconstruye las que faltan por
   su cuenta, las columnas se descolocan y la última —Estado— aparece vacía. De
   ahí la impresión de que esa columna no se usaba: sí se usaba, pero su
   contenido se estaba pintando bajo la cabecera de al lado.

   Aquí solo tipografía: mono para que las cifras se alineen entre filas, sin
   caja. */
.cv-panel .cv-table td.cv-fecha {
  font-family: var(--cv-font-mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--cv-text-muted);
}
.cv-panel .cv-table td.cv-fecha .cv-muted { font-family: var(--cv-font); }

/* Y el chip de identificador NUNCA sobre una celda: solo sobre el elemento de
   dentro. Si vuelve a colarse, que al menos no rompa la tabla. */
.cv-table td.cv-code,
.cv-table th.cv-code { display: table-cell; background: none; border: 0; padding: var(--sp-3) var(--sp-4); }

/* ---------- Una sola escala dentro de un formulario ----------
   Un formulario con cuatro tamaños de texto no se lee como un formulario, se
   lee como cuatro trozos pegados. Aquí hay exactamente tres niveles y ninguno
   más: rótulo de campo, valor, y ayuda.

   El `legend` de un grupo pesa lo mismo que el rótulo de un campo, porque es lo
   mismo: el nombre de lo que hay debajo. */
.cv-panel .cv-form fieldset > legend,
.cv-panel .cv-form .cv-field > span {
  font-family: var(--cv-font);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: 0;
  line-height: 1.4;
  color: var(--cv-text);
  padding: 0;
  margin: 0 0 var(--sp-2);
}
.cv-panel .cv-form .cv-field > span em {
  font-style: normal;
  font-weight: var(--fw-normal);
  color: var(--cv-text-muted);
}
/* La ayuda: un único tamaño, siempre por debajo de su campo y nunca en negrita
   por dentro salvo para el aviso que de verdad cambia la decisión. */
.cv-panel .cv-form small.cv-muted {
  display: block;
  font-size: var(--fs-xs);
  line-height: 1.55;
  color: var(--cv-text-muted);
  max-width: var(--measure);
}
.cv-panel .cv-form small.cv-muted strong { color: var(--cv-text); font-weight: var(--fw-semibold); }

/* ---------- Formulario a dos columnas ----------
   El alta de una campaña son doce controles. En una columna es una tira de dos
   pantallas de alto en la que no se ve dónde acaba una decisión y empieza otra.
   En dos, los pares que se deciden juntos —finalidad y base jurídica,
   destinatarios y su detalle— quedan a la vista a la vez.

   Lo que NO se parte en dos: los campos de texto largo, los grupos de casillas
   y las ayudas extensas. Media columna para una frase de sesenta caracteres
   sería peor que una columna entera. */
.cv-form--2col {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5);
  align-items: start;
  max-width: none;
}
.cv-form--2col > fieldset,
.cv-form--2col > .cv-check,
.cv-form--2col > small,
.cv-form--2col > p,
.cv-form--2col > button,
.cv-form--2col > .cv-actions,
.cv-form--2col > .cv-field--full { grid-column: 1 / -1; }

@media (max-width: 900px) {
  .cv-form--2col { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- Casillas del panel: más pequeñas ----------
   `.cv-check` se dimensionó para el PORTAL, donde una sola casilla decide un
   consentimiento y se marca con el pulgar. En el panel hay listas de cinco o
   seis —qué datos pedir, qué permisos ofrecer— y con ese tamaño una lista
   ocupa media pantalla y pesa más que los campos de al lado.
   La diana sigue siendo la caja entera, así que se puede encoger sin perderla. */
.cv-panel .cv-check {
  min-height: 40px;
  padding: var(--sp-2) var(--sp-3);
  gap: var(--sp-3);
  font-size: var(--fs-sm);
  align-items: center;
}
.cv-panel .cv-check input[type="checkbox"] { width: 17px; height: 17px; }
.cv-panel fieldset > .cv-check + .cv-check { margin-top: var(--sp-1); }

/* Los campos del panel bajan de 48 a 40: con el rótulo a 14 y la ayuda a 12,
   un campo de 48 con texto de 16 era el único elemento fuera de escala. */
.cv-panel .cv-form .cv-field input,
.cv-panel .cv-form .cv-field select,
.cv-panel .cv-form .cv-field textarea { min-height: 40px; font-size: var(--fs-sm); }
@media (max-width: 900px) {
  .cv-panel .cv-form .cv-field input,
  .cv-panel .cv-form .cv-field select,
  .cv-panel .cv-form .cv-field textarea { min-height: 44px; font-size: max(16px, var(--fs-sm)); }
}

/* ---------- El texto de una casilla pesa lo que un rótulo ----------
   `.cv-check > span` fija 16 px DIRECTAMENTE sobre el texto de la casilla. Una
   declaración sobre el propio elemento gana siempre a lo que hereda del padre,
   así que bajar el tamaño del contenedor no le llegaba: en un formulario con
   rótulos a 14 y ayudas a 12, las casillas se quedaban a 16 y eran lo más
   grande de la pantalla. Era el último tamaño fuera de la escala.

   El portal NO entra aquí: allí una casilla decide un consentimiento, se lee de
   pie y se marca con el pulgar, y 16 px es lo correcto. */
.cv-panel .cv-check > span,
.cv-panel .cv-check-text { font-size: var(--fs-sm); line-height: 1.45; }

/* Lo mismo con la ayuda que acompaña a una casilla: iba suelta detrás del
   grupo, sin relación visible con él. Se pega debajo y se sangra hasta la
   altura del texto, no del cuadrito. */
.cv-panel .cv-form .cv-check + small.cv-muted { margin-top: var(--sp-1); padding-left: calc(17px + var(--sp-3)); }

/* El ajuste de altura de los campos también alcanzó al textarea, que tenía 88 px.
   Cuarenta son dos líneas, y un textarea existe justamente porque el texto no cabe
   en una. Los que fijan filas no se veían afectados —la altura por filas manda—,
   pero los que no, se quedaron en una ranura. */
.cv-panel .cv-form .cv-field textarea { min-height: 88px; line-height: 1.55; }
