/* ==========================================================================
   LeadSync - Modales y formularios
   El armazon de las modales y sus variantes, el campo de contrasena con ojo, la
   lista de requisitos, el asistente de recuperacion y la gestion de usuarios.

   OJO CON EL ORDEN: estos ficheros se enlazan en index.html en un orden concreto y
   ese orden ES la cascada. Mover un enlace de sitio cambia que regla gana entre dos
   de la misma especificidad. Si anades uno nuevo, ponlo donde le toque, no al final.
   ========================================================================== */

/* --- Modales --- */
/* LA ENTRADA ES UNA ANIMACIÓN, NO UNA TRANSICIÓN DISPARADA DESDE JS, y la diferencia no
   es de estilo: la modal nacía con `opacity: 0` y se le añadía `.show` desde un
   requestAnimationFrame. Si el rAF no llega a correr —pestaña de fondo, ventana minimizada,
   renderer dormido— esa clase no se pone NUNCA y queda una modal invisible tapando la
   pantalla, que además se traga los clics.
   Se vio de verdad el 20-08 en una pestaña que no estaba componiendo cuadros.
   Una animación sin `fill-mode` termina siempre en el estado natural del elemento, así que
   aunque no se llegue a animar, la modal acaba visible. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .5);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 300;
  animation: modalEntra 200ms ease;
}
/* .saliendo lo pone el cierre para que la salida se vea; `forwards` aquí sí, porque el
   elemento se quita del documento al terminar. */
.modal-overlay.saliendo {
  animation: modalSale 180ms ease forwards;
  pointer-events: none;
}

@keyframes modalEntra {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes modalSale {
  from { opacity: 1; }
  to   { opacity: 0; }
}
.modal-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  width: 420px;
  max-width: 100%;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 20px 60px var(--shadow);
  /* La tarjeta sube al entrar por el mismo motivo: sin `fill-mode`, si la animación no
     llega a correr se queda en su sitio natural en vez de desplazada para siempre. */
  animation: modalCardEntra 200ms ease;
}
.modal-overlay.saliendo .modal-card {
  animation: modalCardSale 180ms ease forwards;
}

@keyframes modalCardEntra {
  from { transform: translateY(10px) scale(.98); }
  to   { transform: none; }
}

@keyframes modalCardSale {
  from { transform: none; }
  to   { transform: translateY(6px) scale(.99); }
}
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 20px 10px;
  /* OPACA Y POR ENCIMA. El cuerpo scrollea justo debajo, y una cabecera transparente deja
     ver pasar por su relleno lo que sube: una tira de otro campo asomando bajo el titulo.
     Es lo mismo que ya necesitan las barras pegajosas de dentro. */
  position: relative;
  z-index: 3;
  background: var(--bg-panel);
  border-top-left-radius: 14px;
  border-top-right-radius: 14px;
}
.modal-header h2 {
  font-size: 18px;
  color: var(--text);
}
.modal-body {
  /* El relleno de abajo es UNA VARIABLE porque hay que restarlo: un `position: sticky` con
     `bottom: 0` se pega al borde del RELLENO del scrollport, no al del contenedor, asi que
     una barra pegada abajo se quedaba estos 8px por encima y el formulario se veia pasar por
     el hueco. Es el mismo tropiezo que la cabecera pegajosa de Configuracion. */
  --modal-body-pad-t: 6px;
  --modal-body-pad-b: 8px;
  padding: var(--modal-body-pad-t) 20px var(--modal-body-pad-b);
  overflow-y: auto;
}
.modal-text {
  color: var(--text-dim);
  line-height: 1.5;
}

/* Resalta la parte del aviso que más se malinterpreta (p. ej. "el cliente aún podrá verlo"). */
.modal-text .warn-em {
  color: var(--warn);
  font-weight: 700;
}
.modal-hint {
  color: var(--text-dim);
  font-size: 12px;
  margin-top: 2px;
}
.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 14px 20px 18px;
}

/* item 13: el pie tiene DOS juegos de botones y solo se ve uno. En reposo "Cerrar"; con
   cambios sin guardar, "Descartar"/"Guardar". Los dos se pintan al abrir y aquí se elige
   cuál: conmutar no reconstruye nodos, así que nadie se queda con una referencia muerta.
   Se toca SOLO `display` (nunca se fuerza el del botón visible), y la entrada es una
   ANIMACIÓN: una transición que no llegue a correr dejaría el pie invisible. */
.modal-footer.sucio > .fb-limpio { display: none; }
.modal-footer:not(.sucio) > .fb-sucio { display: none; }
.modal-footer.sucio > .fb-sucio { animation: dirty-in .16s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .modal-footer.sucio > .fb-sucio { animation: none; }
}

/* --- Variantes del armazón de la modal (item 48) ---------------------------
   Se piden con `options` en openModal y llegan como data-options. El selector por
   ~= casa una palabra suelta dentro de la lista, así que se combinan libremente
   ("no-footer mini-header") sin multiplicar clases. */
.modal-card[data-options~="no-footer"] .modal-footer { display: none; }

/* Sin pie, el CUERPO es el último elemento de la tarjeta, y su fondo —el panel gris de
   Configuración— tapaba las esquinas redondeadas de abajo: se veía un rectángulo asomando
   por debajo del borde curvo. Hereda el radio de la tarjeta y recorta lo que lleve dentro. */
.modal-card[data-options~="no-footer"] .modal-body {
  border-bottom-left-radius: 14px;
  border-bottom-right-radius: 14px;
  overflow: hidden;
}
.modal-card[data-options~="mini-header"] .modal-header { padding: 12px 16px 10px; }
.modal-card[data-options~="mini-header"] .modal-header h2 { font-size: 15.5px; }
.modal-btn {
  padding: 9px 18px;
  border-radius: 8px;
  font-weight: 600;
  font-size: 13.5px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background-color .15s ease, border-color .15s ease, filter .15s ease;
}
.modal-btn.secondary {
  background: var(--bg-panel);
  border-color: var(--border);
  color: var(--text);
}
.modal-btn.secondary:hover { background: var(--bg-hover); }
.modal-btn.primary {
  background: var(--accent);
  color: var(--on-accent);
}
.modal-btn.primary:hover { background: var(--accent-strong); }
.modal-btn.danger {
  background: var(--danger);
  color: #fff;
}
.modal-btn.danger:hover { filter: brightness(1.08); }

/* Estado deshabilitado: se ve atenuado y no responde al hover (item 25). */
.modal-btn:disabled {
  opacity: .5;
  cursor: not-allowed;
}
.modal-btn.secondary:disabled:hover { background: transparent; }
.modal-btn.primary:disabled:hover { background: var(--accent); }
.modal-btn.danger:disabled:hover { filter: none; }

/* PROHIBIDO EL TIRADOR DE REDIMENSIONAR (mandato tuyo, y van dos veces que se me cuela):
   arrastrar la esquina de un textarea es incomodísimo y deja el formulario descuadrado. La
   altura la decide el CONTENIDO, como en el composer: crece con las líneas hasta un tope y
   a partir de ahí hace scroll (lo cablea autoAltoTextarea, en ui.js). */
textarea { resize: none; }

/* Campos de formulario dentro de modales. */
/* EL AIRE DE DENTRO TIENE QUE SER CLARAMENTE MENOR QUE EL DE FUERA, o los campos dejan de
   leerse como bloques. Estaba en 6px dentro y 12px fuera: el doble no basta —el rótulo, que es
   pequeño y gris, se pegaba al campo de ARRIBA y parecía su pie en vez de la cabecera del
   suyo—. Lo reportó él con una captura: *"no tiene espaciado arriba, está pegado"*.
   Además el 6 no estaba en la escala de 4/8 de la guía, así que ningún otro espaciado casaba
   con él. Ahora 8 dentro y 20 fuera: los dos en la escala, y con separación suficiente para
   que la proximidad agrupe. */
.modal-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 20px;
}
.modal-field label {
  font-size: 12px;
  color: var(--text-dim);
  font-weight: 600;
}
.modal-field input {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 11px 13px;
  color: var(--text);
  font-size: 14px;
  outline: none;
  transition: border-color .15s ease;
}
.modal-field input:focus { border-color: var(--accent); }

/* EL TEXTAREA VA CON SUS HERMANOS. Faltaba, y no se ve leyendo el HTML: `.modal-field`
   vestia el `input` y el `select` y dejaba el `textarea` con el aspecto de fabrica del
   navegador — fondo BLANCO en tema oscuro—. No se notaba porque el unico formulario con
   textarea en una modal era el de mensajes rapidos, que lo vestia por su cuenta con
   `.qr-edit-modal textarea`. En cuanto el de plantillas uso `.modal-field` (item A18),
   salio el campo blanco.
   El tirador, prohibido: el alto lo decide el contenido (autoAltoTextarea). */
.modal-field textarea {
  width: 100%;
  box-sizing: border-box;
  resize: none;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 11px 13px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  line-height: 1.5;
  outline: none;
  transition: border-color .15s ease;
}
.modal-field textarea:focus { border-color: var(--accent); }

/* --- Validacion en vivo de un campo (components/forms.js) --------------------
   EL CUANDO ES LA MITAD DEL ASUNTO: el check llega pronto -en cuanto el campo vale, mientras
   se escribe- y la equis llega tarde -al salir del campo-. Poner una equis a la primera tecla
   es decirle a alguien que se equivoca mientras todavia esta escribiendo. */

/* La etiqueta pasa a ser una fila: texto a la izquierda, marca a la derecha. */
.modal-field label {
  display: flex;
  align-items: center;
  gap: 6px;
}
.campo-marca {
  display: inline-flex;
  margin-left: auto;
  flex: 0 0 auto;
}
.campo-marca .ic { width: 14px; height: 14px; }

/* EL RECUENTO DE UN CAMPO CON TOPE (item B64, recoge parte de B59). Va junto al rotulo, no
   debajo del campo: ahi compite con el motivo del error, y son dos cosas distintas —uno dice
   cuanto llevas, el otro que esta mal—.

   Apagado mientras va sobrado, porque no es el dato principal; solo se enciende cuando
   importa. El rojo al pasarse usa el mismo tono que el resto de errores del formulario. */
/* VA PEGADO AL ROTULO, y este es el detalle que rompi al ponerlo (lo cazo el mirandolo):
   con `margin-left: auto` el contador se queda con el hueco elastico del rotulo, que es de
   la MARCA —la que tiene que irse al final—, y los tres elementos acababan desparramados a
   lo ancho: «Atajo … 13/40 … ✓». La forma buena ya estaba escrita en `.inter-cont`, la del
   editor de opciones: el contador no empuja nada y el hueco se lo queda la marca. */
.campo-cont {
  flex: 0 0 auto;
  font-size: 11px;
  font-variant-numeric: tabular-nums; /* los digitos no bailan al cambiar */
  color: var(--text-dim);
  font-weight: 500;
}
.campo-cont.campo-pasado {
  color: var(--danger);
  font-weight: 700;
}
/* SIN `> label` DELANTE, y no es cosmetica: el contenedor de un campo no siempre lleva un
   <label> dentro -en el editor de mensajes con opciones el contenedor ES el label y el rotulo
   es un span-, asi que con el hijo directo exigido la marca salia sin color en medio
   formulario. La regla mira la marca, que es lo unico que hace falta. */
.campo-ok .campo-marca { color: var(--ok); }
.campo-mal .campo-marca { color: var(--danger); }

/* EL BORDE SOLO EN ROJO, no en verde. Un formulario entero con bordes verdes grita tanto
   como uno con bordes rojos, y lo que hay que ver es lo que falla. El acierto se dice con la
   marca de al lado, que es discreta. */
.campo-mal input,
.campo-mal textarea,
.campo-mal select {
  border-color: var(--danger);
}

/* El motivo DICE QUE PASA Y COMO SE ARREGLA. Un borde rojo mudo obliga a adivinar. */
.campo-motivo {
  color: var(--danger);
  font-size: 12px;
  line-height: 1.45;
  /* Entra, no aparece de golpe: si no, el formulario da un salto y se pierde de vista cual
     de los campos se quejo. */
  animation: campo-motivo-entra .14s ease both;
}
@keyframes campo-motivo-entra {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .campo-motivo { animation: none; }
}

/* --- Contraseña con ojo (mostrar/ocultar) --- */
.pw-wrap {
  position: relative;
  display: flex;
}
.pw-wrap input {
  flex: 1;
  padding-right: 42px;
  width: 100%;
}
.pw-eye {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  background: transparent;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  transition: background-color .15s ease, color .15s ease;
}
.pw-eye:hover {
  color: var(--text);
  background: var(--bg-hover);
}
.pw-eye .ic {
  width: 20px;
  height: 20px;
}

/* Ojo del login: sin fondo, icono en color de texto y más grande. */
#login-pass-eye {
  color: var(--text);
  background: transparent;
  width: 36px;
  height: 36px;
}
#login-pass-eye:hover {
  background: transparent;
  opacity: .8;
}
#login-pass-eye .ic {
  width: 27px;
  height: 27px;
}

/* --- Checklist de requisitos de contraseña --- */
.pw-checks {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin: 8px 0 4px;
  transition: opacity .15s ease;
}
.pw-checks.dimmed { opacity: .45; }

/* atenuada mientras no se escribe contraseña */
.pw-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-dim);
  transition: color .15s ease;
}
.pw-check-dot {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  border: 2px solid var(--text-dim);
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: transparent;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.pw-check-dot .ic {
  width: 10px;
  height: 10px;
}
.pw-check.ok { color: var(--accent); }
.pw-check.ok .pw-check-dot {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}

/* --- Panel "Para guardar, falta corregir:" --- */
.form-errors {
  background: var(--warn-bg);
  border-left: 4px solid var(--warn);
  border-radius: 8px;
  padding: 12px 14px;
  margin-top: 6px;
}
.form-errors h4 {
  font-size: 13px;
  color: var(--warn);
  margin-bottom: 6px;
}
.form-errors ul {
  list-style: disc;
  padding-left: 20px;
}
.form-errors li {
  font-size: 13px;
  color: var(--warn);
  margin: 2px 0;
}

/* --- Wizard de recuperación de acceso (item 13) --- */
.rec-code-inputs {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin: 8px 0 4px;
}
.rec-code-box {
  width: 34px;
  height: 44px;
  text-align: center;
  font-size: 20px;
  font-weight: 600;
  text-transform: uppercase;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-elev);
  color: var(--text);
  caret-color: var(--accent);
}
.rec-code-box:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-dim);
}
.rec-code-dash {
  color: var(--text-dim);
  font-weight: 700;
  padding: 0 2px;
}
.rec-code-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 10px;
  flex-wrap: wrap;
}
.rec-countdown {
  font-size: 12.5px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.rec-countdown.expired { color: var(--danger); }
.rec-gens {
  font-size: 12px;
  color: var(--text-dim);
}
.rec-alert {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border-radius: 10px;
  font-size: 13.5px;
  line-height: 1.45;
}
.rec-alert-ic {
  display: inline-flex;
  flex: 0 0 auto;
  margin-top: 1px;
}
.rec-alert-ic .ic {
  width: 20px;
  height: 20px;
}
.rec-alert.danger {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
}
.rec-alert.info {
  background: var(--info-bg);
  color: var(--info);
}
.rec-block-countdown {
  text-align: center;
  margin-top: 14px;
  color: var(--text-dim);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

/* --- Config: secciones, usuarios, roles --- */
.settings-section {
  border-top: 1px solid var(--border);
  padding-top: 16px;
  margin-top: 16px;
}
.settings-section:first-child {
  border-top: none;
  padding-top: 0;
  margin-top: 0;
}

/* Las filas de una sección respiran entre sí (el reporte: quedaban pegadas). */
.settings-section > .switch-row { margin-bottom: 14px; }
.settings-section > .switch-row:last-child { margin-bottom: 0; }
.settings-section > .switch-row > span:first-child { line-height: 1.45; }
.settings-section h3 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .4px;
  color: var(--text-dim);
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.settings-section h3 .ic {
  width: 15px;
  height: 15px;
}
.modal-field select {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 11px 13px;
  color: var(--text);
  font-size: 14px;
  outline: none;
}
.modal-btn.sm {
  padding: 7px 12px;
  font-size: 12.5px;
}
.modal-btn.block {
  width: 100%;
  justify-content: center;
}
.switch-row {
  width: 100%;
  justify-content: space-between;
  gap: 12px;
  color: var(--text);
}
.switch-row > span:first-child { font-size: 13.5px; }
.user-row {
  display: flex;
  align-items: center;

  /* SIN space-between. Con él y los botones sueltos como hermanos, el reparto del hueco
     libre metía el lápiz en mitad de la fila, y en la fila de uno mismo —que no lleva
     papelera— se iba al extremo: los lápices no se alineaban entre filas. Ahora los
     datos crecen (flex:1) y las acciones se quedan pegadas al final. */
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 8px;
}
.user-row .user-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  flex: 1 1 auto;
}

/* La columna de acciones: ANCHO FIJO de TRES botones, siempre al final.
   El ancho fijo es lo que alinea los lápices. Con la columna ajustándose al contenido, la
   fila de uno mismo —que no lleva papelera— dejaba su lápiz 36px más a la derecha que el
   resto (medido). Reservando el hueco, el lápiz cae en la misma vertical en todas las filas
   y el sitio vacío no se nota.

   EL NÚMERO SALE DE LOS BOTONES, no es redondo: 3 × 34px + 2 huecos de 2px = 106px. Al
   añadir la corona del item A41 la columna pasó de dos botones a tres, y dejar el 70px de
   antes habría desalineado los lápices otra vez — que es justo el fallo que este bloque
   existe para evitar. Si algún día se añade un cuarto, este número sube con él. */
.user-row .user-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  width: 106px;
  justify-content: flex-start;
  margin-left: auto;
}
.user-row .user-actions .icon-btn { flex: 0 0 34px; }
.user-row .user-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* item 57: con nombre de equipo la fila se apila —nombre arriba, usuario de acceso debajo
   y atenuado—; sin nombre solo hay una línea y queda como estaba. */
.user-row .user-ids {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.user-row .user-login {
  font-size: 11px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.role-pill {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 8px;
  background: var(--bg-hover);
  color: var(--text-dim);
  flex: 0 0 auto;
}
.role-pill.admin {
  background: var(--accent-dim);
  color: var(--author-bot);
}
.icon-btn.danger:hover { color: var(--danger); }

/* Badge que marca al usuario actual ("Tú") en la lista de usuarios. */
.me-pill {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .3px;
  padding: 2px 8px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--on-accent);
  flex: 0 0 auto;
}
.user-row.me { border-color: var(--accent); }

/* LOS PERMISOS de la ventana de rol (item A41).

   La caja tiene ALTO MAXIMO y scroll propio: son siete hoy, pero la lista crece con el
   producto y una ventana que se sale de la pantalla deja el boton de Guardar fuera de
   alcance — que es la peor forma de perder un cambio ya escrito. */
.perm-caja {
  margin-top: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  max-height: 260px;
  overflow-y: auto;
}
.perm-cabecera {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 12px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .3px;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);

  /* Pegada arriba al hacer scroll: sin esto, al bajar por la lista se pierde de vista que
     estos interruptores son "permisos" y quedan siete casillas sueltas sin titulo. */
  position: sticky;
  top: 0;
  background: var(--bg-panel, var(--bg));
  z-index: 1;
}
.perm-nota {
  font-size: 11px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
}
.perm-fila {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  cursor: pointer;
  border-top: 1px solid var(--border);
}
.perm-fila:first-of-type { border-top: 0; }
.perm-fila:hover { background: var(--bg-hover); }
.perm-check { margin-top: 2px; flex: 0 0 auto; }
.perm-texto { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.perm-nombre { font-size: 13.5px; font-weight: 600; color: var(--text); }
.perm-desc { font-size: 11.5px; color: var(--text-dim); }

/* DELICADO se dice con una palabra, no solo con un color: "conceder configuracion" y
   "conceder consumo" no pesan lo mismo, y una hilera de casillas identicas sugiere que si.
   El color solo no vale — hay quien no lo distingue. */
.perm-aviso {
  font-size: 9.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .3px;
  padding: 1px 5px;
  border-radius: 6px;
  background: var(--danger-dim, var(--bg-hover));
  color: var(--danger);
  vertical-align: middle;
}

/* Con administrador puesto los interruptores se apagan pero NO se esconden: esconderlos
   haria pensar que los permisos se perdieron. Se ven, atenuados, y la nota de la cabecera
   explica que siguen guardados por si vuelve a ser operador. */
.perm-caja.todo-concedido .perm-fila { opacity: .5; cursor: default; }
.perm-caja.todo-concedido .perm-fila:hover { background: transparent; }

/* LA FILA QUE ACABA DE CAMBIAR DE ROL se señala al repintarse (item A41). Cambiar un rol
   redibuja la lista entera, y sin esto lo único que se mueve es el texto de una pastilla
   pequeña, en una fila que quizá ni estabas mirando: la acción parecía no haber hecho nada.

   Es una ANIMACIÓN, no una transición disparada desde JS: si el rAF no llega a correr
   —pestaña de fondo, renderer dormido— una transición dejaría la fila con el fondo del
   destello puesto para siempre. Una animación sin fill-mode termina siempre en el estado
   natural del elemento. La quita animarUnaVez al acabar. */
.user-row-cambiado {
  animation: userRowCambiado 520ms ease-out;
}
@keyframes userRowCambiado {
  0%   { background: var(--accent-dim); border-color: var(--accent); }
  100% { background: transparent; border-color: var(--border); }
}

@media (prefers-reduced-motion: reduce) {
  .user-row-cambiado,
  .modal-overlay,
  .modal-overlay.saliendo,
  .modal-card,
  .modal-overlay.saliendo .modal-card { animation: none; }
}

/* --- Visor de mapa (item 138) ----------------------------------------------- */
/* Más ancha que una modal normal: un mapa de 420px no sirve para orientarse, que es lo
   único que se viene a hacer aquí. */
.mapa-modal {
  width: 760px;
}
.mapa-caja {
  position: relative;
  width: 100%;
  /* Proporción fija en vez de alto en píxeles: así el mapa se ve igual de bien en un
     portátil que en una pantalla grande, sin quedarse en una franja. */
  aspect-ratio: 16 / 10;
  max-height: 62vh;
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-hover);
}
.mapa-iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
.mapa-pie {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--text-dim);
}
.mapa-coords {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.mapa-externo {
  color: var(--accent-strong);
  text-decoration: underline;
}

@media (max-width: 820px) {
  .mapa-modal { width: 100%; }
  .mapa-caja { aspect-ratio: 4 / 3; }
}

/* EL TIRON AL LLEGAR AL TOPE (decidido en el prototipo de validacion, 17-09).
   La tecla que no cabe no entra; sin el tiron, no entrar se parece demasiado a que el teclado
   se colgo. Es la senal de "te he leido, pero ahi ya no cabe".

   ANIMACION Y NO TRANSICION: una transicion que no llega a arrancar -pestana de fondo, motor
   dormido- deja el elemento con estilos a medias para siempre; una animacion, corra o no,
   termina en el estado natural. */
.campo-tiron {
  animation: campoTiron .18s ease;
}
@keyframes campoTiron {
  0%   { transform: translateX(0); }
  30%  { transform: translateX(3px); }
  60%  { transform: translateX(-2px); }
  100% { transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
  .campo-tiron { animation: none; }
}
