/* nexel/www — portal público de clasificados.
 * Look inspirado en los mockups: color de marca en las barras, acento
 * vibrante en las acciones, tarjetas redondeadas, mobile-first. Sin
 * bootstrap ni bundlers: CSS plano con variables. */

/* --------- paleta de marca ---------
 *
 * PUNTO ÚNICO DE VERDAD. Los dos colores de la marca se declaran acá y
 * en ningún otro lado: las variantes (más clara, más oscura, el fondo
 * suave, el halo de foco) salen por `color-mix()` del color base, así
 * que cambiando estos dos hexes cambia el portal entero.
 *
 * Los metadatos que no pueden leer una variable CSS — el theme-color del
 * navegador, el manifest de la PWA y los botones inline de los mails —
 * leen estos mismos dos valores desde PHP: `appMarcaColor()` en
 * lib/marca.php los parsea de este bloque. No los repitas allá.
 *
 * ── `--primario` tiene un eco que NO se puede derivar: el ícono ──
 *
 * Los PNG de favicon/ están pintados con este mismo hex ADENTRO del
 * archivo, y no hay forma de que lo lean de acá. El splash de la PWA los
 * pone juntos —`background_color` sale de esta línea, el dibujo sale del
 * PNG— así que si los dos no son el mismo color, se ve el cuadrado del
 * ícono recortado contra un fondo de otro azul. Era exactamente lo que
 * pasaba hasta el 2026-10-11: la paleta decía #1877f2 y el juego de
 * íconos venía del generador en #006fff. Ganó el del ícono.
 *
 * O sea: tocar `--primario` es repintar también los seis archivos de
 * www/favicon/ (los dos web-app-manifest, apple-touch, favicon-96, el
 * .svg y el .ico) y el site.webmanifest que los acompaña. No alcanza con
 * cambiar el hex de abajo.
 *
 * `color-mix()` pide Chrome 111+ / Safari 16.2+ / Firefox 113+ (2023).
 */
:root {
  --primario:      #006fff;   /* color de marca — barras, links, chips activos */
  --acento:        #f97316;   /* acción primaria — ofertar, favoritos, CTAs */

  /* Variantes derivadas. Los porcentajes se calibraron para reproducir
   * clavados los hexes que antes estaban escritos a mano (#4a93f5,
   * #0f56b3, #d24a7d, #fce4ec), así que cambiar un color base conserva
   * las mismas relaciones de luminosidad que tenía el diseño original. */
  --primario-2:    color-mix(in srgb, var(--primario) 78%, #fff);   /* variante clara */
  --primario-d:    color-mix(in srgb, var(--primario) 72%, #000);   /* panel de filtros */
  --acento-h:      color-mix(in srgb, var(--acento)   88%, #000);   /* hover */
  --acento-soft:   color-mix(in srgb, var(--acento)   16%, #fff);   /* fondos suaves */
  --acento-ring:   color-mix(in srgb, var(--acento)   15%, transparent); /* halo de foco de los inputs */

  /* Verde pleno, para lo que es una NOVEDAD y no un cartel de "salió
   * bien": hoy lo usa el sello de conversar cuando el vendedor contestó y
   * el mensaje sigue sin abrir. Va aparte de `--mint-soft`, que es el
   * fondo suave de los carteles de confirmación y no tiene contraste para
   * llevar un glifo blanco encima. El hover se deriva como el del acento,
   * con la misma mezcla.
   *
   * Es un verde VIVO y no uno de bandera: el sello vive sobre una foto y
   * al lado de los naranjas de `--acento`, que son igual de saturados. Un
   * verde oscuro se hunde contra las fotos y deja de leerse como la
   * novedad que es. */
  --ok:          #22c55e;
  --ok-h:        color-mix(in srgb, var(--ok) 88%, #000);

  /* El rojo de la tercera cara del sello: "el vendedor no te lo aceptó".
   *
   * Es el par de `--ok` y no de `--danger`, y por eso es OTRO rojo: vive
   * en el mismo lugar que el verde —un círculo de 30px sobre una foto, al
   * lado de los naranjas de `--acento`— así que necesita la misma
   * saturación para leerse ahí. El `--danger` de abajo es oscuro a
   * propósito porque vive dentro de un modal blanco; sobre una foto se
   * hunde igual que se hundiría un verde de bandera.
   *
   * El hover se deriva con la misma mezcla que el acento y el ok. */
  --no:          #ef4444;
  --no-h:        color-mix(in srgb, var(--no) 88%, #000);

  /* El rojo de "esto deshace algo". Hoy lo usa `.auth-btn-danger`, que es
   * la baja de un gesto —cancelar una visita, retirar una oferta—.
   *
   * No es un rojo nuevo: es el `#b91c1c` que el portal ya venía usando a
   * mano en los carteles de error (`.hoja-estado`, `.auth-error` y
   * compañía). Acá pasa a tener nombre porque lo empezó a necesitar algo
   * que no es un cartel. Esos cinco lugares siguen con el literal y
   * podrían apuntar a esta variable el día que se toquen; no se
   * reescribieron ahora para no mover estados de error sin probarlos.
   *
   * Es un rojo OSCURO y no uno vivo, al contrario que `--ok`: el verde
   * vive sobre una foto y tiene que competir con los naranjas de la card,
   * y esto vive dentro de un modal blanco, donde un rojo saturado grita
   * más de lo que una cancelación necesita. Con blanco encima da 7,3:1.
   *
   * El hover se deriva con la misma mezcla que el acento y el ok. */
  --danger:      #b91c1c;
  --danger-h:    color-mix(in srgb, var(--danger) 88%, #000);

  --yellow-soft: #fef3c7;
  --mint-soft:   #d4f4e9;
  --lilac:       #e9e6ff;   /* fondo global suave del mockup */
  --bg:          #eef0fa;
  --canvas:      #dfe2ec;   /* fondo del shell — más oscuro que --bg para que las tarjetas blancas destaquen */
  --surface:     #ffffff;
  --border:      #e5e7eb;
  --text:        #101636;   /* azul-tinta del mockup */
  --muted:       #7078a0;
  --star:        #f5b301;

  /* Pines y globos de grupo de la vista mapa del listado. Es el rojo de
   * Google Maps y **el único color del portal que no sale de la paleta**:
   * adentro de un mapa el pin rojo quiere decir "acá hay algo" y lo sabe
   * leer cualquiera, mientras que un pin en `--primario` obliga a aprender
   * de nuevo algo ya aprendido y encima compite con el azul de las rutas y
   * del agua que dibuja el propio mapa.
   *
   * Entra por token igual que todos los demás —el pin suelto y el globo
   * del grupo tienen que dar exactamente el mismo color y con dos hexes
   * escritos a mano se desincronizan— y lo lee `colorPin()` en
   * assets/js/mapa.js. Mismo criterio y mismo valor que la vista Mapa del
   * back office (panel/DESIGN.md §21). */
  --map-pin:     #ea4335;

  --radius-lg:   22px;
  --radius-md:   14px;
  --radius-sm:   10px;
  --shadow-sm:   0 2px 8px rgba(16, 22, 54, 0.06);
  --shadow-md:   0 8px 24px rgba(16, 22, 54, 0.08);
  --shadow-lg:   0 20px 40px rgba(16, 22, 54, 0.12);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  font-size: 15px;
  line-height: 1.4;
}

a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
button { font-family: inherit; cursor: pointer; }

/* Contenedor mobile-first — el portal se pinta como un stack vertical
 * angosto que respeta el look "app" incluso en desktop. */
.app-shell {
  max-width: 480px;
  margin: 0 auto;
  background: var(--canvas);
  min-height: 100vh;
  position: relative;
  /* deja lugar para que el contenido no quede tapado por el .app-nav
   * flotante. Cuenta: 16px de separación del bottom + ~48px del nav +
   * ~15px de gap visible entre el borde inferior de la tarjeta y el
   * borde superior del nav + safe-area de iOS. */
  padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
}
/* En la página de detalle sumamos 20px extra de padding para que se
 * pueda scrollear un poco más allá del borde inferior de la tarjeta,
 * dejando aire visible entre la tarjeta y el nav flotante. */
.app-shell.page-detalle {
  padding-bottom: calc(95px + env(safe-area-inset-bottom, 0px));
}
/* En los listados sumamos 30px extra para que la última card no quede
 * tapada por la barra de navegación flotante al hacer scroll. */
.app-shell.page-listado {
  padding-bottom: calc(110px + env(safe-area-inset-bottom, 0px));
}

/* --------- header navy --------- */

.app-header {
  background: var(--primario);
  color: #fff;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  /* El header es fijo y el contenido le pasa por debajo: sin sombra las
   * cards se cortaban contra un borde plano y parecía que se recortaran
   * solas. Es el mismo par que usa la barra de navegación de escritorio
   * (ver escritorio.css), que cumple el mismo rol allá.
   *
   * No cae sobre el tirador: la lengüeta va en z 30 y el header en 20, o
   * sea que ella pinta por encima. Al revés que en escritorio, donde la
   * barra está por delante y sí le sombrea el nacimiento. */
  box-shadow: 0 2px 12px rgba(16, 22, 54, .18),
              0 1px 3px  rgba(16, 22, 54, .10);
  /* El padding-bottom era 60px de cuando el buscador "colgaba" del header
   * sobre un bloque azul alto. Con el panel de filtros saliendo de abajo,
   * ese aire sobra: la continuidad ya la da el panel. */
  padding: 14px 20px 31px;
  position: relative;
  overflow: hidden;
}

/* Grilla de tres columnas con los costados iguales (1fr cada uno): así el
 * slot del medio queda centrado respecto a la pantalla, sin importar que
 * a la izquierda haya una flecha y a la derecha dos botones. Con flex y
 * `space-between` el centro sólo cae en el medio si ambos lados miden
 * exactamente lo mismo — que no es el caso en el home.
 *
 * Escritorio vuelve a flex en escritorio.css: allá el header tiene otra
 * composición (bloque de texto a la izquierda, acciones a la derecha). */
.app-header-top {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
  position: relative;
  z-index: 2;
  /* Sin min-height: la fila mide lo que miden sus hijos. El viejo 60px
   * fijo repartía aire arriba y abajo del logo aunque no hiciera falta. */
}
.app-header-top > .app-back        { justify-self: start; }
/* La campana del home ocupa el slot izquierdo, el mismo que en las demás
 * páginas usa la flecha de volver. El menú NO está acá: vive dentro de
 * `.app-header-actions`, que ya se alinea al borde derecho. */
.app-header-top > .app-header-bell { justify-self: start; }
.app-header-top > .app-header-logo { justify-self: center; }
.app-header-actions                { justify-self: end; }
.app-header-title {
  font-size: 1.7rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.app-header-title .accent { color: #f0b5cb; font-weight: 400; }

/* Logo del header móvil. Ocupa el lugar del título en el home; el PNG es
 * blanco y depende de que el header siga siendo azul. */
.app-header-logo img {
  /* Mismo alto que el logo de la barra de escritorio: a 32px quedaba más
   * grande que los botones circulares de los costados y se comía la fila.
   * El PNG está recortado al dibujo, así que este número es lo que se ve.
   * `width: auto` mantiene la proporción; la imagen no se toca. */
  height: 26px;
  width: auto;
  display: block;
}

.app-back {
  background: rgba(255,255,255,.15);
  border: none;
  color: #fff;
  width: 36px; height: 36px;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.1rem;
  text-decoration: none;
}
.app-header-actions {
  display: flex; gap: 10px; align-items: center;
}
.app-icon-btn {
  background: rgba(255,255,255,.15);
  border: none;
  color: #fff;
  width: 36px; height: 36px;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1rem;
  position: relative;
}
.app-icon-btn .dot {
  position: absolute; top: 4px; right: 4px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--acento);
  border: 2px solid var(--primario);
}

/* buscador flotante que "cuelga" del header */
.app-search-card {
  background: #fff;
  border-radius: var(--radius-md);
  padding: 12px 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  box-shadow: var(--shadow-md);
  position: relative;
  z-index: 2;
}
.app-search-card input {
  flex: 1;
  /* Sin esto el input no baja de su ancho intrínseco: en flex el mínimo
   * automático de un `<input>` sin `size` es el de ~20 caracteres, y ese
   * piso gana sobre el `flex: 1`. Cuando el navegador agranda la letra
   * —Chrome de Android con la escala de texto subida— el campo dejaba de
   * achicarse, se comía el renglón entero y empujaba al botón AFUERA de
   * la tarjeta blanca; el `overflow: hidden` del header lo terminaba
   * recortando contra el borde de la pantalla. Con el piso en 0 el input
   * cede lo que haga falta y el botón nunca se va de la tarjeta. */
  min-width: 0;
  border: none;
  outline: none;
  font-size: 0.95rem;
  color: var(--text);
  background: transparent;
  padding: 4px 0;
  font-family: inherit;
}
.app-search-card input::placeholder { color: #a3a9c4; }
/* Los dos costados del buscador miden lo que miden y no se negocian: el
 * único que da y toma ancho es el campo del medio (ver el `min-width: 0`
 * de arriba). Sin el `flex: 0 0 auto` el botón queda con el `shrink: 1`
 * por defecto y, cuando el renglón queda justo, se achica hasta dejar de
 * ser un cuadrado — el ícono se le sale por los costados. */
.app-search-card .search-icon {
  flex: 0 0 auto;
  color: #a3a9c4;
  font-size: 1.1rem;
}
.app-search-card .filter-icon {
  flex: 0 0 auto;
  background: var(--acento-soft);
  color: var(--acento);
  border: none;
  width: 34px; height: 34px;
  border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.95rem;
}

/* --------- panel de filtros desplegable (home móvil) --------- */

/* El tirador es una lengüeta colgada del borde inferior del header: mismo
 * azul que él, al ras, esquinas de arriba rectas y las de abajo
 * redondeadas. Al compartir el color y no dejar junta se lee como una
 * prolongación del header — que es de donde sale el panel.
 *
 * Reemplaza al círculo blanco que iba montado a caballo del borde: sobre el
 * azul de marca se leía como una burbuja pegoteada encima, ajena tanto al
 * header como al contenido.
 *
 * El diseño es el mismo en las dos vistas — lo único que cambia es de qué
 * cuelga, del header en móvil y de la barra de navegación en escritorio —
 * así que vive acá y escritorio.css ya no lo pisa.
 *
 * Va después del <header> y no adentro: el header recorta con
 * `overflow: hidden` y ahí dentro la lengüeta no existiría. */
.panel-filtros-toggle {
  margin: 0 auto;
  position: relative;
  /* Por encima del panel: es el único control que lo cierra, así que no
   * puede quedar tapado por él. */
  z-index: 30;
  width: 64px; height: 26px;
  border: none;
  border-radius: 0 0 13px 13px;
  background: var(--primario);
  color: #fff;
  font-size: .78rem;
  display: flex; align-items: center; justify-content: center;
  /* Sombra sólo hacia abajo: arriba se funde con el header, y una sombra
   * ahí marcaría justo la junta que no queremos que se vea. */
  box-shadow: 0 5px 10px rgba(16, 22, 54, .16);
  transition: background .18s, color .18s;
}
.panel-filtros-toggle i { transition: transform .25s ease; }
/* Abierto: el chevron apunta arriba, o sea "esto se cierra"... */
.panel-filtros-toggle.is-open i { transform: rotate(180deg); }
/* ...y la lengüeta queda sobre el cuerpo del panel, que es azul oscuro:
 * azul sobre azul desaparecería. Se invierte a pastilla blanca, el mismo
 * recurso que usan las pestañas activas de la barra de escritorio. */
.panel-filtros-toggle.is-open { background: #fff; color: var(--primario); }

/* El hover va detrás de `hover: hover` porque en touch queda pegado: el
 * dedo levanta el estado y la lengüeta se quedaba con el color de hover
 * hasta tocar otra cosa. `:active` sí sirve en ambas — es el feedback del
 * click, que hace falta ahora que la lengüeta no se encoge (el
 * `scale(.94)` del círculo acá dejaba ver la junta con el header). */
@media (hover: hover) {
  .panel-filtros-toggle:hover          { background: var(--primario-d); }
  .panel-filtros-toggle.is-open:hover  { background: var(--acento-soft); color: var(--acento); }
}
.panel-filtros-toggle:active           { background: var(--primario-d); }
.panel-filtros-toggle.is-open:active   { background: var(--acento-soft); color: var(--acento); }

/* --------- pila superior fija (header + panel + tirador) --------- */

/* Los tres viajan juntos: el panel cuelga de un ancla pegada al header y
 * el tirador se monta sobre su borde, así que si el header se moviera
 * solo se separarían. Sticky sobre el conjunto los mantiene solidarios.
 *
 * Se esconde al scrollear hacia abajo y vuelve apenas se sube — la clase
 * la pone assets/js/cabecera.js. */
/* `fixed` y no `sticky`: sticky lo anula cualquier ancestro con overflow,
 * y `.app-shell` lo tiene a partir de 640px. Con sticky roto el header se
 * iba por scroll natural y sólo reaparecía al llegar al tope de la página.
 * `fixed` se posiciona contra el viewport y no le afecta.
 *
 * left/right en 0 + `margin: 0 auto` centra la barra dentro del ancho del
 * shell sin gastar `transform`, que queda libre para el ocultamiento.
 *
 * Al salir del flujo deja de reservar su alto: `cabecera.js` se lo devuelve
 * al shell como padding-top. */
.app-topstack {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  margin: 0 auto;
  max-width: 480px;   /* mismo ancho que .app-shell */
  z-index: 40;
  transition: transform .28s ease;
}
.app-topstack.is-hidden { transform: translateY(-100%); }

/* El header tiene que tapar la parte del panel que queda metida detrás.
 * Ya era `position: relative`; sólo le falta ganar la capa. */
.app-header { z-index: 20; }

/* Ancla de alto 0: marca dónde termina el header sin ocupar lugar en el
 * flujo. El panel cuelga de acá en absoluto, así al abrirse baja POR
 * ENCIMA del contenido en vez de empujarlo, y el chevron no se mueve. */
.panel-filtros-anchor {
  position: relative;
  height: 0;
  z-index: 10;   /* por debajo del header (20), por encima del contenido */
}

/* Cuerpo propio, no una prolongación del header: azul más oscuro, 10px de
 * aire a cada lado y esquinas redondeadas abajo. `--tuck` es cuánto se
 * mete por detrás del header — más que el radio de 22px, para que las
 * esquinas superiores queden tapadas incluso en la curva. */
.panel-filtros {
  --tuck: 26px;
  position: absolute;
  top: calc(-1 * var(--tuck));
  left: 10px;
  right: 10px;
  background: var(--primario-d);
  color: #fff;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  box-shadow: 0 14px 28px rgba(16, 22, 54, .30);
  overflow: hidden;
  /* Truco de grid: 0fr -> 1fr anima la altura REAL sin tener que
   * conocerla de antemano, cosa que max-height no permite. */
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .32s cubic-bezier(.22, .61, .36, 1);
}
.panel-filtros.is-open { grid-template-rows: 1fr; }

.panel-filtros-inner {
  overflow: hidden;   /* recorta el contenido mientras la fila crece */
  min-height: 0;      /* sin esto, el contenido impone su alto y no colapsa */
}
/* El padding vive acá y no en el inner: en el inner se vería como una
 * franja azul incluso con el panel cerrado. Arriba se suma `--tuck`, que
 * es la franja que queda escondida detrás del header, más lo que mide el
 * tirador (26px) y 18 de respiro: la lengüeta cae dentro del panel y sin
 * ese aire el primer filtro le queda pegado. */
.panel-filtros-body { padding: calc(var(--tuck) + 44px) 18px 20px; }

/* Los tiles ya vienen con fondo azul y sombra propios porque en escritorio
 * son una tarjeta suelta. Acá están sobre azul: se anulan los dos para que
 * no quede una caja dentro de otra del mismo color. */
.panel-filtros .tiles-panel {
  background: transparent;
  box-shadow: none;
  padding: 0;
  overflow: visible;
}
.panel-filtros .tiles-panel::before,
.panel-filtros .tiles-panel::after { display: none; }
/* Aire propio arriba del "¿Qué buscás?". Va acotado al panel: en
 * escritorio los tiles son una tarjeta suelta y ya lo aporta su padding. */
.panel-filtros .tiles-title { margin-top: 15px; }

/* --- el formulario, re-vestido para fondo azul --- */

.panel-filtros .filtros-label { color: rgba(255, 255, 255, .7); }

/* Chip apagado: velo blanco translúcido. Elegido: pastilla blanca sólida
 * con texto azul — máximo contraste contra el fondo. */
.panel-filtros .filtros-chip span {
  background: rgba(255, 255, 255, .14);
  color: #fff;
}
.panel-filtros .filtros-chip input:checked + span {
  background: #fff;
  color: var(--primario);
}

.panel-filtros .filtros-form input[type="text"] {
  background: rgba(255, 255, 255, .14);
  border-color: rgba(255, 255, 255, .22);
  color: #fff;
}
.panel-filtros .filtros-form input[type="text"]::placeholder { color: rgba(255, 255, 255, .55); }
.panel-filtros .filtros-form input[type="text"]:focus {
  background: #fff;
  border-color: #fff;
  color: var(--text);
}

/* Slider: la pista tiene que verse sobre azul, y el thumb rosa ya
 * contrasta bien tal cual está. */
.panel-filtros .range-values     { color: #fff; }
.panel-filtros .range-track      { background: rgba(255, 255, 255, .25); }
.panel-filtros .range-slider.is-off .range-track-fill { background: rgba(255, 255, 255, .45); }
.panel-filtros .range-slider.is-off .range-values     { color: rgba(255, 255, 255, .6); }
.panel-filtros .range-slider.is-off .range-input::-webkit-slider-thumb { background: rgba(255, 255, 255, .7); }
.panel-filtros .range-slider.is-off .range-input::-moz-range-thumb     { background: rgba(255, 255, 255, .7); }

.panel-filtros .filtros-section:last-of-type { margin-bottom: 6px; }
/* Las dos acciones del pie del panel: el alternador de vista y el envío.
 * En móvil van apiladas y a todo el ancho —el panel es una columna y dos
 * botones al 50% dejan los rótulos partidos en dos líneas— con el
 * alternador ARRIBA: "Ver resultados" es la salida natural del formulario
 * y tiene que quedar última, pegada al borde. */
.panel-filtros-acciones {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.panel-filtros-submit,
.panel-filtros-vista {
  width: 100%;
  padding: 14px 16px;
  font-size: .95rem;
  gap: 8px;
}
/* Se suma a los 6px que deja la última .filtros-section: 16px en total de
 * aire entre el último filtro y las acciones. */
.panel-filtros-acciones { margin-top: 10px; }
/* Sobre el azul del panel el secundario gris de `.filtros-btn-secondary`
 * se ensucia: acá va el mismo velo blanco translúcido que usan los chips
 * apagados y los inputs, que es el vocabulario de "elemento apagado sobre
 * la banda" (ver los bloques de arriba). */
.panel-filtros .filtros-btn-secondary {
  background: rgba(255, 255, 255, .14);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, .28);
}
.panel-filtros .filtros-btn-secondary:hover {
  background: rgba(255, 255, 255, .24);
  border-color: rgba(255, 255, 255, .45);
}

/* --------- guardar la búsqueda como radar --------- */

/* Va separada de las dos acciones de arriba por una línea, y es a
 * propósito: las de arriba devuelven resultados AHORA y ésta no devuelve
 * nada hoy — pide que el portal escriba mañana. Al lado competirían, y la
 * que la persona vino a tocar es "Ver resultados".
 *
 * La línea es blanca translúcida porque el panel está pintado de azul; un
 * `--border` acá se ensucia, igual que le pasa al secundario de arriba. */
.panel-filtros-radar {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, .22);
}
/* Fantasma sobre el azul: sólo contorno. Es el escalón que le faltaba al
 * vocabulario del panel —relleno naranja para la acción principal, velo
 * blanco para lo apagado— y deja claro que es de otra familia sin sumar
 * un color nuevo. */
.filtros-btn-radar {
  width: 100%;
  padding: 12px 16px;
  gap: 8px;
  font-size: .9rem;
  background: transparent;
  color: #fff;
  border: 1px dashed rgba(255, 255, 255, .5);
}
.filtros-btn-radar:hover:not(:disabled) {
  background: rgba(255, 255, 255, .14);
  border-style: solid;
}
.filtros-btn-radar:disabled { opacity: .85; cursor: default; }

.panel-filtros-radar-pie {
  margin-top: 7px;
  text-align: center;
  font-size: .74rem;
  line-height: 1.4;
  color: rgba(255, 255, 255, .75);
}
/* El acuse y el error se dicen en el mismo renglón que la promesa: es el
 * texto de ese bloque el que cambia, no un cartel nuevo que empuje el
 * panel hacia abajo justo cuando se acaba de tocar un botón. */
.panel-filtros-radar-pie.is-ok    { color: #bbf7d0; font-weight: 600; }
.panel-filtros-radar-pie.is-error { color: #fecaca; font-weight: 600; }

/* chips debajo del buscador (Best offers, Free, etc.) */
.app-chip-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 14px 10px 4px;
  scrollbar-width: none;
}
.app-chip-row::-webkit-scrollbar { display: none; }

.app-chip {
  background: #fff;
  color: var(--text);
  border: 1px solid var(--border);
  padding: 6px 14px;
  border-radius: 99px;
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
  transition: background .15s, color .15s, border-color .15s;
}
.app-chip.pink,
.app-chip.mint,
.app-chip.lilac { background: #fff; color: var(--text); }
.app-chip.active {
  background: var(--primario);
  color: #fff;
  border-color: var(--primario);
}

/* --------- bloque de categorías (tarjetas grandes tipo mockup) --------- */

/* Las secciones dejan 10px libres a cada lado para que ni el panel de
 * tiles ni las tarjetas de aviso peguen contra el borde del shell. */
.app-section {
  padding: 20px 10px 4px;
}
.app-section-title {
  font-size: 1.05rem;
  font-weight: 700;
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.app-section-title .link {
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--acento);
}

.tiles-panel {
  background: var(--primario);
  border-radius: var(--radius-lg);
  padding: 22px 18px;
  color: #fff;
  position: relative;
  overflow: hidden;
  box-shadow: 0 0 32px rgba(16, 22, 54, 0.22),
              0 0 8px  rgba(16, 22, 54, 0.10);
}
.tiles-panel::before {
  content: '';
  position: absolute;
  right: -30px; bottom: -30px;
  width: 90px; height: 90px;
  background: #f0b344;
  border-radius: 50%;
  opacity: .3;
}
.tiles-panel::after {
  content: '';
  position: absolute;
  left: -18px; bottom: -18px;
  width: 50px; height: 50px;
  background: #8b5cf6;
  border-radius: 12px;
  transform: rotate(20deg);
  opacity: .35;
}
.tiles-title {
  position: relative;
  z-index: 1;
  color: #fff;
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: -.01em;
  margin-bottom: 14px;
  min-height: 20px;
  opacity: .95;
  text-align: center;
}

/* Grilla de 4 columnas que baja de renglón: TODOS los tiles de la vista se
 * ven a la vez, sin scroll.
 *
 * Antes era un carrusel (`overflow-x: auto` + tiles de 92px fijos) y la
 * vista más poblada —5 tiles: Volver + 4 opciones— dejaba el último
 * cortado contra el borde. El scroll horizontal no se anuncia solo: sin
 * flecha ni sombra de corte, la mitad de la gente nunca descubría que
 * "Camión" o "Todos" existían.
 *
 * El tile mide exactamente un cuarto de la fila (descontados los 3 gaps),
 * así que entran 4 por renglón y el quinto baja. Es deliberado que la
 * medida NO dependa de cuántos tiles haya: repartiendo el ancho entre
 * todos, una vista de 5 los achicaba a ~57px y una de 3 los dejaba
 * enormes, y la misma opción cambiaba de tamaño al navegar entre vistas.
 * Con el cuarto fijo, "Casa" mide igual en todas.
 *
 * `flex-shrink: 0` es lo que garantiza el corte en 4: sin eso flex prefiere
 * apretar antes que bajar de renglón. Lo que sobra —el renglón huérfano de
 * 1 tile, o el ancho de más cuando el `max-width` ya topó— lo centra el
 * `justify-content`.
 *
 * Sin `overflow` tampoco se recorta el `.tile-badge`, que cuelga fuera
 * de la caja del ícono. */
.tiles-row {
  --tiles-cols: 4;
  --tiles-gap: 8px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px var(--tiles-gap);
  padding: 2px 0;
  position: relative;
  z-index: 1;
}

.tile {
  flex: 0 0 auto;
  width: calc((100% - var(--tiles-gap) * (var(--tiles-cols) - 1)) / var(--tiles-cols));
  min-width: 0;
  max-width: 96px;
  background: transparent;
  color: #fff;
  border: none;
  text-align: center;
  padding: 8px 2px;
  border-radius: var(--radius-md);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  transition: background .15s;
  text-decoration: none;
  font: inherit;
  cursor: pointer;
}
.tile:hover { background: rgba(255,255,255,.08); }
.tile[disabled] {
  opacity: .4;
  cursor: not-allowed;
}
.tile[disabled]:hover { background: transparent; }

.tiles-view { position: relative; }
/* Los 56px del cuadrado pasan de medida fija a techo: si el tile es más
 * angosto que eso, el ícono lo acompaña y se mantiene cuadrado con
 * `aspect-ratio`. Con 4 columnas eso recién ocurre por debajo de ~308px de
 * viewport (la pantalla de tapa de un plegable); en cualquier teléfono real
 * el cuadrado da los 56px enteros. El glifo se queda en 1.7rem: aun en ese
 * caso extremo la caja ronda los 49px y lo contiene sin apretarlo. */
.tile-icon {
  font-size: 1.7rem;
  width: 100%;
  max-width: 56px;
  height: auto;
  aspect-ratio: 1;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.08);
  border-radius: 14px;
  position: relative;
}
/* Las etiquetas más largas son de 9 caracteres ("Camioneta", "Vehículos")
 * y son palabras sueltas: no hay dónde cortarlas en dos renglones, así que
 * el que se achica es el cuerpo. Sólo hace falta por debajo de ~400px de
 * viewport; de ahí para arriba la columna ya da los 0.85rem completos. El
 * `ellipsis` es la red de contención por si aparece una etiqueta más larga
 * que las de hoy. */
.tile-label {
  font-size: clamp(.72rem, 3.4vw, .85rem);
  font-weight: 500;
  letter-spacing: -.01em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tile-badge {
  position: absolute;
  top: -4px; right: -8px;
  background: var(--acento);
  color: #fff;
  font-size: 0.6rem;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 6px;
}

/* --------- tarjetas de aviso (listado / destacados) --------- */

.card-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 0 10px;
}
/* Cuando el listado vive dentro de una .app-section (home destacados),
 * la sección ya aporta el inset lateral — evitamos doblarlo. */
.app-section .card-list { padding: 0; }

/* Tarjeta grande con foto arriba y datos abajo. */
.aviso-card {
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: 0 0 24px rgba(16, 22, 54, 0.18),
              0 0 6px  rgba(16, 22, 54, 0.08);
  overflow: hidden;
  color: var(--text);
  display: block;
  padding: 3px;
  transition: box-shadow .2s ease;
}
/* Card en foco: halo del azul de marca en vez de la sombra gris de
 * reposo. El foco lo marcan dos disparadores distintos y el realce tiene
 * que ser el mismo en los dos: el hover del mouse en escritorio, y la
 * clase `.is-foco` —que pone assets/js/gallery.js sobre la card que el
 * auto-slide eligió por viewport— en móvil. */
.aviso-card:hover,
.aviso-card.is-foco {
  box-shadow: 0 0 34px color-mix(in srgb, var(--primario) 65%, transparent),
              0 0 10px color-mix(in srgb, var(--primario) 50%, transparent);
}

.aviso-media {
  position: relative;
  aspect-ratio: 16 / 10;
  background: #eef0fa center/cover no-repeat;
  border-radius: var(--radius-sm);
  overflow: hidden;
  touch-action: pan-y;
}
.aviso-media img {
  width: 100%; height: 100%; object-fit: cover;
}

/* --- galería swipeable (listado + detalle) --- */
.media-strip {
  display: flex;
  width: 100%; height: 100%;
  transition: transform .25s ease;
  will-change: transform;
}
.media-strip img {
  flex: 0 0 100%;
  width: 100%; height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none; /* deja el swipe al contenedor */
  transform: scale(1);
  transition: transform .3s ease-out;
  will-change: transform;
}
/* Zoom "Ken Burns" durante el auto-slide: sólo se aplica a la img
 * visible de la card auto-slideante. La clase la agrega/quita el JS
 * en sync con cada tick del intervalo. */
.media-strip img.is-zooming {
  transform: scale(1.45);
  transition: transform 3s ease-out;
}
/* El ancho lo pone el contenido, así que sin `max-width` una publicación
 * con muchas fotos arma una fila más larga que la card y se derrama por
 * los costados. El tope de fotos de la card (APP_CARD_MAX_FOTOS en
 * lib/render.php) ya evita que pase ahí; esto es la red de seguridad para
 * el resto de los usos — el hero del detalle sí monta todas las fotos.
 *
 * Envuelve en vez de recortar: con `overflow:hidden` el punto activo
 * puede quedar fuera de vista, y ahí el indicador deja de indicar. Al
 * estar anclado por `bottom`, las filas extra crecen hacia arriba y
 * quedan dentro de la imagen. */
.media-dots {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  max-width: calc(100% - 16px);
  gap: 5px;
  z-index: 3;
  padding: 3px 8px;
  background: rgba(16, 22, 54, .35);
  border-radius: 99px;
}
.media-dots span {
  flex: 0 0 5px;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .55);
  transition: background .15s;
}
.media-dots span.active { background: #fff; }
.aviso-ribbon {
  position: absolute; top: 10px; left: 10px;
  background: var(--acento-soft);
  color: var(--acento);
  padding: 4px 10px;
  border-radius: 99px;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* "Mi aviso" — el cartel de las cards propias, DEBAJO del rubro y en la
 * misma columna izquierda (ver lib/render.php).
 *
 * Estuvo hasta 2026-09-28 en la esquina derecha, ocupando el lugar que
 * en las cards ajenas llevan el corazón y el hamburguesa. Leía mal: esa
 * esquina es la de los GESTOS —todo lo que hay ahí se toca— y el cartel
 * no es un gesto sino una propiedad del aviso, igual que el rubro. Puesto
 * en la esquina de los botones se leía como un botón apagado, y encima
 * hacía saltar el ojo de una punta a la otra para juntar dos datos que
 * son del mismo tipo.
 *
 * Ahora van los dos en una columna: el rubro arriba, el cartel abajo.
 * Misma pastilla, misma medida, y lo único que los distingue es el color
 * —el rubro en el acento, éste en el de MARCA— porque hablan de cosas
 * distintas: uno del aviso, el otro de quien lo mira. El fondo se mezcla
 * acá y no en `:root` porque es el único que lo usa; la proporción es la
 * de `--acento-soft`, para que las dos pastillas pesen lo mismo sobre la
 * foto.
 *
 * El corrimiento es un `translateY` de su PROPIA altura y no un `top`
 * calculado a mano: las dos pastillas tienen el mismo `font-size`, el
 * mismo `padding` y heredan el mismo `line-height` del contenedor, así
 * que miden igual y el salto sigue siendo exacto si mañana cambia la
 * tipografía. Un `top: 38px` habría que recalcularlo a ojo.
 *
 * `z-index` igual al que tenían los botones: la fila de `.media-dots`
 * pasa por abajo. */
.aviso-mio {
  position: absolute; top: 10px; left: 10px;
  transform: translateY(calc(100% + 6px));
  background: color-mix(in srgb, var(--primario) 16%, #fff);
  color: var(--primario);
  padding: 4px 10px;
  border-radius: 99px;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  z-index: 4;
}
/* En la ficha, el mismo cartel en la medida del hero: los 12px de la
 * esquina y un cuerpo acorde al tamaño de la foto grande.
 *
 * Va en la MISMA esquina que en la card —arriba a la izquierda— porque
 * las dos superficies tienen que decir lo mismo en el mismo lugar. Pero
 * sin el corrimiento: el hero no dibuja rubro, así que la esquina está
 * libre y el cartel se apoya directamente en ella. */
.detalle-hero .aviso-mio {
  top: 12px; left: 12px; right: auto;
  transform: none;
  font-size: 0.75rem;
  padding: 5px 12px;
}
/* EL CORAZÓN — la única pieza del favorito en la card, y tiene TRES
 * caras. Durante un tiempo el favorito no hizo más que guardar, y avisar
 * cuando el aviso cambiaba fue un gesto aparte con su propia campana en la
 * fila de sellos de al lado. Los dos se fusionaron y el corazón se comió
 * esa campana.
 *
 *   · vacío                          → no lo sigue
 *   · relleno, pastilla blanca       → lo sigue
 *   · relleno blanco, pastilla naranja → lo sigue Y el aviso cambió sin
 *                                        que lo haya visto
 *
 * Las dos primeras las alternan el PHP al pintar y el JS al tocarlo, entre
 * `fa-regular` y `fa-solid`; la tercera es `.tiene-novedad` (ver
 * lib/render.php, detalle.php y `pintarCorazon()` en
 * assets/js/aviso-modales.js). `.is-fav` queda como estado para el JS, no
 * como gancho de color: el color del glifo no cambia con él.
 *
 * ---------------------------------------------------------------
 * Por qué la pastilla naranja aparece SÓLO en la tercera
 * ---------------------------------------------------------------
 * La pastilla llena en el acento es el lenguaje de los SELLOS (ver
 * `.aviso-sello[data-cara]`) y quiere decir una cosa muy concreta: hay algo
 * del otro lado que no viste. Por eso estuvo mal hasta 2026-09-18f, cuando
 * el corazón la usaba para decir "guardado" — un estado que nadie tiene que
 * ir a mirar, pintado como una notificación, en una fila que ya tenía
 * cuatro.
 *
 * Ahora sí la usa, y por el motivo correcto: en la tercera cara el corazón
 * ESTÁ diciendo "el aviso cambió, andá a verlo". Es el mismo naranja y el
 * mismo significado que tenía la campana que reemplaza, así que la persona
 * que ya lo tenía aprendido no tiene que aprender nada nuevo. Guardar un
 * favorito, en cambio, sigue sin ser una notificación: ésa es la cara del
 * medio y se queda en la pastilla blanca. */
.aviso-corazon {
  position: absolute; top: 10px; right: 10px;
  width: 30px; height: 30px;
  background: rgba(255,255,255,.85);
  color: var(--acento);
  border: none;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.9rem;
  z-index: 4;   /* por encima de .media-dots, si no el corazón queda tapado */
  transition: background .15s, transform .15s;
}
/* La tercera cara, en las dos medidas. El glifo va en blanco y sigue
 * relleno: la novedad sólo puede darse sobre un aviso marcado, así que
 * vaciarlo acá sería contradecir el estado que el botón está informando.
 *
 * Una sola regla para la card y para el hero: cambia la medida, no el
 * significado. Le gana a las dos bases por ESPECIFICIDAD (dos clases contra
 * una) y no por orden, que es lo que permite que viva acá aunque
 * `.detalle-corazon-lg` se declare mucho más abajo. */
.aviso-corazon.tiene-novedad,
.detalle-corazon-lg.tiene-novedad {
  background: var(--acento);
  color: #fff;
}
.aviso-corazon.tiene-novedad:hover,
.detalle-corazon-lg.tiene-novedad:hover { background: var(--acento-h); }
/* Hamburguesa de acciones — la misma pastilla que el corazón y en el
 * mismo rosa, justo debajo (10px del borde + 30px del corazón + 6px de
 * aire). Son dos botones de la misma familia: lo que los distingue es el
 * glifo, no el color. Abierto se invierte igual que un corazón guardado. */
.aviso-menu {
  position: absolute; top: 46px; right: 10px;
  width: 30px; height: 30px;
  background: rgba(255,255,255,.85);
  color: var(--acento);
  border: none;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.85rem;
  z-index: 4;   /* igual que el corazón: por encima de .media-dots */
  transition: background .15s, transform .15s;
}
.aviso-menu[aria-expanded="true"] { background: var(--acento); color: #fff; }

/* Sellos de lo que esta persona ya hizo con el aviso —visitó, ofertó,
 * consultó, sigue—, a la izquierda del corazón y en su misma fila.
 *
 * La fila arranca donde termina el corazón (10px del borde + 30px del
 * corazón + 6px de aire) y CRECE HACIA LA IZQUIERDA: al estar anclada a
 * la derecha, un sello apagado no deja un hueco en el medio, los que
 * quedan se corren solos. Por eso es un contenedor flex y no cuatro
 * `right` fijos.
 *
 * Cada sello abre el mismo modal que su ítem del menú (ver
 * assets/js/aviso-acciones.js): dicen qué pasó y llevan a donde se hace
 * de nuevo —o se deshace, en el caso del favorito.
 *
 * Nacen en la MISMA pastilla blanca del corazón y el hamburguesa —fondo
 * semitransparente, glifo en `--acento`— y no rellenos, que es como
 * estaban hasta 2026-09-17d. Con cuatro sellos posibles por card, una fila
 * de círculos naranjas llenos al lado del corazón y el hamburguesa pálidos
 * se leía como cuatro alertas sobre cada aviso, y en una grilla de treinta
 * cards eso es una pared de naranja que no dice nada. La pastilla pálida
 * es el estado de reposo de todos los botones que viven sobre la foto: el
 * relleno queda reservado para cuando hay algo que decir (ver
 * `[data-cara]` abajo).
 *
 * `hidden` no alcanza contra el `inline-flex`: lo apaga la última regla,
 * igual que el resto de los `[hidden]` del archivo. */
.aviso-sellos {
  position: absolute; top: 10px; right: 46px;
  display: flex;
  gap: 6px;
  z-index: 4;   /* mismo plano que el corazón y el hamburguesa */
}
.aviso-sello {
  width: 30px; height: 30px;
  background: rgba(255,255,255,.85);
  color: var(--acento);
  border: none;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.85rem;
  transition: background .15s, transform .15s;
}
.aviso-sello:hover  { background: #fff; }
.aviso-sello:active { transform: scale(.94); }
.aviso-sello[hidden] { display: none !important; }

/* LAS CARAS SÓLIDAS. La pastilla pálida de arriba es la de `espera` y no
 * necesita regla: es el fondo base y quiere decir "esto está en curso y no
 * hay nada nuevo".
 *
 * El relleno es lo que llama la atención, así que sólo lo llevan los dos
 * sellos que tienen algo para contar. Qué dicen lo decide el servidor
 * (`appSelloCara()` en lib/marcas.php) y depende de la marca:
 *
 *   · visita y oferta → el RESULTADO. Verde si el vendedor aceptó —y ahí
 *     se queda para siempre—, rojo si no, hasta que se lee.
 *   · conversar y favorito → la NOVEDAD. Hay mensajes sin leer, o el aviso
 *     cambió. Nunca rojo: no hay nada que rechazar.
 *
 * Y de ahí que la misma cara `ok` se pinte de dos colores distintos, que
 * es la única excepción de este bloque: en visita y oferta el verde dice
 * QUE SÍ —es un resultado, y el verde es lo que lo distingue del rojo de
 * al lado—, pero en conversar y favorito no hay un sí ni un no, sólo "mirá
 * esto". Para eso el color del portal es `--acento`, el mismo del corazón
 * con novedad y del hamburguesa abierto. Un chat en verde estaría diciendo
 * que algo salió bien cuando lo único que pasó es que llegó un mensaje.
 *
 * El cambio es sólo de fondo y de glifo: misma medida, mismo ícono y mismo
 * lugar en la fila, porque sigue siendo el mismo sello.
 *
 * Van después de la regla base y del hover para ganarle a los dos sin
 * `!important`: misma especificidad más orden. El atributo lo pone el
 * servidor al rendir y lo mueve el JS sin recargar, desde dos lados: el
 * latido que pregunta cada tanto si pasó algo (assets/js/novedades.js) y
 * la respuesta del GET del gesto, que es la que puede dejar el sello en su
 * cara final (`appNovedadSello()`, mismo archivo).
 *
 * ---------------------------------------------------------------
 * Sin parpadeo, a propósito
 * ---------------------------------------------------------------
 * Hasta 2026-09-17 el glifo del sello verde parpadeaba a 1 Hz, y tenía
 * sentido mientras el verde era exclusivo de conversar: era un sello
 * único, y un color quieto se vuelve parte de la foto a los dos segundos
 * de mirarla.
 *
 * Con los cuatro sellos capaces de pintarse eso se dio vuelta: en una card
 * pueden quedar cuatro glifos parpadeando a la vez, cada uno con su fase,
 * y en una grilla de treinta cards son decenas. Deja de llamar la atención
 * sobre algo y pasa a ser ruido sobre todo. El contraste contra la
 * pastilla pálida ya distingue la cara, que es lo único que el sello tiene
 * que decir.
 *
 * Con esto el portal se queda sin ninguna animación infinita: las que
 * quedan son pops de medio segundo, así que tampoco hace falta la guarda
 * de `prefers-reduced-motion` que acompañaba al parpadeo. */
.aviso-sello[data-cara="ok"],
.aviso-sello[data-cara="no"] { color: #fff; }

.aviso-sello[data-cara="ok"]       { background: var(--ok); }
.aviso-sello[data-cara="ok"]:hover { background: var(--ok-h); }
.aviso-sello[data-cara="no"]       { background: var(--no); }
.aviso-sello[data-cara="no"]:hover { background: var(--no-h); }

/* La novedad de conversar, en el acento y no en el verde (ver arriba). Va
 * después para ganarle por orden, con la especificidad justa para pisar
 * sólo ésta.
 *
 * `favorito` estaba en esta misma regla hasta que su sello se fue de la
 * fila: esa novedad la pinta ahora el corazón, con este mismo naranja (ver
 * `.aviso-corazon.tiene-novedad`). El color es el mismo a propósito —
 * cambió qué pieza lo lleva, no qué significa. */
.aviso-sello[data-aviso-sello="conversar"][data-cara="ok"] { background: var(--acento); }
.aviso-sello[data-aviso-sello="conversar"][data-cara="ok"]:hover { background: var(--acento-h); }

/* Menú de acciones del aviso — uno solo por página (ver
 * assets/js/aviso-acciones.js). Va al centro de la pantalla en cualquier
 * medida, con el mismo velo que los modales `.hoja-*` a los que lleva
 * cada ítem: son el mismo gesto en dos pasos y el segundo tiene que
 * aparecer donde estaba el primero. De qué aviso habla lo dice la tira de
 * arriba (`.hoja-aviso`), no la posición. */
.aviso-acciones[hidden] { display: none !important; }
.aviso-acciones {
  position: fixed;
  inset: 0;
  z-index: 230;   /* por encima de los modales, que abren desde acá */
  display: flex;
  align-items: center;
  justify-content: center;
  /* Sin aire a los costados: en teléfono el panel va de borde a borde,
   * igual que la tarjeta de los modales `.hoja-*`. El que manda el ancho
   * es el `max-width` del panel, no un margen de acá.
   *
   * Arriba y abajo sí queda: el panel se centra con flex, y un panel más
   * alto que la pantalla —cinco botones, con los rótulos partidos en dos
   * renglones— se sale por los dos lados a la vez sin poder scrollear. */
  padding: 20px 0;
}
.aviso-acciones-backdrop {
  position: absolute; inset: 0;
  background: rgba(16, 22, 54, .55);
  backdrop-filter: blur(2px);
}
/* Las MISMAS MEDIDAS que la tarjeta de los modales `.hoja-*` a los que
 * lleva cada ítem, en las dos anchuras: en teléfono toma la pantalla
 * entera (tope de 480, el ancho del `.app-shell`) y de 640 para arriba se
 * planta en 420 en el medio. El menú y el modal que abre son la misma
 * caja en dos pasos, y eso incluye cuánto miden.
 *
 * En teléfono no es sólo coherencia: los rótulos de los cuatro de abajo
 * viven en media caja, y el más largo —"Hacer una oferta"— necesita cada
 * píxel que haya. */
.aviso-acciones-panel {
  position: relative;
  width: 100%;
  max-width: 480px;
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: 0 18px 50px rgba(16, 22, 54, .32);
  /* El MISMO margen interno que la tarjeta de los modales `.hoja-*` a los
   * que lleva cada ítem (ver `.hoja-card`): el menú y el modal que abre
   * son la misma caja en dos pasos, y si el contenido de uno arranca
   * pegado al borde y el del otro no, el segundo paso se lee como otra
   * pantalla y no como la continuación del primero.
   *
   * Sin el `env(safe-area-inset-bottom)` que sí lleva `.hoja-card`: eso
   * es de una hoja pegada al pie de la pantalla, y este panel va
   * centrado. */
  padding: 16px 18px 20px;
  overflow: hidden;
  animation: acciones-pop .16s ease-out;
  /* Dos columnas: "Coordinar compra" ocupa las dos y los otros cuatro van
   * de a uno. La jerarquía la dice la FORMA y no el rótulo — en una lista
   * de cinco renglones iguales hay que leerlos todos para saber cuál es
   * el importante (ver `appAvisoAccionesMenu()` en lib/acciones.php).
   *
   * Un ítem oculto no deja hueco: `display: none` lo saca de la grilla y
   * los de abajo se reacomodan solos. Eso importa porque los cuatro de
   * abajo se muestran u ocultan por card según los módulos de la cuenta
   * dueña del aviso. */
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Aire parejo en las dos direcciones, y el mismo que hay entre la tira
   * y el primer botón: cinco rectángulos casi pegados se leen como un
   * bloque partido por ranuras, y lo que se elige acá es UNO.
   *
   * Es el aire ENTRE botones y nada más: a qué distancia quedan del
   * borde de la caja lo fija el `padding` del panel, arriba. */
  gap: 16px;
}
/* La tira va de lado a lado: es de qué aviso habla todo el menú, no una
 * opción más. El `role="presentation"` que la envuelve es el mismo que
 * la saca de la cuenta de ítems de un lector de pantalla.
 *
 * El aire de abajo lo pone entero el `gap` — sin margen propio, que se
 * le sumaría y la dejaría más lejos de los botones que ellos entre sí.
 * Que la tira no se lea como un ítem más de la pila ya lo dice la forma:
 * una tarjeta clara con foto y precio arriba de botones pintados. */
.aviso-acciones-panel > [role="presentation"] {
  grid-column: 1 / -1;
}
/* El mismo corte y los mismos valores que usan `.hoja-modal` / `.hoja-card`
 * para pasar de hoja a tarjeta: de acá para arriba hay pantalla de sobra y
 * un panel de borde a borde sería una tira, no un menú. */
@media (min-width: 640px) {
  .aviso-acciones       { padding: 20px; }
  .aviso-acciones-panel { max-width: 420px; }
}
/* Entra creciendo apenas, para que se lea como que sale del botón que la
 * abrió y no como un cartel que cayó de la nada. */
@keyframes acciones-pop {
  from { opacity: 0; transform: scale(.94); }
  to   { opacity: 1; transform: scale(1); }
}
/* La tira viene con los márgenes que usa dentro del modal, donde cuelga
 * de un encabezado; acá es lo primero de la caja y el aire de arriba lo
 * pone el padding del panel. */
/* El aire de abajo lo pone ahora el `gap` de la grilla; con el margen
 * puesto se sumaban los dos. */
.aviso-acciones .hoja-aviso { margin: 0; }
/* BOTONES de verdad, no renglones de una lista.
 *
 * Hasta acá eran texto suelto alineado a la izquierda sobre fondo
 * transparente, que es la forma de un menú contextual de escritorio y la
 * única del portal que se veía así. Ahora usan el mismo lenguaje que los
 * `.auth-btn` del resto de las pantallas —pintados, centrados, radio
 * grande, cuerpo en 700— y sobre todo la misma PAREJA DE TONOS que ya
 * usan los modales a los que este menú lleva: el naranja para lo único
 * que la pantalla propone hacer, y el gris claro para lo que lo acompaña
 * (ver `.auth-btn` y `.auth-btn-gris-claro`).
 *
 * Los valores no se copian a ojo: son los mismos de esas dos clases. No
 * se reusan por `class` porque `.auth-btn` trae `margin-top` y un ancho
 * pensado para apilarse en una tarjeta de sesión, y acá el que manda es
 * el `gap` de la grilla. */
.aviso-acciones button {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%;
  min-width: 0;          /* sin esto el texto largo desborda la columna */
  /* Más altos que un `.auth-btn` de formulario: acá están pegados de a
   * dos y uno arriba del otro, y lo que separa a cada uno del que sigue
   * son 6px. El cuerpo del botón es entonces lo que se ve, y un botón
   * flaco en esa pila se lee como un renglón de lista. De paso es el
   * blanco que se toca con el pulgar en un teléfono. */
  padding: 17px 10px;
  border: none;
  border-radius: var(--radius-md);
  background: #f1f5f9;
  /* Una sombra suave en lugar de un contorno: dice dónde empieza y dónde
   * termina cada botón levantándolo del fondo, que es lo que ya hacen la
   * tarjeta del aviso y el panel entero. Un borde lo dibujaba como una
   * caja y metía en el menú una línea que no existe en ninguna otra
   * pantalla del portal.
   *
   * Misma tinta que la sombra del panel —el azul oscuro de la marca y no
   * un gris— apenas desplazada hacia abajo y muy corta: tiene que
   * separarlo del fondo, no hacerlo flotar. */
  box-shadow: 0 1px 3px rgba(16, 22, 54, .12);
  color: color-mix(in srgb, var(--muted) 70%, #000);
  font-family: inherit;
  font-size: .9rem;
  font-weight: 700;
  text-align: center;
  /* A media caja "Hacer una oferta" entra justo, y en una pantalla más
   * angosta o con el cuerpo de letra agrandado deja de entrar. Con dos
   * renglones permitidos se parte y la grilla empareja el alto de las dos
   * celdas de esa fila, que es mejor que recortar el rótulo o achicar el
   * cuerpo. */
  white-space: normal;
  line-height: 1.25;
  transition: background .15s;
}
.aviso-acciones button[hidden] { display: none !important; }
.aviso-acciones button:hover   { background: #e2e8f0; }
/* El ancho entero Y el naranja del portal: es la única acción del menú
 * que arranca de un acuerdo ya hecho, y con los otros cuatro en gris es
 * el único que se lee como "esto es lo que hay que hacer acá".
 *
 * Es el mismo `.auth-btn` pelado que llevan los botones de enviar de los
 * cinco modales, así que el naranja significa lo mismo en los dos pasos
 * del gesto. */
.aviso-acciones button[data-accion="compra"] {
  grid-column: 1 / -1;
  background: var(--acento);
  color: #fff;
}
.aviso-acciones button[data-accion="compra"]:hover { background: var(--acento-h); }
/* El glifo va en el acento sobre los grises y hereda el blanco sobre el
 * naranja — un naranja sobre naranja no se vería. Sin ancho fijo: eso
 * servía para alinear los íconos de una lista, y en un botón centrado
 * sólo mete aire muerto entre el glifo y el rótulo. */
.aviso-acciones button i {
  flex: none;            /* el glifo no se achica cuando el rótulo se parte */
  font-size: 1rem;
  color: var(--acento);
}
.aviso-acciones button[data-accion="compra"] i { color: inherit; }
/* "Gestionar servicios" tuvo acá un `grid-column: 1 / -1` que lo mandaba a
 * lo ancho, abajo de todo. Era una solución de PARIDAD y no de jerarquía:
 * con cinco ítems abajo del ancho, la grilla de dos columnas dejaba al
 * último solo a media caja, que se lee como si faltara algo.
 *
 * Los cinco pasaron a ser cuatro cuando el favorito perdió su ítem —lo
 * hace el corazón de la card, ver `appAvisoAccionesMenu()` en
 * lib/acciones.php—, así que la grilla cierra sola y servicios entró en el
 * casillero que quedó libre. Sigue en gris como los otros tres: el naranja
 * es de "Coordinar compra", la única acción que la pantalla propone. */
/* Pulso al tocar el corazón — feedback inmediato, antes de que responda el
 * servidor. */
.corazon-pulse { animation: corazon-pop .32s ease; }
@keyframes corazon-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.28); }
  100% { transform: scale(1); }
}

.aviso-body { padding: 12px 14px 14px; }
.aviso-title {
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.25;
  margin-bottom: 6px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.aviso-precio {
  color: var(--acento);
  font-weight: 700;
  font-size: 1.05rem;
  margin-bottom: 4px;
}
.aviso-precio .per { color: var(--muted); font-weight: 500; font-size: 0.75rem; }

.aviso-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 12px;
  color: var(--muted);
  font-size: 0.8rem;
  margin-top: 6px;
}
.aviso-meta .dot { color: #cfd3e5; }

.aviso-badges {
  display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap;
}
.aviso-badge {
  padding: 3px 10px;
  border-radius: 6px;
  font-size: 0.72rem;
  font-weight: 600;
}
.aviso-badge.op-venta    { background: var(--primario); color: #fff; }
.aviso-badge.op-alquiler { background: var(--mint-soft); color: #128473; }
.aviso-badge.tipo        { background: var(--lilac); color: #6b5cb8; }

/* Tarjeta más chica horizontal (jobs / listado angosto). */
.aviso-row {
  display: grid;
  grid-template-columns: 84px 1fr;
  gap: 12px;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  align-items: center;
  color: var(--text);
}
.aviso-row-thumb {
  width: 84px; height: 84px;
  border-radius: var(--radius-sm);
  background: #eef0fa center/cover no-repeat;
  overflow: hidden;
}
.aviso-row-thumb img { width: 100%; height: 100%; object-fit: cover; }
.aviso-row-body { min-width: 0; }
.aviso-row-title {
  font-weight: 700; font-size: 0.95rem;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* --------- CTA banner (Post a Job / Publica tu aviso) --------- */

.cta-panel {
  margin: 22px 10px 4px;
  background: var(--primario);
  color: #fff;
  border-radius: var(--radius-lg);
  padding: 18px;
  text-align: center;
  position: relative;
  overflow: hidden;
}
.cta-panel::after {
  content: '';
  position: absolute;
  right: -25px; bottom: -25px;
  width: 60px; height: 60px;
  background: #f0b344;
  border-radius: 50%;
  opacity: .3;
}
.cta-title { font-weight: 600; margin-bottom: 12px; font-size: 0.95rem; }
.cta-btn {
  display: inline-block;
  background: var(--acento);
  color: #fff;
  padding: 10px 30px;
  border-radius: 99px;
  font-weight: 700;
  border: none;
  text-decoration: none;
  font-size: 0.9rem;
}
.cta-btn:hover { background: var(--acento-h); }

/* --------- filtros / listado --------- */

.list-toolbar {
  display: flex;
  gap: 8px;
  padding: 14px 10px 4px;
  align-items: center;
  overflow-x: auto;
  scrollbar-width: none;
}
.list-toolbar::-webkit-scrollbar { display: none; }
.list-toolbar .btn-toolbar {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 8px 14px;
  border-radius: 12px;
  font-size: 0.82rem;
  font-weight: 600;
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap;
  cursor: pointer;
}
.list-toolbar .btn-toolbar.active {
  background: var(--primario); color: #fff; border-color: var(--primario);
}

/* empty state (No results — como el mockup) */
.empty-state {
  padding: 60px 30px;
  text-align: center;
  color: var(--muted);
}
.empty-state h3 {
  color: var(--text);
  margin-bottom: 6px;
  font-size: 1rem;
  font-weight: 700;
}
.empty-state p { font-size: 0.85rem; margin-bottom: 12px; }
/* El `:not()` no es decorativo: sin él esta regla —que es más específica
 * que `.auth-btn`— le pintaba el texto de acento a la salida del estado
 * vacío, y quedaba naranja sobre el azul del botón. El acento es para el
 * link pelado; el botón trae su propio color de texto. */
.empty-state a:not(.auth-btn) { color: var(--acento); font-weight: 600; }

/* La salida del estado vacío, cuando es un botón y no un link, va en
 * `--primario` en cualquier página. Es la misma acción en todas —"andá a
 * hacer lo que todavía no hiciste"— y tiene que verse igual en las dos
 * que hoy la tienen: /mi-cuenta (que ya la pintaba así por la regla de
 * `.page-cuenta`) y /favoritos, que corre bajo `page-listado` y sin esto
 * saldría de acento.
 *
 * Y va A MEDIDA DE SU TEXTO, no al ancho del contenedor: `.auth-btn` nace
 * con `width: 100%` porque su lugar natural es apilado al pie de un
 * formulario, donde estirar es lo correcto. Acá no hay formulario —hay un
 * bloque centrado con un ícono, un título y una frase— y una barra de
 * lado a lado no se lee como el remate de ese bloque sino como otra
 * sección. El centrado ya lo da el `text-align: center` de `.empty-state`
 * sobre una caja inline-flex; no hace falta `margin: auto`. */
.empty-state .auth-btn        { background: var(--primario);
                                width: auto; padding-inline: 26px; }
.empty-state .auth-btn:hover  { background: var(--primario-d); }

/* --------- detalle --------- */

/* Wrapper que contiene todo el aviso (foto + galería + título + ficha +
 * publicado por). Se lee como una .aviso-card "abierta" al hacer tap:
 * misma superficie blanca, mismas esquinas redondeadas y misma sombra
 * suave que las cards del listado. */
.detalle-card {
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: 0 0 24px rgba(16, 22, 54, 0.18),
              0 0 6px  rgba(16, 22, 54, 0.08);
  overflow: hidden;
  margin: 12px 10px 0;
  padding: 3px;
}

.detalle-hero {
  background: var(--surface);
  overflow: hidden;
  position: relative;
  border-radius: var(--radius-sm);
  aspect-ratio: 16 / 10;
  touch-action: pan-y;
}
.detalle-hero > img {
  /* Compat: si no hay `.media-strip` (edge case sin fotos múltiples), la
   * imagen suelta ocupa todo el hero como antes. */
  width: 100%; height: 100%; object-fit: cover; background: #eef0fa;
}
.detalle-hero .media-strip img { background: #eef0fa; }
/* El corazón del hero: el mismo de la card en la medida grande, y con la
 * misma regla —pastilla blanca en los dos estados, el relleno del glifo es
 * lo que dice si está en favoritos— (ver la nota de `.aviso-corazon`). */
.detalle-corazon-lg {
  position: absolute; top: 12px; right: 12px;
  width: 36px; height: 36px;
  background: rgba(255,255,255,.9);
  color: var(--acento);
  border: none;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
}
/* Hamburguesa de acciones del hero — la misma pastilla que el corazón y
 * justo debajo (12px del borde + 36px del corazón + 6px de aire). Es el
 * par que ya llevan las cards del listado (`.aviso-corazon` +
 * `.aviso-menu`), en la medida grande de la ficha. Abierto se invierte
 * igual que allá. */
.detalle-menu-lg {
  position: absolute; top: 54px; right: 12px;
  width: 36px; height: 36px;
  background: rgba(255,255,255,.9);
  color: var(--acento);
  border: none;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.95rem;
  z-index: 4;   /* por encima de .media-dots, igual que en la card */
}
.detalle-menu-lg[aria-expanded="true"] { background: var(--acento); color: #fff; }
/* Anterior / siguiente del hero — la misma pastilla blanca del corazón y el
 * hamburguesa, las DOS JUNTAS en la esquina inferior izquierda.
 *
 * Antes iban una en cada costado y a media altura, que es la convención de
 * un carrusel de escritorio. Acá estorbaban: a media altura caen justo
 * sobre el centro de la foto —donde está lo que la foto muestra— y una de
 * cada lado obliga a cruzar la pantalla con el pulgar para ir y volver. En
 * la esquina las dos quedan bajo el mismo dedo y despejan la imagen.
 *
 * Esa esquina es la única libre del hero: arriba a la derecha viven el
 * corazón, el hamburguesa y los sellos, y los puntos van centrados al pie.
 * Contra ellos el `z-index: 4` es lo que decide, igual que antes.
 *
 * Las separa 6px, y cada una queda a 12 del borde: es el mismo ritmo que ya
 * usan el corazón y el hamburguesa apilados a la derecha (12 + 36 + 6).
 *
 * La sombra corta las separa del fondo: sobre una foto clara —un cielo, una
 * pared blanca— una pastilla de blanco al 90% se queda sin borde y el botón
 * desaparece. El corazón y el hamburguesa no la necesitan porque siempre
 * caen sobre la esquina, que es donde las fotos suelen tener algo. */
.detalle-nav-lg {
  position: absolute;
  bottom: 12px;
  width: 36px; height: 36px;
  background: rgba(255,255,255,.9);
  color: var(--acento);
  border: none;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.95rem;
  box-shadow: 0 1px 4px rgba(16, 22, 54, .25);
  transition: background .15s;
  z-index: 4;   /* por encima de .media-dots, igual que el hamburguesa */
}
.detalle-nav-lg:hover { background: #fff; }
.detalle-nav-lg.prev { left: 12px; }
.detalle-nav-lg.next { left: 54px; }   /* 12 del borde + 36 del botón + 6 de aire */
/* Los sellos del hero — los mismos cuatro de la card (ver `.aviso-sellos`)
 * en la medida grande de la ficha: a la izquierda del corazón y en su
 * misma fila (12px del borde + 36px del corazón + 6px de aire).
 *
 * Reusa `.aviso-sello` en vez de clonarlo: entre las dos medidas lo único
 * que cambia es el lado del círculo y el cuerpo del glifo. El color, el
 * hover y el apagado son los mismos, y duplicarlos sería el camino más
 * corto a que un día se despinten uno del otro.
 *
 * Por eso este bloque NO toca el fondo, aunque el corazón y el hamburguesa
 * del hero usen `.9` de blanco y el sello se quede con el `.85` de la
 * card: un punto de opacidad no se ve, y para ponerlo habría que repetir
 * acá las cinco reglas de `[data-cara]` con más especificidad —más sus
 * hovers—, que es exactamente la duplicación que este comentario viene
 * evitando. */
.detalle-sellos {
  position: absolute; top: 12px; right: 54px;
  display: flex;
  gap: 6px;
  z-index: 4;
}
.detalle-sellos .aviso-sello {
  width: 36px; height: 36px;
  font-size: 0.95rem;
}
.detalle-body { padding: 16px 14px; }
.detalle-title { font-size: 1.35rem; font-weight: 700; margin-bottom: 6px; }
.detalle-precio { color: var(--acento); font-weight: 700; font-size: 1.35rem; margin-bottom: 12px; }
.detalle-precio .per { color: var(--muted); font-weight: 500; font-size: 0.85rem; }

.detalle-section {
  padding: 0 14px;
  margin-bottom: 16px;
}
.detalle-section h4 {
  font-size: 0.95rem;
  font-weight: 700;
  margin-bottom: 8px;
}
.detalle-section p {
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1.55;
}

.spec-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}
.spec {
  background: var(--bg);
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-size: 0.83rem;
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  gap: 10px;
}
.spec-body { min-width: 0; display: flex; flex-direction: column; justify-content: center; }
.spec .k { color: var(--muted); display: block; margin-bottom: 2px; font-size: 0.75rem; }
.spec .v { color: var(--text); font-weight: 600; }
.spec .spec-icon {
  flex-shrink: 0;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  font-size: 1.75rem;
  line-height: 1;
  color: var(--text);
  opacity: 0.5;
}

.tags-row {
  display: flex; gap: 6px; flex-wrap: wrap;
}
.tag {
  padding: 4px 12px;
  border-radius: 99px;
  font-size: 0.72rem;
  font-weight: 600;
  background: var(--acento-soft);
  color: var(--acento);
}
/* Las variantes .mint / .lilac / .yellow se quitaron: el único lugar que
 * las usaba era el renglón de etiquetas del detalle, y ahí las tres van
 * en el rosa de `.tag`. Si vuelve a hacer falta una etiqueta de otro
 * color, sale de las variables, no de hexadecimales sueltos. */

/* Barra de acciones al final del detalle. Vive dentro de .detalle-card
 * como sección interna: padding propio, sin margen externo. */
.detalle-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  padding: 8px 14px 16px;
}
/* `a` y `button` juntos: los dos botones son <button> desde que dejaron de
 * navegar y pasaron a abrir un modal (ver detalle.php), pero el selector
 * cubre también el enlace por si vuelve a haber una acción que navegue.
 * Los dos tienen que verse igual. */
.detalle-actions a,
.detalle-actions button {
  padding: 14px 16px;
  text-align: center;
  font-weight: 700;
  font-size: 0.95rem;
  font-family: inherit;
  border: none;
  border-radius: var(--radius-md);
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  cursor: pointer;
  /* TODA la barra en el acento. Son los gestos que la ficha propone —lo
   * único que se puede hacer con el aviso además de mirarlo— y van todos
   * del mismo color para que se lean como una sola botonera.
   *
   * Hubo una etapa con "Comprar" naranja y los otros cuatro en gris
   * claro, copiando la pareja de tonos del menú del hamburguesa (ver
   * `.aviso-acciones button`), con la idea de que un solo naranja marcara
   * el gesto principal. El gris terminó pesando más de lo que marcaba:
   * al pie de la ficha, cuatro bloques apagados se leen como deshabilitados
   * antes que como acciones. "Comprar" no necesita el color para
   * destacarse — toma la fila entera él solo, que es una diferencia más
   * grande que el tono.
   *
   * Esto deja la barra distinta del menú del hamburguesa, que sigue con
   * uno naranja y el resto grises. */
  background: var(--acento);
  color: #fff;
  /* La misma sombra corta de siempre: con los botones separados por 10px
   * de blanco, es lo que les da borde contra la tarjeta de la ficha. */
  box-shadow: 0 1px 3px rgba(16, 22, 54, .12);
  transition: background .15s;
}
.detalle-actions a:hover,
.detalle-actions button:hover { background: var(--acento-h); }
/* "Comprar" toma la fila entera, igual que "Coordinar compra" encabeza el
 * menú del hamburguesa (ver `.aviso-acciones-panel`): es el único de los
 * cinco que asume que ya hay acuerdo. Ahora que el color es el mismo para
 * todos, el ancho es lo único que lo señala como el gesto que la pantalla
 * propone.
 *
 * De paso deja los otros cuatro en un 2×2 exacto debajo. */
.detalle-actions .btn-comprar {
  grid-column: 1 / -1;
}
/* El glifo hereda el blanco del rótulo: un naranja sobre naranja no se
 * vería. */
.detalle-actions a i,
.detalle-actions button i {
  flex: none;            /* el glifo no se achica cuando el rótulo se parte */
  color: inherit;
}
/* La grilla es de dos columnas fijas y los botones entran en el orden en
 * que los dibuja detalle.php: comprar a lo ancho, visitar y ofertar
 * debajo, conversar y favorito al final. Tres en una fila de 480px dejarían
 * botones de 150px con ícono y texto adentro, que es donde el rótulo
 * empieza a partirse.
 *
 * Cuando la cuenta del aviso no tiene los módulos quedan menos y el
 * último se puede quedar sin compañero: a media fila se leería como si
 * faltara algo, así que toma la fila entera. Sale de la posición del botón
 * y no de una clase que cuente las acciones desde PHP: la barra se
 * reacomoda sola el día que se agregue o se saque una.
 *
 * Son tres reglas y no una porque "Comprar" ya ocupa una fila él solo, y
 * eso DA VUELTA la paridad de los que vienen atrás: sin él el suelto es el
 * impar, con él es el par. Las dos de abajo corrigen las dos mitades de esa
 * vuelta. El `~` no alcanza al propio "Comprar" —nadie es hermano de sí
 * mismo— así que ninguna le pisa su fila entera. */
.detalle-actions > :last-child:nth-child(odd) { grid-column: 1 / -1; }
.detalle-actions > .btn-comprar ~ :last-child:nth-child(odd)  { grid-column: auto; }
.detalle-actions > .btn-comprar ~ :last-child:nth-child(even) { grid-column: 1 / -1; }

/* Los servicios que se pueden contratar sobre el aviso, debajo del botón
 * que los abre (ver detalle.php).
 *
 * UNA sola caja gris con todo adentro —el título y los renglones— y no
 * una tarjeta por servicio: son un bloque, y en gris sobre el blanco de
 * la ficha se lee como una unidad separada de los botones de arriba. Es
 * el mismo `--bg` de las especificaciones del aviso, así que la ficha
 * sigue teniendo dos superficies y no tres.
 *
 * Una columna y no la grilla de dos de la barra de acciones: cada renglón
 * lleva un párrafo de descripción, y a media pantalla se parte en cinco
 * líneas. */
.detalle-servicios {
  background: var(--bg);
  border-radius: var(--radius-md);
  padding: 14px;
}
/* El título adentro de la caja. Pierde el margen de `.detalle-section h4`
 * y gana el suyo: lo que lo separa de la lista es menos que lo que separa
 * dos servicios entre sí. */
.detalle-servicios h4 { margin-bottom: 10px; }
/* La línea entre servicios. Del color del borde de la paleta y sin el
 * relieve que el navegador le da al `<hr>` por defecto. */
.detalle-servicios-sep {
  border: none;
  border-top: 1px solid var(--border);
  margin: 12px 0;
}
/* El ícono y el nombre en la misma línea, con el glifo alineado a la
 * primera letra: el nombre puede envolver en dos renglones y el ícono
 * tiene que quedar arriba, no centrado contra los dos. */
.detalle-servicio-cab {
  display: flex; align-items: baseline; gap: 10px;
}
/* El glifo en el acento, que es el color de las acciones del portal —los
 * íconos de los botones de arriba son los mismos—, y a ancho fijo para
 * que los nombres arranquen todos en la misma vertical: con el ancho del
 * glifo, una llave inglesa y un megáfono desalinean la columna. */
.detalle-servicio-icono {
  flex-shrink: 0;
  width: 20px; text-align: center;
  font-size: 1rem;
  color: var(--acento);
}
.detalle-servicio-nombre { font-size: .95rem; color: var(--text); }
/* La descripción, encolumnada con el nombre y no con el ícono: los 30px
 * son el ancho del glifo más su separación. Sin eso, el párrafo arranca
 * debajo del ícono y la sangría del nombre se lee como un error.
 *
 * `.detalle-section p` le gana por especificidad —dos clases contra una—
 * así que el tamaño y el interlineado se declaran acá con la misma
 * cuenta: sin esto sale como el párrafo de "Descripción", que es texto de
 * lectura corrida y no un pie de renglón. */
.detalle-servicio-desc {
  margin: 6px 0 0 30px;
  font-size: .85rem;
  line-height: 1.45;
  color: var(--muted);
}

/* --------- galería mini abajo del hero --------- */
/* La tira y el hero son hermanos dentro de `.detalle-card`, así que sin
 * padding lateral las miniaturas arrancan en la misma vertical que la foto
 * grande. Los 14px de antes las corrían hacia adentro y la tira se leía
 * como si estuviera mal encolumnada con el hero.
 *
 * El -2px del margen compensa el borde transparente de cada miniatura (ver
 * más abajo), que mete la imagen 2px hacia adentro de su caja: lo que tiene
 * que alinear con el hero es la foto que se ve, no el recuadro. */
.gallery-strip {
  /* Una sola separación para toda la tira: lo que se ve entre dos
   * miniaturas es lo mismo que se ve entre el hero y la fila. Los dos
   * descuentos salen del borde transparente de 2px de cada miniatura (ver
   * más abajo): entre foto y foto se cruzan dos bordes, entre el hero y la
   * fila uno solo. Tocando `--mini-sep` se mueven las dos juntas. */
  --mini-sep: 6px;
  display: flex; overflow-x: auto;
  gap: calc(var(--mini-sep) - 4px);
  padding: calc(var(--mini-sep) - 2px) 0 12px;
  margin: 0 -2px;
  scrollbar-width: none;
}
.gallery-strip::-webkit-scrollbar { display: none; }
.gallery-strip img {
  width: 72px; height: 72px; object-fit: cover;
  border-radius: var(--radius-sm);
  cursor: pointer;
  border: 2px solid transparent;
}
.gallery-strip img.active { border-color: var(--acento); }

/* --------- bottom nav flotante --------- */

/* Barra pill flotante: separada del borde inferior y con espacio a los
 * dos costados, para que "levite" sobre el contenido. */
.app-nav {
  position: fixed;
  left: 50%;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  width: calc(100% - 20px);
  max-width: 460px;      /* app-shell (480) - 10 - 10 */
  background: var(--surface);
  border-radius: 99px;
  box-shadow: 0 0 32px rgba(16, 22, 54, 0.22),
              0 0 8px  rgba(16, 22, 54, 0.10);
  display: flex;
  padding: 8px;
  z-index: 50;
}
/* El último slot es un <button> cuando hay sesión —abre el cajón de apps
 * en vez de navegar, ver lib/nav.php— así que la regla lleva el reset de
 * botón: sin `font-family: inherit` el navegador le pone la suya y el
 * rótulo sale en otra tipografía que el de los cuatro links de al lado. */
.app-nav-item {
  flex: 1;
  min-width: 0;
  padding: 8px 4px;
  text-align: center;
  color: var(--muted);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  font-size: 0.66rem;
  font-weight: 600;
  text-decoration: none;
  border-radius: 99px;
  transition: color .15s, background .15s;
  background: none;
  border: none;
  font-family: inherit;
  cursor: pointer;
}
.app-nav-item i { font-size: 1.1rem; }
.app-nav-item:hover  { color: var(--text); }
.app-nav-item.active {
  color: #fff;
  background: var(--primario);
}
.app-nav-item.active i { color: #fff; }

/* --------- favoritos --------- */

.detalle-corazon-lg,
.detalle-menu-lg { transition: background .15s, transform .15s; }

/* Hubo acá un `.favs-aviso`, el cartel de "tus favoritos viven en este
 * dispositivo" que la pantalla le mostraba al visitante sin cuenta. Se fue
 * cuando el favorito pasó a avisar por correo: eso pide sesión, así que ya
 * no hay nada guardado contra una cookie que haya que explicar. */

.favoritos-count {
  display: inline-block;
  background: var(--acento-soft);
  color: var(--acento);
  border-radius: 99px;
  padding: 1px 9px;
  font-size: .75rem;
  font-weight: 700;
  margin-left: 6px;
}

/* --------- sesión: ingresar / registrarse / mi cuenta --------- */

/* El shell de las páginas de sesión no necesita el padding-bottom
 * grande de los listados: no hay scroll infinito abajo. */
.app-shell.page-auth {
  padding-bottom: calc(90px + env(safe-area-inset-bottom, 0px));
}

.auth-card {
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: 0 0 24px rgba(16, 22, 54, 0.18),
              0 0 6px  rgba(16, 22, 54, 0.08);
  margin: 14px 10px 0;
  padding: 24px 20px 20px;
}

.auth-intro { text-align: center; margin-bottom: 22px; }
.auth-icon {
  width: 62px; height: 62px;
  margin: 0 auto 12px;
  border-radius: 50%;
  background: var(--primario);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.6rem; font-weight: 700;
}
/* Mismo círculo, pero con la foto de perfil adentro en vez de la inicial.
 * Se saca el fondo navy porque una imagen con transparencia (los PNG de
 * Google los tienen) dejaría ver el color detrás, y `overflow:hidden`
 * recorta la foto contra el borde redondeado. */
.auth-icon-foto { background: none; overflow: hidden; }
.auth-icon-foto img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* En /mi-cuenta el avatar es además el botón que abre el modal de la foto
 * de perfil. La chapita de la cámara es lo único que anuncia que se puede
 * tocar: sin ella el círculo se lee como decoración. */
.auth-icon-btn {
  position: relative;
  border: none;
  padding: 0;
  /* La chapita cuelga del borde del círculo, así que el recorte de
   * .auth-icon-foto no puede quedar acá — lo hace el <img>, que se
   * redondea solo. */
  overflow: visible;
}
.auth-icon-btn img { border-radius: 50%; }
.auth-icon-btn:focus-visible { outline: 2px solid var(--acento); outline-offset: 3px; }
.auth-icon-badge {
  position: absolute;
  right: -2px; bottom: -2px;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--acento);
  color: #fff;
  /* Del color de la tarjeta, no del avatar: es el aro que despega la
   * chapita del círculo que tiene detrás. */
  border: 2px solid var(--surface);
  display: flex; align-items: center; justify-content: center;
  font-size: .66rem;
}

.auth-intro h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 6px; }
.auth-intro p  { font-size: .85rem; color: var(--muted); line-height: 1.45; }

.auth-error {
  display: flex; align-items: center; gap: 8px;
  background: #fee2e2;
  color: #b91c1c;
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  font-size: .85rem;
  font-weight: 600;
  margin-bottom: 16px;
}

/* Confirmación — mismo formato que .auth-error pero en verde. La usa
 * /recuperar cuando ya despachó el mail. `flex-start` y no `center`
 * porque el texto acá ocupa dos o tres líneas y el ícono tiene que
 * quedar a la altura de la primera. */
.auth-ok {
  display: flex; align-items: flex-start; gap: 8px;
  background: var(--mint-soft);
  color: #0f6d5f;
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  font-size: .85rem;
  font-weight: 600;
  line-height: 1.45;
  margin-bottom: 16px;
}

.auth-form   { display: flex; flex-direction: column; gap: 14px; }
/* "¿Olvidaste tu contraseña?" — último hijo del campo de contraseña
 * dentro de .auth-form (columna flex), alineado a la derecha y sin el
 * gap completo de 14px arriba: se lee como parte del campo, no como un
 * bloque aparte. */
.auth-olvide {
  align-self: flex-end;
  margin-top: -6px;
  font-size: .8rem;
  font-weight: 700;
  color: var(--acento);
}
.auth-row    { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.auth-field  { display: flex; flex-direction: column; gap: 6px; }
.auth-field label {
  font-size: .78rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .04em;
}
.auth-field label .req { color: var(--acento); }
.auth-field input {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: .95rem;
  font-family: inherit;
  color: var(--text);
  background: #fff;
  outline: none;
  transition: border-color .15s, box-shadow .15s;
}
.auth-field input:focus {
  border-color: var(--acento);
  box-shadow: 0 0 0 3px var(--acento-ring);
}
.auth-hint { font-size: .74rem; color: var(--muted); }

.auth-btn {
  width: 100%;
  padding: 14px 16px;
  margin-top: 4px;
  border: none;
  border-radius: var(--radius-md);
  background: var(--acento);
  color: #fff;
  font-family: inherit;
  font-size: .95rem;
  font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  transition: background .15s;
}
.auth-btn:hover { background: var(--acento-h); }
.auth-btn-ghost {
  background: var(--bg);
  color: var(--text);
  margin-top: 18px;
}
.auth-btn-ghost:hover { background: var(--canvas); }
/* Variante gris plena, para una acción que no es la primaria pero tampoco
 * es una salida: hoy la usa "Quitar de favoritos" (ver lib/acciones.php), que
 * es lo único que se hace en ese modal una vez que el aviso ya se sigue.
 * Con el ghost claro quedaba al mismo peso que un "Cerrar" y se leía como
 * si no hiciera nada.
 *
 * El gris es el mismo de los botones del pie de la ficha —`--muted`
 * oscurecido al 88% con negro— y no `--muted` pelado por lo mismo que
 * está anotado allá: con blanco encima, el muted queda en 4,3:1 y el mix
 * lo lleva a 5,3:1 sin cambiar de color. */
.auth-btn-gris {
  background: color-mix(in srgb, var(--muted) 88%, #000);
  color: #fff;
}
.auth-btn-gris:hover { background: color-mix(in srgb, var(--muted) 70%, #000); }

/* Gris CLARO: fondo gris claro, texto gris oscuro. Es el "Cerrar" de la
 * cara de estado de visita y oferta (ver `appGestoEstadoCara()` en
 * lib/acciones.php), que convive con la baja en `.auth-btn-danger`.
 *
 * El par funciona por contraste de peso y no de tono: el rojo marca lo
 * único que deshace algo, y este gris deja la salida a la vista sin
 * competirle. Al mismo peso visual —los dos grises, o los dos pintados—
 * "Cerrar" y "Retirar la oferta" se leerían como dos acciones
 * equivalentes, cuando una es irreversible y la otra no hace nada.
 *
 * Va en su propia variante y no en el `.auth-btn-ghost` que usa el
 * "Cerrar" de los carteles verdes porque ése saca sus colores de `--bg` y
 * `--text`, que son lavanda y azul-tinta: acá el gris tiene que ser gris
 * de verdad, y el mismo de la pastilla de estado `fin` que puede estar
 * justo encima.
 *
 * El texto es el gris oscuro del proyecto —`--muted` mezclado con negro,
 * la misma receta que el hover de `.auth-btn-gris`— y sobre ese fondo da
 * 6,7:1, holgado sobre el 4,5:1 que pide AA. */
.auth-btn-gris-claro {
  background: #f1f5f9;
  color: color-mix(in srgb, var(--muted) 70%, #000);
}
.auth-btn-gris-claro:hover { background: #e2e8f0; }

/* DANGER: la baja de un gesto —cancelar una visita, retirar una oferta—.
 *
 * Es la única acción irreversible que el portal ofrece desde un modal: no
 * hay "deshacer la cancelación", y el endpoint sólo la acepta mientras el
 * vendedor no haya respondido (ver `APP_GESTO_EN_PIE` en lib/marcas.php).
 * El rojo es lo que pide el segundo de pausa antes de tocarla, y lo que
 * la separa del "Cerrar" que tiene al lado.
 *
 * La usan los tres botones de baja del portal: cancelar una visita,
 * retirar una oferta y "Quitar de favoritos". Los tres son lo mismo visto
 * desde tres gestos distintos —deshacer algo que se había hecho— y por
 * eso se pintan igual, aunque no todos pesen lo mismo: dar de baja un
 * correo se revierte en un toque y cancelar una visita no. Lo que el rojo
 * marca acá es la CLASE de acción, no su gravedad; que sean iguales es lo
 * que hace que se reconozcan sin leerlas.
 *
 * `.auth-btn-gris` quedó sin usar con este cambio. No se elimina: es una
 * variante genérica y correcta —gris pleno para una acción que no es
 * primaria ni una salida— y el próximo botón de esa clase la va a
 * necesitar. */
.auth-btn-danger {
  background: var(--danger);
  color: #fff;
}
.auth-btn-danger:hover { background: var(--danger-h); }

/* Botón de Google. Blanco con borde y no rosa: las pautas de marca sólo
 * admiten fondo blanco, negro o azul Google detrás del logo, y además
 * deja la acción primaria de la tarjeta (Ingresar / Crear cuenta) como
 * el único botón pintado. */
.auth-google {
  width: 100%;
  padding: 13px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: #fff;
  color: var(--text);
  font-size: .95rem;
  font-weight: 600;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  transition: background .15s, border-color .15s;
}
.auth-google:hover { background: var(--bg); border-color: #d6d9e3; }
.auth-google-logo  { width: 19px; height: 19px; flex-shrink: 0; }

/* Separador "o" entre el ingreso con Google y el de email + contraseña.
 * Las líneas van como pseudoelementos flexibles para que se estiren
 * solas sin importar el ancho del texto ni el de la tarjeta. */
.auth-sep {
  display: flex; align-items: center; gap: 12px;
  margin: 18px 0;
  font-size: .8rem;
  color: var(--muted);
}
.auth-sep::before,
.auth-sep::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

.auth-alt {
  margin-top: 18px;
  text-align: center;
  font-size: .85rem;
  color: var(--muted);
}
.auth-alt a { color: var(--acento); font-weight: 700; }

/* Ficha de datos en /mi-cuenta */
.cuenta-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}
.cuenta-item {
  background: var(--bg);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0;
}
.cuenta-item .k { color: var(--muted); font-size: .72rem; }
.cuenta-item .v { color: var(--text); font-weight: 600; font-size: .85rem; word-break: break-word; }

/* /mi-cuenta va en azul, no en rosa. La página ya es navy de punta a punta
 * (header, avatar, vista grande de la foto) y el rosa quedaba como el único
 * resto de la paleta de los mockups. Se pinta acá, colgando de
 * `.page-cuenta`, y no tocando las reglas base de .auth-*: esas las
 * comparten /ingresar, /registrarse, /recuperar y /restablecer, que siguen
 * con el acento rosa.
 *
 * El `:not()` deja afuera a los botones ghost (Cerrar sesión, Quitar foto),
 * que no son acento: sin él la especificidad de `.page-cuenta .auth-btn`
 * le ganaría a `.auth-btn-ghost` y los pintaría de azul. */
.page-cuenta .auth-btn:not(.auth-btn-ghost)       { background: var(--primario); }
.page-cuenta .auth-btn:not(.auth-btn-ghost):hover { background: var(--primario-d); }
/* La chapita de la cámara queda azul sobre el avatar azul: lo que la
 * despega es el aro blanco que ya trae .auth-icon-badge. */
.page-cuenta .auth-icon-badge                     { background: var(--primario); }
.page-cuenta .auth-icon-btn:focus-visible         { outline-color: var(--primario); }

/* --------- /mi-cuenta: pestañas de sección --------- */

/* --------- /mi-cuenta: listas de actividad --------- */

/* Mismos márgenes laterales que .auth-card: las secciones son hermanas
 * de la ficha de perfil y tienen que alinear con ella al cambiar de
 * pestaña. */
.cuenta-panel { margin: 12px 10px 0; }

.cuenta-panel-head { padding: 4px 4px 12px; }
.cuenta-panel-head h2 {
  font-size: 1.05rem;
  font-weight: 700;
  display: flex; align-items: center; gap: 9px;
  margin-bottom: 5px;
}
.cuenta-panel-head h2 i { color: var(--primario); font-size: .95rem; }
.cuenta-panel-head p    { font-size: .8rem; color: var(--muted); line-height: 1.45; }

.act-list { display: flex; flex-direction: column; gap: 10px; }

/* La fila entera es el link al aviso. Cuando el aviso ya no está
 * publicado se emite el mismo markup SIN `href` — sigue siendo un <a>,
 * pero inerte y sin foco. De ahí que el hover y el cursor cuelguen de
 * `[href]` y no de la clase. */
.act-item {
  display: block;
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: 12px;
  transition: box-shadow .15s, transform .1s;
}
.act-item[href]:hover  { box-shadow: var(--shadow-md); }
.act-item[href]:active { transform: scale(.995); }

.act-top { display: flex; align-items: center; gap: 10px; }

.act-foto {
  width: 56px; height: 56px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
}
.act-foto img { width: 100%; height: 100%; object-fit: cover; }

/* `min-width: 0` es lo que habilita el recorte del título: sin eso el
 * hijo flex no baja de su ancho de contenido y el ellipsis nunca entra. */
.act-main { flex: 1; min-width: 0; }
.act-titulo {
  font-size: .9rem; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.act-sub {
  margin-top: 2px;
  font-size: .76rem; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.act-off {
  margin-top: 3px;
  display: flex; align-items: center; gap: 5px;
  font-size: .72rem; font-weight: 600; color: #b91c1c;
}

/* Chapita de estado. Los cinco tonos los reparte appCuentaEstado() y
 * valen igual en las cuatro listas: el mismo color dice lo mismo. */
.act-badge {
  flex-shrink: 0;
  align-self: flex-start;
  padding: 3px 10px;
  border-radius: 99px;
  font-size: .68rem;
  font-weight: 700;
  white-space: nowrap;
}
.act-badge.is-ok   { background: var(--mint-soft);  color: #0f6d5f; }
.act-badge.is-wait { background: var(--yellow-soft); color: #a16207; }
.act-badge.is-info { background: #e0edff;           color: #1d4ed8; }
.act-badge.is-no   { background: #fee2e2;           color: #b91c1c; }
.act-badge.is-off  { background: var(--bg);         color: var(--muted); }

/* Lo propio de cada gesto, debajo de una línea que lo separa del aviso:
 * arriba está de qué aviso se trata, abajo qué hiciste sobre él. */
.act-datos {
  margin-top: 11px;
  padding-top: 11px;
  border-top: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 7px;
}
.act-dato {
  display: flex; align-items: center; gap: 8px;
  font-size: .79rem; color: var(--muted);
}
/* Ancho fijo para que los renglones alineen aunque los glifos midan
 * distinto (un calendario es más ancho que un avión de papel). */
.act-dato i { width: 14px; text-align: center; flex-shrink: 0; }
.act-dato-fuerte { color: var(--text); font-weight: 600; font-size: .85rem; }
.act-dato-fuerte i { color: var(--primario); }

.act-monto {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 1.1rem; font-weight: 700; color: var(--primario);
}
.act-monto-k {
  font-size: .72rem; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
}

/* Texto que escribió la persona (el mensaje de la visita). La barra a la
 * izquierda lo marca como una cita sin tener que meterle comillas al
 * contenido. */
.act-nota {
  border-left: 3px solid var(--border);
  padding-left: 10px;
  font-size: .8rem; color: var(--text); line-height: 1.45;
  white-space: pre-line;      /* respeta los saltos que la persona tipeó */
  word-break: break-word;
}

/* Lo que contestó el vendedor. En verde y en bloque propio: es la
 * novedad de la fila, lo único que cambió desde que se mandó.
 *
 * El `pre-line` cuelga del valor y no del contenedor: preserva los saltos
 * que escribió el vendedor, y en el contenedor también preservaría los
 * del código fuente —la sangría del PHP entre el rótulo y el texto. */
.act-respuesta {
  background: var(--mint-soft);
  border-radius: var(--radius-sm);
  padding: 9px 11px;
  font-size: .8rem; color: #0f5a4e; line-height: 1.45;
}
.act-respuesta-k {
  display: block;
  margin-bottom: 3px;
  font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em;
  opacity: .75;
}
.act-respuesta-v {
  display: block;
  white-space: pre-line;
  word-break: break-word;
}

.act-tope {
  margin-top: 12px;
  text-align: center;
  font-size: .75rem; color: var(--muted);
}

/* --------- pie de acciones de una fila (Mis radares) --------- */

/* Las otras cuatro listas de /mi-cuenta no lo tienen: ahí la fila ENTERA
 * es el gesto —lleva al aviso o abre el modal— y no hay nada más que
 * hacerle. Un radar es una suscripción, así que lo que se viene a hacer
 * con él son tres cosas distintas (mirar, pausar, borrar) y ninguna es
 * "abrir la fila".
 *
 * Cada acción viene envuelta en su propio `<form>` POST, así que la fila
 * se arma con los formularios en `display: contents`: sin eso cada uno
 * sería una caja de bloque y los botones caerían uno abajo del otro. */
.act-acciones {
  margin-top: 11px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  display: flex; flex-wrap: wrap; gap: 8px;
}
.act-accion-form { display: contents; }

/* Se usa en `<a>` y en `<button>`, así que hay que desandar lo que el
 * navegador le pone al botón (fuente propia, borde, fondo). */
.act-accion {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: 99px;
  background: var(--surface);
  font: inherit;
  font-size: .78rem; font-weight: 600;
  color: var(--text);
  cursor: pointer;
  transition: border-color .15s, color .15s, background .15s;
}
.act-accion:hover { border-color: var(--primario); color: var(--primario); }
.act-accion i     { font-size: .8rem; }

/* La destructiva se pinta recién en el hover: en reposo es una opción
 * más de tres, y un rojo permanente en una lista de suscripciones grita
 * más de lo que la acción merece. */
.act-accion-baja:hover { border-color: #b91c1c; color: #b91c1c; }

/* --------- bandeja de conversaciones --------- */

/* La fila de un hilo es un `<button>` y no un `<a>`: lo que abre es el
 * hilo en un modal, no una página. De ahí que haya que desandar lo que el
 * navegador le pone a un botón —fondo, borde, fuente propia, texto
 * centrado—; la caja en sí la aporta `.act-item`, que es la misma que usan
 * las otras listas de /mi-cuenta.
 *
 * El hover y el `:active` no cuelgan de `[href]` como allá —acá no hay
 * href— pero valen lo mismo: siempre se puede tocar. */
.conv-item {
  width: 100%;
  border: none;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.conv-item:hover  { box-shadow: var(--shadow-md); }
.conv-item:active { transform: scale(.995); }
.conv-item:focus-visible { outline: 2px solid var(--acento); outline-offset: 2px; }

/* Hilo con mensajes sin leer: una barra del acento pegada al borde
 * izquierdo. No un fondo pintado, porque la fila ya lleva la chapita del
 * rol arriba y el contador abajo — una tercera mancha de color la
 * convertiría en un semáforo. */
.conv-item.is-sinleer { box-shadow: var(--shadow-sm), inset 3px 0 0 var(--acento); }

/* Lo último que se dijo, en UN renglón: el recorte con elipsis es lo que
 * mantiene todas las filas del mismo alto. Los saltos de línea del
 * mensaje ya vienen aplastados del servidor (`appConvPreview()`), así que
 * acá alcanza con el `nowrap`. */
.conv-ultimo {
  font-size: .8rem; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.conv-ultimo-k { font-weight: 700; color: var(--muted); }

/* El renglón de la fecha, con el contador de sin leer empujado al otro
 * extremo. `flex-wrap` para que en una pantalla angosta el contador baje
 * en vez de apretar la fecha. */
.conv-pie { flex-wrap: wrap; }
.conv-pill {
  margin-left: auto;
  padding: 2px 9px;
  border-radius: 99px;
  background: var(--acento);
  color: #fff;
  font-size: .68rem; font-weight: 700;
  white-space: nowrap;
}

/* --------- toast --------- */

/* Aviso efímero. Hoy lo usa el fallback de compartir cuando el navegador
 * no tiene la hoja nativa del sistema y sólo queda copiar el link. */
.app-toast {
  position: fixed;
  left: 50%;
  bottom: calc(90px + env(safe-area-inset-bottom, 0px));  /* arriba del nav flotante */
  transform: translate(-50%, 12px);
  z-index: 300;
  background: var(--text);
  color: #fff;
  padding: 11px 20px;
  border-radius: 99px;
  font-size: .85rem;
  font-weight: 600;
  white-space: nowrap;
  box-shadow: 0 8px 24px rgba(16, 22, 54, .3);
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s, transform .22s;
}
.app-toast.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}
/* En escritorio no hay barra flotante que esquivar. */
.vista-escritorio .app-toast { bottom: 32px; }

/* --------- vista mapa del listado ---------
 *
 * La otra forma de mirar el mismo resultado. Es una caja de alto fijo y
 * no una pantalla completa: arriba quedan el header y el panel de
 * filtros, que es desde donde se vuelve a la lista, y abajo la barra de
 * navegación flotante. Un mapa a pantalla completa obligaría a esconder
 * las dos cosas y a inventar un botón de salida propio.
 *
 * El alto sale del viewport menos lo que ocupa esa pila. `dvh` y no `vh`:
 * en el navegador del teléfono la barra de direcciones entra y sale, y
 * con `vh` el mapa queda tapado por abajo justo cuando está desplegada.
 */
.mapa-vista {
  position: relative;
  margin: 0 10px;
}
.mapa-lienzo {
  /* 350px = el header azul con su buscador + la franja de avisos + la
   * barra de navegación flotante del pie. Es lo que hace que la
   * atribución de Google, que va en el borde de abajo del lienzo, no
   * quede detrás de la barra.
   *
   * En pantallas cortas manda el `min-height` y el pie sí se mete debajo
   * de la barra: es preferible a una franja de mapa de 150px donde no
   * entra ni una manzana. */
  height: calc(100dvh - 350px);
  min-height: 340px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--canvas);
  box-shadow: var(--shadow-md);
}
/* Sin un solo pin el mapa NO se emite: listado.php pinta un
 * `.empty-state` en su lugar. Un lienzo gris vacío no dice nada y encima
 * se confunde con un mapa que no cargó. */
.mapa-error {
  padding: 24px;
  text-align: center;
  font-size: .9rem;
  color: var(--muted);
  line-height: 1.5;
}

/* La franja de lo que el mapa NO muestra. Va ARRIBA del lienzo y no
 * flotando encima: es una advertencia sobre el mapa entero, no sobre un
 * punto de él, y encima taparía pines.
 *
 * Siempre en ámbar, porque siempre dice lo mismo: que lo que se está
 * viendo no es todo. Cuando no falta nada, la franja no existe — no hay
 * variante informativa, y por eso tampoco hay clases de tono. El par de
 * colores es el mismo de `.act-badge.is-wait`, que es el "esto está
 * pendiente" del portal. */
.mapa-aviso {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 10px;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  font-size: .82rem;
  line-height: 1.45;
  background: var(--yellow-soft);
  color: #a16207;
}
.mapa-aviso[hidden] { display: none; }
.mapa-aviso i { margin-top: 2px; flex-shrink: 0; }

/* La tarjeta del pin elegido. NO va en una esquina: cuelga del pin que se
 * tocó, arriba de él y con un pico que lo señala.
 *
 * El pico es lo único que contesta "¿de cuál de todos estos pines me estás
 * hablando?", y sin esa respuesta la tarjeta es ambigua en cuanto hay más
 * de un pin a la vista — que es el caso normal. `left` y `top` de la
 * tarjeta Y del pico los escribe assets/js/mapa.js en cada movimiento del
 * mapa; el del pico sigue al PIN y no al centro de la tarjeta.
 *
 * ═══ FOTO AL COSTADO, PEGADA AL BORDE ═══
 *
 * Es la misma forma que la vista Mapa del back office (`.mapa-card` en
 * panel/assets/css/panel.css) y las medidas son las de allá. El mapa es
 * una caja apaisada —mucho más ancha que alta— y el alto es lo escaso: con
 * la foto al costado y sin margen alrededor, la tarjeta mide ~120px en vez
 * de ~290px y deja ver el mapa que está tapando. El único filete que
 * separa la foto de los datos es 1px, el mismo del borde de la tarjeta.
 *
 * La foto es CUADRADA y de ahí sale el alto: el lado se declara una sola
 * vez en `--foto` y lo usan el ancho de la imagen y el alto del cuerpo,
 * así no se pueden desincronizar. */
.mapa-card {
  --foto: 104px;
  position: absolute;
  left: 0;
  top: 0;
  width: min(330px, calc(100% - 20px));
  z-index: 5;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.mapa-card[hidden] { display: none; }

/* El pico, en dos triángulos apilados: es la única forma de darle el mismo
 * filete de 1px que tiene la tarjeta — el de atrás en el color del borde,
 * el de adelante un poco más chico y 1px más arriba, en el color del fondo
 * de la tarjeta, que es de donde el pico sale. Un solo triángulo blanco se
 * lee como un pegote suelto contra el gris del mapa. */
.mapa-card-pico {
  position: absolute;
  z-index: 6;
  width: 18px;
  height: 10px;
  pointer-events: none;   /* es decoración: no se clickea ni tapa el mapa */
}
.mapa-card-pico[hidden] { display: none; }
.mapa-card-pico::before,
.mapa-card-pico::after {
  content: '';
  position: absolute;
  border-left-color: transparent;
  border-right-color: transparent;
  border-left-style: solid;
  border-right-style: solid;
  border-top-style: solid;
}
.mapa-card-pico::before {
  top: 0; left: 0;
  border-left-width: 9px; border-right-width: 9px;
  border-top-width: 10px; border-top-color: var(--border);
}
.mapa-card-pico::after {
  top: -1px; left: 1px;
  border-left-width: 8px; border-right-width: 8px;
  border-top-width: 9px; border-top-color: var(--surface);
}

.mapa-card-body {
  display: flex;
  align-items: stretch;
  /* `min-height` y no `height`: en el caso raro de "Alquiler temporal" +
   * "Departamento" las dos píldoras no entran en un renglón, y con alto
   * fijo la segunda línea empujaría el precio fuera del recorte. Así la
   * tarjeta crece 20px en vez de esconder el dato.
   *
   * Lo que NO puede hacer es dejar que el alto lo ponga la foto — ver la
   * nota de `.mapa-card-foto`. */
  min-height: var(--foto);
  text-decoration: none;
  color: inherit;
}
.mapa-card-foto {
  display: block;
  flex-shrink: 0;
  /* CUADRADA, y el lado va declarado en los dos ejes.
   *
   * No alcanza con `width` + `align-self: stretch`: así el alto lo termina
   * poniendo la PROPIA FOTO —una vertical de 600×800 mide 160px de alto a
   * 120 de ancho— y la tarjeta crece hasta ahí arrastrando la imagen con
   * ella. En el back office no pasa porque el cuerpo lleva `height` fijo;
   * acá el cuerpo puede crecer, así que el cuadrado se declara en la foto
   * y no se delega en el contenedor. El `object-fit: cover` es el que
   * recorta lo que sobra sin deformar nada. */
  width: var(--foto);
  height: var(--foto);
  object-fit: cover;
  background: var(--canvas);
  /* Filete BLANCO, del color del fondo de la tarjeta, no gris: separa la
   * foto de los datos sin dibujar una división. Una línea gris ahí parte
   * la tarjeta en dos mitades que se leen como dos cajas pegadas; en
   * blanco, la foto queda apoyada dentro de la tarjeta y el único borde
   * que se ve es el de afuera. */
  border: 1px solid var(--surface);
}
.mapa-card-foto[hidden] { display: none; }
.mapa-card-datos {
  flex: 1;
  min-width: 0;   /* si no, un título largo estira la tarjeta */
  padding: 11px 13px;
  display: flex;
  flex-direction: column;
  gap: 5px;
  /* Con el título en una sola línea la columna queda más baja que la foto:
   * centrada, el bloque se apoya contra el medio de la imagen en vez de
   * dejar un hueco abajo. */
  justify-content: center;
}

/* Las dos píldoras: QUÉ SE HACE con el aviso y QUÉ ES.
 *
 * Son dos colores distintos a propósito, porque dicen cosas de distinto
 * orden. El reparto es el mismo que ya usa la card del listado: el acento
 * para lo que se puede hacer con el aviso (ver `.aviso-ribbon`) y el de
 * marca para clasificarlo (ver `.aviso-mio`). Con las dos en el mismo
 * color se leerían como una sola etiqueta partida en dos.
 *
 * El `padding-right` deja lugar a la ✕, que flota sobre esta esquina. */
.mapa-card-chips {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding-right: 26px;
}
.mapa-card-op,
.mapa-card-tipo {
  border-radius: 99px;
  padding: 2px 9px;
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  white-space: nowrap;
}
.mapa-card-op {
  background: var(--acento-soft);
  color: var(--acento-h);
}
.mapa-card-tipo {
  background: color-mix(in srgb, var(--primario) 13%, #fff);
  color: var(--primario);
}
/* Dos líneas y corta: el título de un aviso puede tener 120 caracteres y
 * entero haría crecer la tarjeta más que la foto que tiene al lado. */
.mapa-card-titulo {
  font-size: .86rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1.32;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.mapa-card-precio {
  color: var(--acento);
  font-size: 1.02rem;
  font-weight: 700;
  line-height: 1.2;
}
.mapa-card-cerrar {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 2;
  width: 26px;
  height: 26px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  font-size: .8rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mapa-card-cerrar:hover { background: var(--bg); color: var(--text); }

/* --------- sentinel de scroll infinito --------- */

.scroll-sentinel {
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 12px;
}
.scroll-spinner {
  width: 22px; height: 22px;
  border: 2px solid var(--border);
  border-top-color: var(--acento);
  border-radius: 50%;
  opacity: 0;
  animation: scroll-spin .8s linear infinite;
  transition: opacity .15s;
}
.scroll-sentinel.is-loading .scroll-spinner { opacity: 1; }
@keyframes scroll-spin {
  to { transform: rotate(360deg); }
}

/* --------- modal de filtros (listado) --------- */

.filtros-modal[hidden] { display: none !important; }
.filtros-modal {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
.filtros-backdrop {
  position: absolute; inset: 0;
  background: rgba(16, 22, 54, .55);
  backdrop-filter: blur(2px);
}
.filtros-card {
  position: relative;
  background: #fff;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 18px 18px 20px;
  width: 100%;
  max-width: 480px;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 -20px 60px rgba(16, 22, 54, .3);
}
@media (min-width: 640px) {
  .filtros-modal { align-items: center; padding: 20px; }
  .filtros-card { border-radius: var(--radius-lg); max-width: 420px; }
}
.filtros-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.filtros-header h3 {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text);
}
.filtros-close {
  background: var(--bg);
  border: none;
  color: var(--text);
  width: 34px; height: 34px;
  border-radius: 50%;
  font-size: 1rem;
  cursor: pointer;
}
.filtros-close:hover { background: var(--canvas); }

.filtros-section { margin-bottom: 16px; }
.filtros-label {
  display: block;
  font-size: .8rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .05em;
  margin-bottom: 8px;
}
.filtros-form input[type="text"],
.filtros-form input[type="number"] {
  width: 100%;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: .95rem;
  font-family: inherit;
  color: var(--text);
  outline: none;
  background: #fff;
}
.filtros-form input[type="text"]:focus,
.filtros-form input[type="number"]:focus { border-color: var(--acento); }

/* --- slider de rango con dos thumbs (precio / año) --- */
.range-slider {
  /* Único lugar donde vive el diámetro del thumb: assets/js/filtros.js lo
   * lee vía `var(--range-thumb)` para alinear el fill con los centros de
   * los thumbs. Si cambia acá, el fill lo sigue solo. */
  --range-thumb: 20px;
  padding: 4px 8px 8px;
}
.range-values {
  display: flex;
  justify-content: space-between;
  font-size: .82rem;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 14px;
}
.range-track-wrap {
  position: relative;
  height: 22px;
}
.range-track {
  position: absolute;
  top: 9px;
  left: 0; right: 0;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
}
.range-track-fill {
  position: absolute;
  top: 0; bottom: 0;
  background: var(--acento);
  border-radius: 2px;
}
.range-input {
  position: absolute;
  top: 0;
  left: 0; right: 0;
  width: 100%;
  height: 22px;
  margin: 0;
  padding: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  pointer-events: none;
  /* Base del apilado. Cuál de los dos queda arriba lo decide filtros.js
   * según dónde esté el thumb bajo — si no, con los thumbs pegados uno
   * de los dos es inagarrable. */
  z-index: 3;
}
.range-input::-webkit-slider-runnable-track {
  height: 22px;
  background: transparent;
  border: none;
}
.range-input::-moz-range-track {
  height: 22px;
  background: transparent;
  border: none;
}
.range-input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  pointer-events: auto;
  width: var(--range-thumb); height: var(--range-thumb);
  border-radius: 50%;
  background: var(--acento);
  border: 2px solid #fff;
  box-shadow: 0 2px 6px rgba(16, 22, 54, .25);
  cursor: pointer;
  margin-top: 0;
}
.range-input::-moz-range-thumb {
  pointer-events: auto;
  width: var(--range-thumb); height: var(--range-thumb);
  border-radius: 50%;
  background: var(--acento);
  border: 2px solid #fff;
  box-shadow: 0 2px 6px rgba(16, 22, 54, .25);
  cursor: pointer;
}
.range-input:focus { outline: none; }

/* Estado "off": el rango cubre todo el espectro, o sea que el filtro no
 * recorta nada. Se ve gris pero sigue siendo interactivo — al mover
 * CUALQUIERA de los dos thumbs hacia adentro se rehabilita solo. */
.range-slider.is-off .range-track-fill { background: #b8bccb; }
.range-slider.is-off .range-input::-webkit-slider-thumb {
  background: #b8bccb;
}
.range-slider.is-off .range-input::-moz-range-thumb {
  background: #b8bccb;
}
.range-slider.is-off .range-values { color: var(--muted); }

.filtros-chips {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.filtros-chip { position: relative; cursor: pointer; }
.filtros-chip input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.filtros-chip span {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 14px;
  border-radius: 99px;
  background: var(--bg);
  color: var(--text);
  font-size: .8rem;
  font-weight: 600;
  transition: background .15s, color .15s;
}
.filtros-chip input:checked + span {
  background: var(--primario);
  color: #fff;
}

.filtros-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 20px;
}
.filtros-btn {
  padding: 12px 16px;
  border-radius: 12px;
  border: none;
  font-weight: 700;
  font-size: .9rem;
  cursor: pointer;
  font-family: inherit;
  text-align: center;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background .15s;
}
.filtros-btn-secondary {
  background: var(--bg);
  color: var(--text);
}
.filtros-btn-secondary:hover { background: var(--canvas); }
.filtros-btn-primary {
  background: var(--acento);
  color: #fff;
}
.filtros-btn-primary:hover { background: var(--acento-h); }

/* --------- modal de instalación PWA --------- */

.pwa-install-modal[hidden] { display: none !important; }
.pwa-install-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.pwa-install-backdrop {
  position: absolute; inset: 0;
  background: rgba(16, 22, 54, .55);
  backdrop-filter: blur(2px);
}
.pwa-install-card {
  position: relative;
  background: #fff;
  border-radius: var(--radius-lg);
  padding: 26px 22px 20px;
  width: 100%;
  max-width: 340px;
  text-align: center;
  box-shadow: 0 20px 60px rgba(16, 22, 54, .35);
}
.pwa-install-close {
  position: absolute;
  top: 10px; right: 10px;
  background: transparent;
  border: none;
  color: var(--muted);
  width: 32px; height: 32px;
  font-size: 1rem;
  border-radius: 50%;
  cursor: pointer;
}
.pwa-install-close:hover { background: var(--bg); }
.pwa-install-icon {
  display: flex; justify-content: center;
  margin-bottom: 14px;
}
.pwa-install-icon img {
  width: 72px; height: 72px;
  border-radius: 18px;
  box-shadow: 0 8px 20px rgba(16, 22, 54, .18);
}
.pwa-install-title {
  font-size: 1.15rem;
  font-weight: 700;
  margin-bottom: 6px;
  color: var(--text);
}
.pwa-install-body {
  font-size: .88rem;
  color: var(--muted);
  line-height: 1.45;
  margin-bottom: 18px;
}
/* Pasos del modo manual — la única parte del modal que no va centrada:
 * es una secuencia que se lee renglón por renglón mientras se hace, y
 * centrada se pierde de dónde arranca cada línea. */
.pwa-install-pasos {
  text-align: left;
  margin: 0 0 18px;
  padding-left: 20px;
  font-size: .88rem;
  color: var(--muted);
  line-height: 1.45;
}
.pwa-install-pasos[hidden] { display: none; }
.pwa-install-pasos li { margin-bottom: 6px; }
.pwa-install-pasos li:last-child { margin-bottom: 0; }
.pwa-install-pasos strong { color: var(--text); }

.pwa-install-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
/* Modo manual: no hay nada que lanzar desde acá, así que queda el botón
 * de cerrar solo y toma la fila entera — a media fila se leería como si
 * faltara el otro. Mismo criterio que el último botón impar de
 * `.detalle-actions`, con la diferencia de que acá la clase la pone el JS
 * (ver lib/pwa.php): esa barra tiene los dos botones siempre en el DOM y
 * esconde uno, así que un selector por posición no los podría contar. */
.pwa-install-actions.una-sola { grid-template-columns: 1fr; }
.pwa-install-btn[hidden] { display: none; }
.pwa-install-btn {
  padding: 12px 16px;
  border-radius: 12px;
  border: none;
  font-weight: 700;
  font-size: .9rem;
  cursor: pointer;
  font-family: inherit;
  transition: background .15s;
}
.pwa-install-btn-secondary {
  background: var(--bg);
  color: var(--text);
}
.pwa-install-btn-secondary:hover { background: var(--canvas); }
.pwa-install-btn-primary {
  background: var(--acento);
  color: #fff;
}
.pwa-install-btn-primary:hover { background: var(--acento-h); }

/* --------- pantalla de "quedó instalada" --------- */

/* Lo que sale cuando la instalación terminó (ver mostrarListo() en
 * lib/pwa.php). No es un modal más: no tiene velo ni tarjeta, es azul de
 * marca y tapa el portal entero. Y es a propósito — dice que de acá en
 * más la app se abre desde el cajón de aplicaciones, o sea que esta
 * pestaña dejó de ser el lugar. Con el portal asomando por los costados
 * se leería como cualquier otro cartel: leelo y seguí acá.
 *
 * Sólo la ve Android; ver el comentario de mostrarListo(). */
.pwa-listo[hidden] { display: none !important; }
.pwa-listo {
  position: fixed;
  inset: 0;
  z-index: 260;   /* sobre el modal de instalación (200) y el de versión nueva (240) */
  background: var(--primario);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 32px 26px calc(32px + env(safe-area-inset-bottom, 0px));
}
/* El portal de atrás no se ve, pero sin esto se sigue pudiendo arrastrar
 * y el azul se despega de los bordes. La clase la pone el JS. */
body.pwa-listo-abierto { overflow: hidden; }
.pwa-listo-inner { max-width: 340px; }
.pwa-listo-icono {
  font-size: 3.4rem;
  line-height: 1;
  margin-bottom: 18px;
}
.pwa-listo-titulo {
  font-size: 1.5rem;
  font-weight: 800;
  margin-bottom: 10px;
  color: #fff;
}
.pwa-listo-body {
  font-size: .95rem;
  line-height: 1.5;
  margin-bottom: 26px;
  color: rgba(255, 255, 255, .92);
}
.pwa-listo-btn {
  background: #fff;
  color: var(--primario);
  border: none;
  border-radius: 12px;
  padding: 12px 30px;
  font-weight: 700;
  font-size: .95rem;
  font-family: inherit;
  cursor: pointer;
}

/* --------- modal de versión nueva --------- */

/* El aviso de "hay una versión nueva" (ver lib/actualizacion.php). Misma
 * mecánica que el modal de instalación —fixed + backdrop + tarjeta
 * centrada— y a propósito: son los dos carteles que el portal levanta por
 * su cuenta, sin que nadie toque nada, y conviene que se lean como el
 * mismo tipo de cosa. Lo propio de acá es el disco del ícono, que
 * reemplaza al logo de la app: esto no es sobre la app como objeto sino
 * sobre una acción, la de recargar.
 *
 * Por encima de todo lo demás (foto de perfil y hojas del aviso van en
 * 220, el corazón de la card en 230) porque puede aparecer con cualquiera
 * de ellos abierto, y debajo del toast (300), que es la única cosa que
 * nunca tiene que quedar tapada. */
.act-modal[hidden] { display: none !important; }
.act-modal {
  position: fixed;
  inset: 0;
  z-index: 240;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.act-backdrop {
  position: absolute; inset: 0;
  background: rgba(16, 22, 54, .55);
  backdrop-filter: blur(2px);
}
.act-card {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 26px 22px 20px;
  width: 100%;
  max-width: 340px;
  text-align: center;
  box-shadow: 0 20px 60px rgba(16, 22, 54, .35);
}

/* Disco naranja con la flecha de recargar. El mismo par de colores que
 * usan los fondos suaves del portal, así el glifo llega a 4,5:1 contra su
 * disco sin salirse de la paleta. */
.act-icono {
  width: 60px; height: 60px;
  margin: 0 auto 14px;
  border-radius: 50%;
  background: var(--acento-soft);
  color: var(--acento-h);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.5rem;
}
.act-titulo {
  font-size: 1.15rem;
  font-weight: 700;
  margin-bottom: 6px;
  color: var(--text);
}
.act-texto {
  font-size: .88rem;
  color: var(--muted);
  line-height: 1.45;
  margin-bottom: 18px;
}

.act-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.act-btn {
  padding: 12px 16px;
  border-radius: 12px;
  border: none;
  font-size: .9rem;
  font-weight: 600;
  font-family: inherit;
  transition: background .15s;
}
.act-btn:disabled { opacity: .65; cursor: default; }
/* El modal se abre solo y le pasa el foco a "Actualizar" (ver
 * assets/js/actualizacion.js), así que el anillo se ve sí o sí: sin esto
 * queda el negro del navegador encima del naranja. Mismo anillo que el
 * resto de los botones del portal (`.auth-icon-btn`), y el offset es lo
 * que lo hace visible sobre el relleno del primario. */
.act-btn:focus-visible { outline: 2px solid var(--acento); outline-offset: 3px; }
.act-btn-secondary {
  background: var(--bg);
  color: var(--text);
}
.act-btn-secondary:hover { background: var(--canvas); }
.act-btn-primary {
  background: var(--acento);
  color: #fff;
}
.act-btn-primary:hover { background: var(--acento-h); }

/* --------- menú de apps --------- */

/* El modal que abre el botón hamburguesa (ver lib/menu-apps.php). Se
 * lee como el cajón de apps de un teléfono: tarjeta blanca, cuadrados
 * grises, íconos gris oscuro y el texto de cada app debajo del ícono.
 *
 * Es el único bloque del portal que no usa la paleta navy/rosa. La
 * escala de grises es a propósito — el menú es chrome del sistema, no
 * contenido, y el gris lo separa de todo lo que sí es un aviso. Los
 * tonos viven acá adentro y no en `:root` porque no los comparte nadie
 * más. */
.menu-apps {
  --ma-card:   #fff;      /* blanco de la tarjeta */
  --ma-tile:   #e6e8ef;   /* gris del cuadrado del ícono */
  --ma-tile-h: #dadde7;
  --ma-ink:    #3a3f52;   /* gris oscuro de íconos y textos */

  position: fixed;
  inset: 0;
  z-index: 210;   /* por encima del modal PWA (200) */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.menu-apps[hidden] { display: none !important; }

.menu-apps-backdrop {
  position: absolute; inset: 0;
  background: rgba(16, 22, 54, .55);
  backdrop-filter: blur(2px);
}

.menu-apps-card {
  position: relative;
  background: var(--ma-card);
  border-radius: var(--radius-lg);
  padding: 16px 18px 22px;
  width: 100%;
  max-width: 360px;
  box-shadow: 0 20px 60px rgba(16, 22, 54, .35);
  /* Con muchas apps la grilla puede pasarse de alto en pantallas bajas
   * (teléfono apaisado): que scrollee la tarjeta, no la página. */
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}

.menu-apps-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.menu-apps-title {
  font-size: 1rem;
  font-weight: 700;
  color: var(--ma-ink);
}
.menu-apps-close {
  background: transparent;
  border: none;
  color: var(--ma-ink);
  opacity: .65;
  width: 32px; height: 32px;
  font-size: 1rem;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
}
.menu-apps-close:hover { background: rgba(16, 22, 54, .08); opacity: 1; }

/* Tres columnas fijas: el shell móvil mide 480px como mucho, así que no
 * hay rango donde convenga otro número, y con 3 el label de dos palabras
 * entra en dos líneas sin cortarse. */
.menu-apps-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px 10px;
}

/* El `display: flex` de abajo le gana al `hidden` del atributo, y hay un
 * ítem que se esconde solo: "Instalar app" cuando la PWA ya está
 * instalada (ver lib/pwa.php). */
.menu-apps-item[hidden] { display: none; }
/* Los ítems con pantalla propia salen como <a> (hoy "Mi cuenta", ver
 * lib/menu-apps.php), así que además del reset de botón va el de link:
 * sin `text-decoration: none` el rótulo aparece subrayado y la tarjeta
 * deja de verse como las de al lado. */
.menu-apps-item {
  background: transparent;
  border: none;
  padding: 4px 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  color: var(--ma-ink);
  font-family: inherit;
  text-decoration: none;
  cursor: pointer;
}
.menu-apps-icon {
  width: 58px; height: 58px;
  border-radius: 16px;
  background: var(--ma-tile);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.5rem;
  color: var(--ma-ink);
  box-shadow: 0 2px 6px rgba(16, 22, 54, .12);
  transition: background .15s, transform .15s;
}
.menu-apps-item:hover .menu-apps-icon { background: var(--ma-tile-h); }
.menu-apps-item:active .menu-apps-icon { transform: scale(.94); }
.menu-apps-item:focus-visible { outline: none; }
.menu-apps-item:focus-visible .menu-apps-icon {
  outline: 2px solid var(--ma-ink);
  outline-offset: 2px;
}
.menu-apps-label {
  font-size: .72rem;
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
  color: var(--ma-ink);
}

/* --------- modal de la foto de perfil --------- */

/* Lo abre el avatar de /mi-cuenta. Misma mecánica que el menú de apps
 * (fixed + backdrop + tarjeta centrada); lo propio de acá es la vista
 * grande de la foto actual, que es lo que se viene a mirar. */
.foto-modal[hidden] { display: none !important; }
.foto-modal {
  position: fixed;
  inset: 0;
  z-index: 220;   /* por encima del menú de apps (210) */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.foto-backdrop {
  position: absolute; inset: 0;
  background: rgba(16, 22, 54, .55);
  backdrop-filter: blur(2px);
}
.foto-card {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 16px 18px 20px;
  width: 100%;
  max-width: 340px;
  box-shadow: 0 20px 60px rgba(16, 22, 54, .35);
  /* En teléfono apaisado la tarjeta no entra: que scrollee ella y no la
   * página de atrás. */
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}

.foto-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.foto-title { font-size: 1rem; font-weight: 700; color: var(--text); }
.foto-close {
  background: transparent;
  border: none;
  color: var(--muted);
  width: 32px; height: 32px;
  font-size: 1rem;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
}
.foto-close:hover { background: var(--bg); color: var(--text); }

/* El avatar al doble del de la tarjeta: es el tamaño donde se nota si la
 * foto quedó bien recortada dentro del círculo. */
.foto-preview {
  width: 132px; height: 132px;
  margin: 12px auto 20px;
  border-radius: 50%;
  background: var(--primario);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 3rem; font-weight: 700;
  overflow: hidden;
}
.foto-preview-img { background: none; }
.foto-preview img { width: 100%; height: 100%; object-fit: cover; }

/* Los .auth-btn de adentro vienen con el margen que usan apilados en una
 * tarjeta de sesión; acá van pegados uno debajo del otro. */
.foto-card .auth-btn { margin-top: 0; }
.foto-card form + form .auth-btn { margin-top: 10px; }
.foto-card .auth-btn:disabled { opacity: .65; cursor: default; }

/* Quitar la foto es la única acción destructiva del modal. Va en ghost
 * igual que las demás salidas del portal, y el rojo es el mismo de
 * .auth-error — sólo en el texto, para que no compita con la acción
 * principal. */
.foto-quitar { color: #b91c1c; }

.foto-hint {
  margin-top: 14px;
  text-align: center;
  font-size: .74rem;
  color: var(--muted);
}

/* --------- hojas: los modales del aviso --------- */

/* El shell que comparten los modales que abre el menú de la card —visita,
 * conversar, oferta y favorito (ver lib/acciones.php)— más los botones del
 * pie del detalle. Se llamaban `.consulta-*` cuando el único era el de
 * consulta, que se eliminó en 2026-09-17; el nombre `hoja` es el que ya
 * usaba el comentario de abajo para describir la forma.
 *
 * Misma mecánica que el modal de la foto de perfil (fixed + backdrop +
 * tarjeta). Lo propio de cada uno es su contenido: el día y la hora en
 * visita, el hilo en conversar, el monto grande en oferta, y en el de
 * favorito sólo el texto de lo que está por pasar — no hay nada que llenar,
 * sólo que confirmar.
 *
 * `align-items: flex-end` y no `center`: con el teclado virtual abierto
 * el viewport se parte al medio y una tarjeta centrada queda debajo del
 * teclado. Pegada abajo se comporta como una hoja, que es además el gesto
 * esperado para escribir en un teléfono. De 640px para arriba —donde no
 * hay teclado que corra nada— vuelve al centro.
 *
 * ---------------------------------------------------------------
 * El alto, con el teclado virtual abierto
 * ---------------------------------------------------------------
 * `top`/`height` explícitos y no `inset: 0`, porque un `fixed` se mide
 * contra el LAYOUT y el teclado no lo achica en todos lados. Sin esto el
 * modal ocupa la pantalla entera, la mitad de abajo queda tapada, el
 * navegador scrollea para destapar el campo enfocado y la cabecera del
 * modal se va por arriba — que es exactamente el síntoma que esto vino a
 * arreglar.
 *
 * Los dos valores tienen tres fuentes, de más a menos preferida, y las
 * tres escriben lo mismo cuando no hay teclado:
 *
 *   1. `--vv-alto` / `--vv-top`, que publica assets/js/cabecera.js leyendo
 *      `visualViewport`. Es el único camino en Safari.
 *   2. `100dvh`, que ya mide bien en Chrome/Android gracias al
 *      `interactive-widget=resizes-content` del `meta` (lib/chrome.php).
 *   3. `100vh`, para el navegador que no conozca `dvh`.
 *
 * Y de ahí que el alto de las tarjetas de adentro sea un PORCENTAJE y no
 * otro `vh`: así cuelgan de esta única medida en vez de volver a preguntar
 * por su cuenta y contestarse distinto. */
.hoja-modal[hidden] { display: none !important; }
.hoja-modal {
  position: fixed;
  left: 0; right: 0;
  top: var(--vv-top, 0px);
  height: 100vh;
  height: 100dvh;
  height: var(--vv-alto, 100dvh);
  z-index: 220;   /* mismo plano que el modal de la foto de perfil */
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0;
}
.hoja-backdrop {
  position: absolute; inset: 0;
  background: rgba(16, 22, 54, .55);
  backdrop-filter: blur(2px);
}
.hoja-card {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 16px 18px calc(20px + env(safe-area-inset-bottom, 0px));
  width: 100%;
  max-width: 480px;   /* el ancho del .app-shell */
  box-shadow: 0 -12px 40px rgba(16, 22, 54, .28);
  max-height: 88%;
  overflow-y: auto;
}
/* El modal de oferta recibe el foco en la tarjeta al abrir para no enfocar
 * el campo del monto (eso levantaría el teclado del teléfono). La tarjeta
 * ya es todo lo que se ve: marcarle un anillo de foco alrededor sólo
 * confunde. */
.hoja-card:focus { outline: none; }
@media (min-width: 640px) {
  .hoja-modal { align-items: center; padding: 20px; }
  .hoja-card  { border-radius: var(--radius-lg); max-width: 420px; }
}

.hoja-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.hoja-title {
  font-size: 1rem; font-weight: 700; color: var(--text);
  display: inline-flex; align-items: center; gap: 8px;
}
.hoja-title i { color: var(--primario); }
.hoja-close {
  background: transparent;
  border: none;
  color: var(--muted);
  width: 32px; height: 32px;
  font-size: 1rem;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.hoja-close:hover { background: var(--bg); color: var(--text); }

/* De qué aviso se está preguntando: la foto de portada a la izquierda, el
 * título y la cuenta a su derecha. Va arriba de todo porque el modal abre
 * en el medio de la pantalla —lejos de la card que lo disparó— y esto es
 * lo único que dice sobre qué aviso se está por actuar.
 *
 * Pintado como una tira sobre `--bg` y no como texto suelto: la foto ya
 * le da peso de bloque, y el recuadro lo separa del formulario de abajo
 * sin tener que meter una línea divisoria. */
.hoja-aviso {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0 16px;
  padding: 8px;
  background: var(--bg);
  border-radius: var(--radius-md);
}
/* Miniatura cuadrada: las fotos de aviso son panorámicas y las de vehículo
 * no siempre, así que `cover` recorta en vez de deformar. La esconde el JS
 * cuando el aviso no trae foto (assets/js/aviso-modales.js).
 *
 * El lado es grande a propósito: la foto es lo que identifica el aviso en
 * una grilla de cards iguales, y a 54px no se distinguía de la de al lado.
 * Como la tira es la misma pieza en el menú de acciones y en los tres
 * modales, el tamaño se define acá una sola vez y vale en los cuatro. */
.hoja-aviso-foto {
  flex-shrink: 0;
  width: 108px; height: 108px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--border);
}
.hoja-aviso-foto[hidden] { display: none !important; }
/* `min-width: 0` para que el recorte de dos renglones de abajo pueda
 * achicarse: sin esto el título largo estira la fila y desborda. */
.hoja-aviso-txt {
  min-width: 0;
  font-size: .82rem;
  line-height: 1.35;
}
/* Los títulos de inmueble son largos: dos renglones y corta. */
.hoja-aviso-titulo {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--text);
  font-weight: 700;
}
.hoja-aviso-cuenta {
  display: block;
  margin-top: 2px;
  font-size: .76rem;
  color: var(--muted);
}
.hoja-aviso-cuenta:empty { display: none; }
/* El precio publicado, debajo de la cuenta y en el acento de la marca —el
 * mismo tratamiento que tiene en la card del listado, así que se reconoce
 * como el mismo dato. Va en la tira y no adentro de cada modal porque los
 * tres gestos son sobre este número: visitar algo que cuesta tanto,
 * preguntar por algo que cuesta tanto y ofertar contra ese precio.
 *
 * Vacío desaparece —un disparador que no manda el precio, ver
 * assets/js/aviso-modales.js— en vez de dejar un renglón en blanco que
 * descuadra la tira contra la foto. */
.hoja-aviso-precio {
  display: block;
  margin-top: 4px;
  color: var(--acento);
  font-weight: 700;
  font-size: 1rem;
}
.hoja-aviso-precio:empty { display: none; }

.hoja-label {
  display: block;
  font-size: .78rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .04em;
  margin-bottom: 6px;
}
.hoja-textarea {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: .95rem;
  font-family: inherit;
  line-height: 1.45;
  color: var(--text);
  background: #fff;
  outline: none;
  resize: vertical;
  min-height: 110px;
  transition: border-color .15s, box-shadow .15s;
}
.hoja-textarea:focus {
  border-color: var(--acento);
  box-shadow: 0 0 0 3px var(--acento-ring);
}
.hoja-hint {
  margin: 8px 0 0;
  font-size: .74rem;
  color: var(--muted);
  line-height: 1.45;
}
/* La pregunta de un modal que sólo pregunta — hoy: la baja de una
 * publicación en /mi-cuenta. Es el renglón que hay que leer antes de
 * tocar el botón rojo, así que va en el cuerpo del texto y en `--text`;
 * el `.hoja-hint` de abajo, que dice qué se pierde, es la aclaración y
 * por eso queda en gris y más chico. Un modal así no tiene formulario:
 * este párrafo ocupa el lugar que en los otros ocupa el campo. */
.hoja-pregunta {
  margin: 12px 0 0;
  font-size: .95rem;
  color: var(--text);
  line-height: 1.45;
}

/* Hermana del hint, para lo que hay que leer DESPUÉS del botón y no
 * antes: hoy es la advertencia de que el boleto que se imprime es un
 * modelo y no un instrumento firmado (ver `appCompraModal()` en
 * lib/acciones.php). Centrada y más chica que el hint — es una nota al
 * pie, no una instrucción del formulario. */
.hoja-nota {
  margin: 10px 0 0;
  font-size: .7rem;
  color: var(--muted);
  line-height: 1.4;
  text-align: center;
}
/* =================================================================
 * Los servicios de "Coordinar compra"
 * =================================================================
 * Un renglón por servicio, cada uno con su ícono, su nombre, la oración
 * que lo explica y la llave a la derecha (ver
 * `appCompraServiciosBloque()` en lib/acciones.php).
 *
 * El renglón tiene la MISMA cara que un campo del formulario del
 * documento —borde, radio y padding de `.hoja-input`, y el fondo suave y
 * sin borde de `.hoja-dato` cuando está prendido—. No es casualidad: es
 * el mismo par de estados que ya usan las pastillas de `.hoja-opcion`
 * ("esto es un hueco" / "esto ya está resuelto"), y repetirlo acá es lo
 * que hace que los dos pasos del gesto se lean como el mismo formulario.
 *
 * Lo que dice si está prendido es la LLAVE y no el color del renglón: en
 * una lista de tres, el relleno suave solo obliga a comparar los tres
 * fondos entre sí para saber cuál está marcado. */
.hoja-servicios { margin-top: 16px; }
.hoja-servicio { position: relative; display: block; cursor: pointer; }
.hoja-servicio + .hoja-servicio { margin-top: 8px; }
/* El checkbox, transparente encima del renglón entero. No `display:none`
 * ni `visibility:hidden`, por lo mismo que en `.hoja-opcion input`: varios
 * navegadores lo sacan del recorrido del tabulador. */
.hoja-servicio input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.hoja-servicio-caja {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: #fff;
  transition: background .15s, border-color .15s;
}
.hoja-servicio input:checked + .hoja-servicio-caja {
  border-color: transparent;
  background: var(--bg);
}
/* El foco se dibuja en el renglón, que es lo que se ve: el checkbox es
 * transparente y su anillo propio caería sobre nada. */
.hoja-servicio input:focus-visible + .hoja-servicio-caja {
  border-color: var(--acento);
  box-shadow: 0 0 0 3px var(--acento-ring);
}
/* Ancho fijo y centrado: los tres glifos no miden lo mismo y sin la caja
 * los nombres arrancan en tres columnas distintas.
 *
 * En el naranja del acento SIEMPRE, prendido y apagado. Estuvo en gris
 * apagándose y prendiéndose con el estado, y sobraba: de decir cuáles
 * están adentro ya se encarga la llave de la derecha, así que el ícono
 * cambiando de color era el mismo dato dicho dos veces —y con los tres
 * apagados la lista entera quedaba gris, leyéndose como deshabilitada. */
.hoja-servicio-icono {
  flex-shrink: 0;
  width: 20px;
  text-align: center;
  font-size: 1rem;
  color: var(--acento);
}
/* `min-width: 0` para que el detalle pueda cortar: sin esto la oración
 * estira el renglón y empuja la llave fuera de la tarjeta. */
.hoja-servicio-txt { flex: 1; min-width: 0; }
/* El MISMO texto que adentro de un `.hoja-input`: tamaño, interlineado,
 * color y peso normal. En negrita competiría con el rótulo del grupo. */
.hoja-servicio-nombre {
  display: block;
  font-size: .95rem;
  line-height: 1.25;
  color: var(--text);
}
/* La oración que dice qué incluye, en el cuerpo del `.hoja-hint`: es la
 * misma clase de texto —una aclaración del formulario— y leerla en dos
 * tamaños distintos según dónde esté sería ruido. */
.hoja-servicio-detalle {
  display: block;
  margin-top: 2px;
  font-size: .74rem;
  line-height: 1.35;
  color: var(--muted);
}
/* La llave. Es lo ÚNICO que dice si el servicio está adentro o afuera, y
 * por eso es lo único del renglón que cambia de color con el estado.
 *
 * En el naranja del acento, el mismo de los íconos de la izquierda y el
 * del botón de abajo: el bloque entero queda en un solo color de marca en
 * vez de repartirse entre el azul y el naranja, que era lo que pasaba
 * cuando las llaves iban en `--primario`. El gris del apagado es el mismo
 * `--border` de los campos vacíos, así que "todavía no" se lee igual acá
 * que en el formulario del paso siguiente.
 *
 * El pulgar va en `::after` y no en un `span` propio: es decoración pura
 * —el estado ya lo canta el checkbox nativo— así que no tiene por qué
 * existir en el markup. */
.hoja-servicio-llave {
  flex-shrink: 0;
  position: relative;
  width: 38px;
  height: 22px;
  border-radius: 11px;
  background: var(--border);
  transition: background .18s;
}
.hoja-servicio-llave::after {
  content: '';
  position: absolute;
  top: 3px; left: 3px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(16, 22, 54, .3);
  transition: transform .18s;
}
.hoja-servicio input:checked + .hoja-servicio-caja .hoja-servicio-llave {
  background: var(--acento);
}
.hoja-servicio input:checked + .hoja-servicio-caja .hoja-servicio-llave::after {
  /* 38 − 16 − 3 − 3: el ancho menos el pulgar menos los dos márgenes. */
  transform: translateX(16px);
}

/* Los .auth-btn de adentro vienen con el margen que usan apilados en una
 * tarjeta de sesión; acá el gap lo pone el .hoja-hint de arriba. */
.hoja-card .auth-btn { margin-top: 12px; }
.hoja-card .auth-btn:disabled { opacity: .65; cursor: default; }

/* El error del envío — mismo formato que .auth-error, sin el margen
 * inferior que allá separa del formulario. */
.hoja-estado {
  display: flex; align-items: center; gap: 8px;
  background: #fee2e2;
  color: #b91c1c;
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  font-size: .85rem;
  font-weight: 600;
  margin-bottom: 14px;
}
.hoja-estado[hidden] { display: none !important; }

/* Estado "enviada": reemplaza al formulario entero. Verde de .auth-ok. */
.hoja-ok {
  display: flex; align-items: flex-start; gap: 10px;
  background: var(--mint-soft);
  color: #0f6d5f;
  border-radius: var(--radius-sm);
  padding: 14px;
  font-size: .85rem;
  line-height: 1.45;
}
.hoja-ok[hidden] { display: none !important; }
.hoja-ok i { font-size: 1.1rem; margin-top: 1px; }

/* ---------------------------------------------------------------
 * La cara "este aviso es tuyo"
 * ---------------------------------------------------------------
 * La tercera cosa que puede reemplazar al cuerpo de un modal, después
 * del cartel verde y de la invitación a ingresar (ver `appHojaPropio()`
 * en lib/acciones.php).
 *
 * MISMA FORMA que `.hoja-ok` —el ícono a la izquierda, el texto a la
 * derecha, el mismo padding y el mismo cuerpo— y distinto COLOR: el azul
 * de marca en vez del verde. Las dos son el portal contando algo que ya
 * pasó, pero una celebra un gesto que salió y ésta explica uno que no se
 * puede hacer; el verde ahí se leería como que la oferta se mandó.
 *
 * El azul es el mismo de `.aviso-mio` —el cartel del hero y de la card—
 * y tiene que serlo: son las dos superficies que dicen lo mismo, una en
 * la foto y la otra adentro del modal que se abrió desde ahí. */
.hoja-propio {
  display: flex; align-items: flex-start; gap: 10px;
  background: color-mix(in srgb, var(--primario) 10%, #fff);
  color: var(--primario);
  border-radius: var(--radius-sm);
  padding: 14px;
  font-size: .85rem;
  line-height: 1.45;
}
.hoja-propio i { font-size: 1.1rem; margin-top: 1px; }
/* El `strong` encabeza la frase y sigue el motivo en el mismo párrafo:
 * son dos oraciones de la misma idea —qué pasa y por qué— y partirlas en
 * dos bloques las haría leer como dos avisos distintos. */
.hoja-propio strong { display: block; }

[data-hoja-propio][hidden] { display: none !important; }

/* Y lo que la cara TAPA: el cuerpo del modal que la abrió.
 *
 * Va por clase en el modal y no con un `hidden` por pieza desde el JS
 * porque las piezas son distintas en cada uno —un `[data-hoja-form]` en
 * compra, visita y oferta, y el hilo más el compositor en conversar— y
 * esa lista, escrita allá, es una que hay que acordarse de actualizar
 * cada vez que un modal cambie de forma. Acá vive al lado de las clases
 * que nombra.
 *
 * El `!important` es por lo mismo que en la lista de `.auth-btn` de más
 * abajo: `.chat-compositor` es `display: flex` y le ganaría. */
.hoja-modal.es-propio [data-hoja-form],
.hoja-modal.es-propio .chat-hilo,
.hoja-modal.es-propio .chat-compositor { display: none !important; }

/* La tarjeta del chat tiene alto FIJO para que el compositor no se mueva
 * mientras se escribe (ver `.hoja-card-chat` más abajo). Sin hilo ni
 * compositor ese alto deja una caja de media pantalla con tres renglones
 * arriba, así que acá vuelve a medir lo que mide su contenido — que es
 * como se comportan los otros cuatro modales en esta misma cara. */
.hoja-modal.es-propio .hoja-card-chat { height: auto; }

/* TODO `.auth-btn` que nazca oculto tiene que estar en esta lista.
 *
 * `.auth-btn` es `display: inline-flex`, y eso le gana al `display: none`
 * que el navegador le da al atributo `hidden` —misma especificidad, gana
 * la hoja de autor—. Un botón con `hidden` puesto se dibuja igual, así
 * que hace falta declararlo acá.
 *
 * Es un tropiezo fácil y ya pasó TRES veces: el `[data-gesto-conversar]`
 * se agregó junto con el botón y sin esta línea, y aparecía en la cara de
 * estado incluso con el gesto `pendiente` —donde el servidor manda
 * `conversar: false`—, o sea que el JS lo ocultaba bien y el CSS lo volvía
 * a mostrar. Lo mismo con `[data-gesto-reintentar]` en 2026-09-17c, que
 * salía en los cuatro estados a la vez. Si agregás un `.auth-btn` que
 * arranque en `hidden`, sumalo acá EN LA MISMA EDICIÓN: el bug no se ve
 * leyendo el JS ni el markup, que son los dos lugares donde uno va a
 * buscarlo.
 *
 * Los seis de hoy (ver lib/acciones.php):
 *
 *   · `[data-hoja-listo]`       → el "Cerrar" de los carteles verdes
 *   · `[data-favorito-quitar]`   → "Quitar de favoritos", en su modal
 *   · `[data-favorito-cerrar]`   → el "Cerrar" que lo acompaña
 *   · `[data-gesto-baja]`       → cancelar la visita / retirar la oferta
 *   · `[data-gesto-conversar]`  → el salto al hilo, sólo con el gesto aceptado
 *   · `[data-gesto-reintentar]` → volver a proponer, sólo con el gesto rechazado */
[data-hoja-listo][hidden],
[data-gesto-baja][hidden],
[data-gesto-conversar][hidden],
[data-gesto-reintentar][hidden],
[data-favorito-quitar][hidden],
[data-favorito-cerrar][hidden] { display: none !important; }

/* ---------------------------------------------------------------
 * La cara de ESTADO de visita y oferta
 * ---------------------------------------------------------------
 *
 * La segunda cara de esos dos modales: en qué quedó el gesto que ya está
 * hecho (ver `appGestoEstadoCara()` en lib/acciones.php). Reemplaza al
 * formulario, igual que el cartel de "enviada".
 *
 * El orden visual es el de importancia y no el de la base: primero el
 * DATO —el día propuesto, el monto ofrecido—, después el estado, y al
 * final los textos. Quien abre esto viene a ver una de dos cosas: qué
 * había pedido, o qué le contestaron. */
.gesto[hidden] { display: none !important; }

/* El spinner de mientras viaja el GET. A diferencia del del chat, que se
 * centra en una tarjeta de alto fijo, acá la tarjeta mide lo que mida su
 * contenido: sin un alto propio el modal se colapsaría al alto del
 * spinner y volvería a crecer de golpe al llegar la respuesta. Estos
 * ~120px son lo que mide la cara de estado más corta —dato, estado y nada
 * más— así que el salto es el mínimo posible en cualquiera de los dos
 * caminos. */
.gesto-cargando {
  display: flex; align-items: center; justify-content: center;
  min-height: 120px;
  color: var(--muted);
  font-size: 1.2rem;
}
.gesto-cargando[hidden] { display: none !important; }

/* El dato grande. Centrado y en el cuerpo más grande del modal: es lo
 * único que se lee sin enfocar la vista, y el resto de la tarjeta lo
 * acompaña. */
.gesto-dato {
  text-align: center;
  padding: 6px 0 14px;
}
.gesto-dato strong {
  display: block;
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1.2;
}
.gesto-dato-sub {
  display: block;
  margin-top: 2px;
  font-size: .95rem;
  color: var(--muted);
}
/* Un sub vacío no deja aire: en oferta el dato es un renglón solo. */
.gesto-dato-sub:empty { display: none; }

/* El estado, en una pastilla a todo el ancho. Los cuatro tonos salen del
 * `data-tono` que pone el servidor (ver `APP_GESTO_ESTADOS` en
 * lib/marcas.php), que es el que sabe qué significa cada valor del ENUM.
 *
 * `ok` reusa el verde de `.hoja-ok` y `no` el rojo de `.hoja-estado`: son
 * los mismos dos colores con los que este modal ya dice "salió bien" y
 * "salió mal", y agregar un par nuevo sólo para acá haría que la misma
 * idea se pinte de dos maneras. `espera` va en el ámbar de los avisos
 * —nada falló, todavía no pasó nada— y `fin` en gris. */
.gesto-estado {
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  font-size: .85rem;
  font-weight: 600;
  text-align: center;
  line-height: 1.4;
}
.gesto-estado[data-tono="espera"] { background: #fef3c7; color: #92400e; }
.gesto-estado[data-tono="ok"]     { background: var(--mint-soft); color: #0f6d5f; }
.gesto-estado[data-tono="no"]     { background: #fee2e2; color: #b91c1c; }
.gesto-estado[data-tono="fin"]    { background: #f1f5f9; color: var(--muted); }

/* Los textos: lo que escribió la persona al proponer y lo que contestó el
 * vendedor. Los dos con la misma forma —un rótulo chico arriba y el texto
 * debajo— porque son la misma clase de cosa dicha por las dos puntas.
 *
 * `pre-wrap` por lo mismo que en las burbujas del chat: los saltos de
 * línea los escribió una persona y el texto entra por `textContent`, así
 * que no hay `<br>` que los sostenga. */
.gesto-nota {
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: #f8fafc;
  border: 1px solid var(--border);
  font-size: .85rem;
  line-height: 1.45;
  white-space: pre-wrap;
}
.gesto-nota[hidden] { display: none !important; }
/* La respuesta del vendedor es lo NUEVO —es lo que el sello verde vino a
 * anunciar— así que se separa del resto con el borde izquierdo en acento.
 * No va en verde: el verde de este modal significa "el resultado fue
 * bueno", y una respuesta puede ser un rechazo. */
.gesto-nota-resp {
  border-left: 3px solid var(--acento);
  background: #fff;
}
.gesto-nota-de {
  display: block;
  margin-bottom: 3px;
  font-size: .72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--muted);
  white-space: normal;
}

/* ---------------------------------------------------------------
 * El chat de "Conversar"
 * ---------------------------------------------------------------
 *
 * Es el único de los cinco modales que no termina, y eso le cambia la
 * mecánica del alto. Los otros cuatro crecen con su contenido y la
 * tarjeta entera scrollea cuando se pasa de 88vh; acá el alto es FIJO y
 * lo que scrollea es el hilo, con el compositor clavado al pie.
 *
 * El motivo es que el contenido crece MIENTRAS la persona lo usa: con
 * alto variable, cada mensaje empuja el campo de texto un renglón hacia
 * abajo —justo debajo del dedo que está escribiendo— hasta que la
 * tarjeta topa contra el 88vh y recién ahí se queda quieta. Con alto
 * fijo el compositor no se mueve nunca. */
.hoja-card-chat {
  display: flex;
  flex-direction: column;
  /* La tarjeta ya no scrollea: de eso se encarga el hilo. Sin esto el
   * `overflow-y: auto` que hereda de `.hoja-card` crea un segundo
   * contenedor scrolleable anidado y la rueda del mouse mueve el
   * equivocado. */
  overflow: hidden;
  /* Un PORCENTAJE del modal y no `88dvh`, que es lo que decía hasta
   * 2026-09-17d: el `.hoja-modal` ya resolvió cuánto se ve de verdad con
   * el teclado abierto —y en Safari lo resuelve con una variable que el
   * `dvh` no conoce— así que preguntarlo otra vez acá era la forma de que
   * las dos medidas terminaran en desacuerdo. Con el 88% la tarjeta
   * siempre entra en el modal, y la cabecera queda a la vista.
   *
   * Resuelve porque el padre es flex con alto definido. */
  height: 88%;
}
@media (min-width: 640px) {
  /* En escritorio el modal está centrado y no pegado abajo: 88vh sería
   * casi toda la pantalla para un chat que rara vez tiene veinte
   * mensajes. El `min()` lo deja crecer con la ventana hasta un techo
   * cómodo de lectura. */
  .hoja-card-chat { height: min(78vh, 620px); }
}

/* El hilo: lo único que scrollea. `flex: 1` con `min-height: 0` —sin lo
 * segundo un hijo flex no baja de su alto de contenido y el scroll nunca
 * se activa, el mismo detalle que el `.modal-body` del panel.
 *
 * Los márgenes negativos lo estiran hasta los bordes de la tarjeta: las
 * burbujas se leen mejor contra el ancho completo, y el `padding` de
 * adentro repone el aire. Así, además, la sombra del scroll llega al
 * borde en vez de cortarse a 18px. */
.chat-hilo {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;   /* el rebote no arrastra la página de atrás */
  margin: 0 -18px;
  padding: 4px 18px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.chat-cargando {
  margin: auto;
  color: var(--muted);
  font-size: 1.2rem;
}
.chat-cargando[hidden] { display: none !important; }

/* El hilo vacío. Centrado en el alto disponible —`margin: auto` dentro
 * de un flex column— para que no quede colgando arriba en una tarjeta
 * que mide 88vh. */
.chat-vacio {
  margin: auto;
  padding: 0 24px;
  text-align: center;
  color: var(--muted);
  font-size: .85rem;
  line-height: 1.5;
}
.chat-vacio[hidden] { display: none !important; }
.chat-vacio i {
  display: block;
  margin: 0 auto 10px;
  font-size: 2rem;
  color: var(--border);
}

/* Separador de día. La línea se dibuja con dos bordes que crecen a los
 * costados del rótulo (`flex: 1`), que es lo que lo mantiene centrado
 * sea cual sea el largo del texto — "Hoy" y "16 de septiembre de 2025"
 * quedan los dos en el medio. */
.chat-sep {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0 4px;
}
.chat-sep::before,
.chat-sep::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}
.chat-sep span {
  font-size: .7rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .06em;
}

/* La burbuja. `max-width: 82%` y no 100%: lo que dice de qué lado viene
 * cada mensaje es que no llegue al otro borde — el color solo no alcanza
 * para quien no lo distingue.
 *
 * `align-self` es lo que la manda a su lado; el ancho lo da el contenido
 * (`width: fit-content` implícito en un ítem flex con align-self). */
.chat-msg {
  align-self: flex-start;
  max-width: 82%;
  background: var(--bg);
  color: var(--text);
  border-radius: var(--radius-md);
  /* La esquina del lado del que habla va cuadrada: es la "colita" de la
   * burbuja, dibujada sin pseudo-elemento. */
  border-bottom-left-radius: 4px;
  padding: 9px 12px 6px;
  font-size: .9rem;
  line-height: 1.4;
}
.chat-msg.is-mio {
  align-self: flex-end;
  background: var(--primario);
  color: #fff;
  border-bottom-left-radius: var(--radius-md);
  border-bottom-right-radius: 4px;
}
/* `pre-wrap` conserva los saltos de línea que escribió la persona sin
 * tener que convertirlos a `<br>` — el JS pinta el texto con
 * `textContent`, que es lo que lo vuelve seguro (ver aviso-modales.js). */
.chat-msg p {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;   /* una URL larga no ensancha la burbuja */
}
/* La hora, chiquita y pegada al final. Va en el renglón de abajo y
 * alineada a la derecha en las dos: probar a meterla en la misma línea
 * del texto obliga a reservarle lugar y deja un hueco raro en los
 * mensajes cortos. */
.chat-msg time {
  display: block;
  margin-top: 2px;
  text-align: right;
  font-size: .65rem;
  opacity: .6;
}

/* El compositor, clavado al pie de la tarjeta. El borde de arriba lo
 * separa del hilo que scrollea por detrás. */
.chat-compositor {
  flex-shrink: 0;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  /* Sólo horizontal: el borde de arriba tiene que llegar a los dos
   * bordes de la tarjeta, pero abajo manda el padding de `.hoja-card`,
   * que ya reserva el `safe-area-inset` del teléfono. */
  margin: 0 -18px;
  padding: 10px 18px 0;
  border-top: 1px solid var(--border);
}
/* El campo arranca de un renglón y crece con lo que se escribe hasta
 * este tope, que es donde empieza a scrollear (lo estira el JS, ver
 * `chatAjustarCampo()`).
 *
 * `resize: none` a propósito, a diferencia de `.hoja-textarea`: el alto
 * lo maneja el JS y una manija de arrastre pelearía con él. */
.chat-campo {
  flex: 1;
  min-width: 0;
  max-height: 120px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 20px;
  font-size: .9rem;
  font-family: inherit;
  line-height: 1.4;
  color: var(--text);
  background: #fff;
  outline: none;
  resize: none;
  overflow-y: auto;
  transition: border-color .15s, box-shadow .15s;
}
.chat-campo:focus {
  border-color: var(--acento);
  box-shadow: 0 0 0 3px var(--acento-ring);
}
.chat-campo:disabled { opacity: .65; }

/* El botón de enviar: redondo y del tamaño del renglón de un campo
 * vacío, para que los dos arranquen alineados. Va en el acento —es la
 * acción del modal— igual que "Ofertar" en la barra de la ficha. */
.chat-enviar {
  flex-shrink: 0;
  width: 42px; height: 42px;
  border: none;
  border-radius: 50%;
  background: var(--acento);
  color: #fff;
  font-size: .95rem;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .15s, transform .1s;
}
.chat-enviar:hover    { background: var(--acento-h); }
.chat-enviar:active   { transform: scale(.94); }
.chat-enviar:disabled { opacity: .65; transform: none; }

/* El compositor se esconde cuando la conversación quedó cerrada (ver
 * `.conv-cerrada`). Hace falta el `!important` porque la regla de arriba
 * le puso `display: flex` y eso le gana al `display: none` que el
 * navegador aplica por el atributo `hidden`. */
.chat-compositor[hidden] { display: none !important; }

/* Los dos renglones de contexto del hilo en la BANDEJA de /mi-cuenta —con
 * quién se está hablando, y el link a la ficha del aviso—. Van entre la
 * tira y el hilo, y no existen en el chat de la ficha: allá las dos cosas
 * ya se saben, porque se entró desde el aviso y la otra punta es siempre
 * el vendedor.
 *
 * `flex-shrink: 0` porque dentro de `.hoja-card-chat` el único que puede
 * ceder alto es el hilo. */
.conv-meta {
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
  font-size: .76rem;
}
/* Con los dos renglones ocultos la fila no deja su margen colgando. Si el
 * navegador no entiende `:has()` la regla se descarta entera y lo peor que
 * queda son 8px de aire. */
.conv-meta:not(:has(> :not([hidden]))) { display: none; }
.conv-con {
  min-width: 0;
  color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.conv-ver { flex-shrink: 0; color: var(--primario); font-weight: 600; }
.conv-ver:hover { text-decoration: underline; }

/* Reemplaza al compositor cuando el aviso dejó de estar publicado y el
 * hilo pasó a ser sólo de lectura. Lleva el mismo borde de arriba y los
 * mismos márgenes negativos que él, para que el pie de la tarjeta no
 * cambie de forma cuando el campo no está. */
.conv-cerrada {
  flex-shrink: 0;
  margin: 0 -18px;
  padding: 12px 18px 0;
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 8px;
  font-size: .78rem; color: var(--muted);
}
.conv-cerrada[hidden] { display: none !important; }

/* El monto de la oferta — es todo el contenido del modal, así que se
 * pinta del tamaño de un precio de portada y no del de un input. El
 * símbolo va pegado al número y no adentro del campo: con `padding-left`
 * el cursor arranca corrido y en un número largo se nota.
 *
 * `inputmode="numeric"` y no `type="number"`: el campo lleva separador de
 * miles (lo formatea el JS mientras se escribe) y un `number` con puntos
 * adentro es un valor inválido para el navegador, que lo vacía sin avisar.
 * Además saca las flechitas de spinner, que a este tamaño quedan enormes. */
.hoja-monto {
  display: flex; align-items: center; justify-content: center;
  gap: 8px;
  /* El margen de abajo es el que antes ocupaba el renglón de la
   * diferencia, que se mudó arriba (ver `.hoja-monto-tope`): sin él la
   * caja del monto quedaba pegada al botón de enviar. */
  margin: 6px 0 16px;
  padding: 14px 10px;
  background: var(--bg);
  border-radius: var(--radius-md);
}

/* El renglón de arriba del monto: el rótulo a la izquierda y la
 * diferencia contra el precio publicado a la derecha.
 *
 * La diferencia vivía debajo del campo hasta 2026-09-18 y ahí no se veía:
 * los −/+ se aprietan con el pulgar y la mano tapa la franja de abajo de
 * la caja justo mientras el número se mueve, que es el único momento en
 * que la cuenta importa. Arriba del monto queda fuera del alcance de la
 * mano, y de paso aprovecha el aire que le sobraba al rótulo.
 *
 * Alineados por la base y no por el medio: son dos textos chicos y del
 * mismo tamaño, y centrados verticalmente se notaría el medio punto que
 * los separa. */
.hoja-monto-tope {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
/* El margen del rótulo ahora lo pone el renglón. */
.hoja-monto-tope .hoja-label { margin-bottom: 0; }
/* El símbolo y el número van en su propia caja para poder alinearse por
 * la base entre ellos: el `US$` se apoya en el número, no en su medio.
 * Los botones de paso, en cambio, se centran contra el alto del renglón. */
.hoja-monto-valor {
  flex: 1;
  min-width: 0;
  display: flex; align-items: baseline; justify-content: center;
  gap: 6px;
}
/* El campo mide lo que tiene escrito, no el renglón entero: con un input
 * elástico el símbolo termina contra el borde de la caja y el número
 * centrado a varios centímetros, que es justo lo que no se quiere.
 *
 * El ancho lo da un espejo (`::after`) con el mismo texto y la misma
 * tipografía, apilado con el input en la única celda del grid; el JS
 * mantiene `data-valor` en sincronía con el campo. El `max-width` es el
 * tope para un monto largo: ahí el número deja de crecer y scrollea
 * adentro en vez de empujar los −/+ fuera de la tarjeta. */
.hoja-monto-campo {
  display: inline-grid;
  min-width: 3ch;    /* un aviso sin precio abre vacío y hay que poder tocarlo */
  max-width: 100%;
  /* La tipografía del monto vive acá y no en el input: así la heredan los
   * dos —campo y espejo— de un solo lado, y el `ch` del `min-width` mide
   * en el cuerpo del número y no en el del texto de alrededor. */
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.1;
}
.hoja-monto-campo::after {
  content: attr(data-valor);
  visibility: hidden;
  white-space: pre;
}
/* −/+ de a 10.000: mover el precio publicado unos escalones es más rápido
 * que reescribir el monto entero, sobre todo en el teclado del teléfono.
 * 44px es el mínimo cómodo para el dedo.
 *
 * Pintados en el acento desde el vamos. Antes eran dos círculos blancos
 * con el glifo naranja y el relleno aparecía sólo en hover: en el teléfono
 * —que es donde se usan— no hay hover, así que ese estado no lo veía
 * nadie y los dos botones quedaban casi del mismo blanco que la caja del
 * monto. Llenos se ven antes de buscarlos, que es lo que hace falta en el
 * único control con el que se mueve el número. */
.hoja-monto-paso {
  flex-shrink: 0;
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  border: none;
  border-radius: 50%;
  background: var(--acento);
  color: #fff;
  font-size: 1rem;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background .15s, color .15s;
}
.hoja-monto-paso:hover  { background: var(--acento-h); }
.hoja-monto-paso:active { transform: translateY(1px); }
.hoja-monto-simbolo {
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--acento);
  flex-shrink: 0;
}
/* El input y su espejo comparten celda y tipografía: si se separan, el
 * ancho que mide uno deja de ser el que necesita el otro. Los 2px de los
 * costados son para que el cursor al final del número no quede cortado. */
.hoja-monto input,
.hoja-monto-campo::after {
  grid-area: 1 / 1;
  font: inherit;
  letter-spacing: inherit;
  padding: 0 2px;
}
.hoja-monto input {
  width: 100%;
  min-width: 0;
  border: none;
  background: transparent;
  outline: none;
  color: var(--acento);
  text-align: center;
}
/* Cuánto se está rebajando respecto del precio publicado —el monto de la
 * diferencia y el porcentaje—, a la derecha del rótulo. Es la consecuencia
 * de lo que se está tocando abajo, así que va en un cuerpo chico y en
 * gris: lo que se edita es el monto, esto es lo que resulta.
 *
 * No hace falta reservarle alto: comparte renglón con el rótulo, que lo
 * sostiene aunque el texto esté vacío —un aviso sin precio nunca lo
 * muestra—, así que nada se mueve cuando aparece o desaparece.
 *
 * En un solo renglón y con puntos suspensivos si no entra: el texto es
 * corto a propósito (ver `refDiferencia()` en assets/js/aviso-modales.js),
 * pero una diferencia de ocho cifras en una pantalla angosta podría
 * partirse en dos y desalinear el rótulo. El `min-width: 0` es lo que deja
 * que el flex lo achique hasta ahí en vez de desbordar la tarjeta.
 *
 * Los dos números que importan van en `<strong>`: el de la rebaja y el
 * porcentaje (los pone el JS, ver assets/js/aviso-modales.js). */
.hoja-monto-ref {
  margin: 0;
  min-width: 0;
  text-align: right;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: .78rem;
  color: var(--muted);
}
.hoja-monto-ref strong { color: var(--text); }

/* El día y la hora de la visita. Van los dos en un renglón y no apilados:
 * son un solo dato partido en dos controles (ver lib/acciones.php), y uno
 * debajo del otro se leerían como dos preguntas distintas.
 *
 * El día se lleva más ancho que la hora porque eso es lo que miden: una
 * fecha completa contra cuatro dígitos. Con 1fr y 1fr el campo de la hora
 * queda medio vacío y el del día, en los navegadores que muestran el día
 * de la semana, recortado. */
.hoja-cuando {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 10px;
}
.hoja-cuando-campo { min-width: 0; }

/* Los `date` y `time` nativos. Mismo borde, radio y foco que el textarea
 * —son campos del mismo formulario— pero con el alto de un input. */
.hoja-input {
  width: 100%;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: .95rem;
  font-family: inherit;
  color: var(--text);
  background: #fff;
  outline: none;
  transition: border-color .15s, box-shadow .15s;
}
.hoja-input:focus {
  border-color: var(--acento);
  box-shadow: 0 0 0 3px var(--acento-ring);
}
/* Safari en iOS le da a los campos de fecha un alto propio y los centra
 * arriba; sin esto el renglón queda con los dos campos desalineados. */
.hoja-input::-webkit-date-and-time-value { text-align: left; min-height: 1.2em; }
/* El ícono del selector nativo, en el color de marca en vez del gris de
 * fábrica — es el único affordance de que el campo abre algo. */
.hoja-input::-webkit-calendar-picker-indicator { cursor: pointer; opacity: .55; }
.hoja-input:hover::-webkit-calendar-picker-indicator { opacity: 1; }

/* El día y la hora de la visita, llenos del acento.
 *
 * Va acotado a `.hoja-cuando` y no a `.hoja-input` a secas a propósito:
 * la misma clase la usan los quince campos del formulario del boleto, y
 * ahí quince cajas naranjas serían una pared. Estos dos son los únicos
 * campos de su modal —todo lo que se decide al agendar es cuándo— así que
 * pintados se leen como el control de la pantalla y no como un hueco más.
 *
 * El borde se va al mismo naranja: con el `--border` gris encima del
 * relleno quedaba un hilo claro alrededor de cada caja. */
.hoja-cuando .hoja-input {
  background: var(--acento);
  border-color: var(--acento);
  color: #fff;
}
/* El foco se marca con el borde oscuro + el halo de siempre: sobre el
 * relleno naranja, el `border-color: var(--acento)` del foco genérico no
 * se distinguiría del fondo. */
.hoja-cuando .hoja-input:focus { border-color: var(--acento-h); }
/* El glifo del selector nativo lo dibuja el navegador en negro y no hay
 * forma de darle un color: `invert(1)` sobre un negro da blanco, que es
 * lo que hace falta sobre el relleno. Y va opaco del todo —el .55 de
 * reposo era para que un ícono oscuro no pesara sobre el campo blanco;
 * acá el que se pierde es el blanco a media opacidad. */
.hoja-cuando .hoja-input::-webkit-calendar-picker-indicator {
  filter: invert(1);
  opacity: 1;
}

/* =================================================================
 * El formulario de "Preparar boleto"
 * =================================================================
 * Veinte campos en una lista corrida son una pared. Van en bloques que
 * siguen el orden del documento —el encuentro, el precio, el bien, el
 * pago, las dos partes, los gastos— así que llenarlo es recorrer el
 * boleto de arriba abajo (ver `appPrepararModal()` en lib/acciones.php).
 * ================================================================= */

/* La tarjeta se ensancha en escritorio: con los 420px del resto de los
 * modales la grilla de dos columnas queda con campos de 190px y un
 * domicilio no entra. En el teléfono no cambia nada — ahí la grilla ya
 * colapsa a una columna.
 *
 * Son DOS escalones y no uno solo porque los 920px del tope no entran en
 * una pantalla de notebook chica: ahí el modal quedaría pegado a los dos
 * bordes, sin el marco de fondo que es lo que dice que atrás hay una
 * página esperando. El escalón de 640px es para esas pantallas y el de
 * 1024px para el monitor, que es donde este formulario se llena de verdad
 * —veinte campos no se tipean desde el teléfono— y donde el ancho extra
 * se nota: los bloques entran de a dos renglones en vez de cuatro y el
 * scroll deja de ser el único modo de ver el documento entero. */
@media (min-width: 640px) {
  .hoja-card-form { max-width: 720px; }
}
@media (min-width: 1024px) {
  .hoja-card-form { max-width: 920px; }
}

/* Título de bloque. No es un `.hoja-label` más grande: los rótulos de
 * campo son versalitas grises y éste tiene que leerse como el renglón que
 * los agrupa, así que va en el color del texto y con una divisoria arriba
 * que corta con el bloque anterior. */
.hoja-grupo {
  margin: 20px 0 10px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  font-size: .82rem;
  font-weight: 800;
  color: var(--text);
  letter-spacing: .02em;
}
/* El primero no lleva divisoria: arriba tiene la explicación, no otro
 * bloque. Va por clase y no por `:first-of-type`, que acá acertaría por
 * casualidad —depende de que ningún `div` se cuele antes— y dejaría de
 * acertar sin que nadie lo note. */
.hoja-grupo-1 { margin-top: 12px; padding-top: 0; border-top: none; }
.hoja-grupo[hidden] { display: none !important; }

.hoja-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.hoja-grid[hidden] { display: none !important; }
.hoja-campo { min-width: 0; }
/* Los campos que no entran en media columna: un domicilio, "cuándo se
 * entrega la posesión". Un correo y un celular SÍ entran, y van uno al
 * lado del otro: son el par de contacto de una misma parte. */
.hoja-campo-ancho { grid-column: 1 / -1; }

/* El dato que YA ESTÁ: la marca, el modelo, el año, el color —lo que sale
 * del aviso y va al papel igual, pero acá no se edita—.
 *
 * Es un `div` con texto y no un `input[readonly]`, y la diferencia es lo
 * que se entiende sin leer nada: un campo con borde de input, cursor de
 * texto y fondo blanco promete que se puede escribir, y el que lo intenta
 * descubre que no a fuerza de tocarlo. Éste comparte el alto, el radio y
 * el interlineado de un `.hoja-input` —así forma renglón con los campos de
 * al lado, que es lo que mantiene la grilla— pero se apoya en el fondo
 * suave y sin borde: se lee como un dato, no como un hueco.
 *
 * El rótulo es el mismo `.hoja-label` de los editables, a propósito: el
 * bloque es UNA cláusula —lo que ya está y lo que falta— y dos familias de
 * rótulo lo partirían en dos listas que no se parecen. */
.hoja-dato {
  padding: 11px 12px;
  border: 1px solid transparent;
  border-radius: 12px;
  background: var(--bg);
  font-size: .95rem;
  line-height: 1.25;
  color: var(--text);
  /* Un número de chasis o una dirección larga no tienen dónde cortar:
   * sin esto ensanchan la columna de la grilla y desalinean el renglón. */
  overflow-wrap: anywhere;
}

/* El campo que falta para poder imprimir. Se marca DESDE QUE SE ABRE el
 * formulario y se apaga solo en cuanto el campo recibe algo, campo por
 * campo mientras se tipea.
 *
 * Hasta 2026-09-18 se marcaba recién al intentar imprimir, para que el
 * formulario no se leyera como que ya está mal antes de que nadie lo
 * hubiera tocado. El problema es que "está mal" no es lo que dice este
 * rojo: acá TODOS los campos son obligatorios (ver `appBoletoFaltan()` en
 * lib/boleto.php) y lo que marca es simplemente lo que todavía no está
 * puesto. Con veinte campos repartidos en ocho bloques, un cartel que
 * dice "faltan 7 datos" sin señalar cuáles obliga a recorrer el
 * formulario entero adivinando; con el rojo puesto desde el arranque, lo
 * que falta se ve de un vistazo y el modal se va apagando a medida que se
 * llena, que es el progreso hecho color.
 *
 * SÓLO EL BORDE en reposo. El anillo queda para el campo enfocado: con
 * quince campos vacíos, quince anillos de 3px son una pared roja, y lo
 * que tiene que resaltar es la forma del hueco, no el hueco gritando. */
.hoja-input.is-falta { border-color: var(--danger); }
.hoja-input.is-falta:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 16%, transparent);
}

/* El campo de DOS PARTES: quién carga con los gastos. La respuesta es EL
 * VENDEDOR o EL COMPRADOR y no hay una tercera, así que se muestran las dos
 * y se elige — un campo de texto acá invitaba a escribir "comprador", "El
 * Comprador" o "a cargo del comprador" en el mismo lugar del mismo papel,
 * y la cláusula lo imprime tal cual.
 *
 * Son dos radios de verdad y no dos botones con JS: el teclado, el lector
 * de pantalla y el "uno solo a la vez" vienen del control nativo. Lo que se
 * ve es el `span` —y el tilde de la elegida—; el radio queda transparente
 * detrás, no `display:none`, que en varios navegadores lo saca del
 * recorrido del tabulador.
 *
 * Las dos ocupan la mitad del renglón cada una y no el ancho de su texto:
 * son la misma clase de respuesta y una pastilla más grande que la otra se
 * lee como la opción recomendada. */
.hoja-opciones {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  /* El campo ocupa el renglón entero —el rótulo es largo— pero las
   * pastillas no se estiran hasta los 920px de la tarjeta: dos botones de
   * media pantalla para elegir entre dos palabras se leen como el botón de
   * enviar el formulario, que es el que no hay que confundir. */
  max-width: 460px;
}
.hoja-opcion { position: relative; cursor: pointer; min-width: 0; }
.hoja-opcion input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
/* Mismo borde, radio y alto que un `.hoja-input`: es un campo más del
 * formulario y tiene que formar renglón con los de arriba. */
.hoja-opcion span {
  display: block;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: #fff;
  /* El MISMO texto que adentro de un `.hoja-input` o un `.hoja-dato`:
   * tamaño, interlineado, color y peso normal. Estuvo en 600 mientras la
   * elegida era una pastilla azul —ahí el rótulo competía con el relleno—
   * y con la cara de campo que tiene ahora, la negrita la separaba de los
   * veinte campos con los que forma renglón. */
  color: var(--text);
  font-size: .95rem;
  line-height: 1.25;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: background .15s, border-color .15s, color .15s;
}
/* La ELEGIDA, con la misma cara que un dato que ya está puesto: el fondo
 * suave y sin borde de `.hoja-dato`, y el texto en el azul-tinta de
 * siempre. Era una pastilla azul llena con el texto en blanco, y en un
 * formulario donde lo único azul lleno es el botón de guardar, la
 * respuesta a "quién paga los gastos" se leía como una acción.
 *
 * Lo que dice cuál está elegida pasa a ser el TILDE, no el color: el
 * relleno suave lo comparte con los campos que la cuenta ya completó
 * —que también son "esto ya está resuelto"— y la que no se eligió sigue
 * en blanco con borde, que es la cara de un hueco. */
.hoja-opcion input:checked + span {
  border-color: transparent;
  background: var(--bg);
  color: var(--text);
}
/* El tilde vive en el markup adentro del `span`, pegado al rótulo (ver
 * `appPrepCampo()` en lib/acciones.php), y se prende con el `:checked`.
 *
 * Es INLINE y no absoluto contra el borde derecho: al lado del texto se
 * lee como parte de la respuesta —"EL COMPRADOR ✓"— y no como un adorno
 * de la pastilla. En el color del texto y no en el de marca por lo mismo
 * que el fondo dejó de ser azul: el campo dice un dato, no ofrece una
 * acción.
 *
 * Nace en `opacity: 0` y no en `display: none`: así OCUPA su lugar en las
 * dos, y el rótulo queda en la misma posición esté elegida o no — con el
 * tilde entrando y saliendo del flujo, el texto de las dos pastillas se
 * corría unos píxeles a cada click. */
.hoja-opcion-tilde {
  margin-left: 7px;
  font-size: .9em;
  color: var(--text);
  opacity: 0;
  transition: opacity .15s;
}
.hoja-opcion input:checked + span .hoja-opcion-tilde { opacity: 1; }
/* El foco se dibuja en la pastilla, que es lo que se ve: el radio es
 * transparente y su anillo propio caería sobre nada. */
.hoja-opcion input:focus-visible + span {
  border-color: var(--acento);
  box-shadow: 0 0 0 3px var(--acento-ring);
}
/* Lo que todavía no se eligió, en el mismo rojo que los campos vacíos: acá
 * el grupo es el que lleva `is-falta` —el `data-bol` es el contenedor— y el
 * borde va a las dos pastillas. Se apaga en cuanto se marca una. */
.hoja-opciones.is-falta .hoja-opcion input:not(:checked) + span {
  border-color: var(--danger);
}
/* Documento cerrado: se ve cuál quedó elegida y no se puede cambiar. */
.hoja-opciones.is-bloqueado .hoja-opcion,
.hoja-opciones.is-bloqueado .hoja-opcion input { cursor: default; }
.hoja-opciones.is-bloqueado .hoja-opcion input:not(:checked) + span {
  opacity: .55;
}

/* El cartel de cuántos datos faltan, pegado arriba del botón apagado.
 * Dice el número y no la lista: con quince campos vacíos la lista es más
 * larga que el formulario, y los que faltan ya están marcados en rojo a
 * la vista. */
.hoja-faltan {
  margin: 16px 0 0;
  padding: 10px 12px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--danger) 8%, transparent);
  color: var(--danger);
  font-size: .85rem;
  font-weight: 600;
  text-align: center;
}
.hoja-faltan[hidden] { display: none !important; }

@media (max-width: 420px) {
  .hoja-grid { grid-template-columns: 1fr; }
}

/* El rótulo del mensaje opcional: separa del renglón de arriba, que es
 * otro asunto. */
.hoja-label-sep { margin-top: 16px; }
/* "(opcional)" en peso normal: si fuera igual de fuerte que el rótulo se
 * leería como parte del nombre del campo. */
.hoja-label-opt {
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
/* El mensaje de la visita acompaña a la fecha, no es el contenido del
 * modal: entra con tres renglones para que el botón de enviar no se vaya
 * de la pantalla del teléfono. */
.hoja-textarea-corta { min-height: 74px; }

/* --------- utility --------- */

.center { text-align: center; }
.hide-desktop { display: block; }

@media (min-width: 640px) {
  body { padding: 20px 0; }
  .app-shell {
    box-shadow: var(--shadow-lg);
    border-radius: 32px;
    overflow: hidden;
    min-height: calc(100vh - 40px);
  }
}

/* =================================================================
 * El boleto de compraventa que sale por la impresora
 * =================================================================
 * La hoja (`#boletoImpresion`, ver `appBoletoHoja()` en lib/acciones.php)
 * es hija directa de `<body>` y vive oculta todo el tiempo. El botón del
 * modal de "Coordinar compra" la llena y llama a `window.print()`; lo que
 * sigue es lo que la deja sola en el papel.
 *
 * `body > *:not(#boletoImpresion)` esconde el shell, los seis modales y
 * el menú de un saque, sin tener que enumerarlos. Por eso la hoja tiene
 * que ser HERMANA de todo eso y no descendiente de nada — si colgara del
 * modal, este selector la escondería junto con su padre.
 *
 * Se imprime el propio documento y no una ventana nueva ni un iframe:
 * `window.open()` se lo comen los bloqueadores de pop-ups y el iframe
 * falla en Safari de iOS. Este portal se usa sobre todo desde el
 * teléfono, así que gana el camino que anda en todos lados.
 *
 * ACÁ NO VA EL FORMATO DEL DOCUMENTO, y eso cambió el 2026-09-18g. La
 * tipografía, los márgenes de la hoja y la geometría de sus tablas viajan
 * ADENTRO del documento, en el `<style>` que escribe `appBoletoCss()`
 * (lib/boleto.php), porque el mismo string lo imprime también el panel de
 * la cuenta y tenerlo dos veces —una acá y otra en panel.css— era tener
 * dos papeles distintos para el mismo boleto. Lo que queda es la PÁGINA:
 * esconder los hermanos y mostrar la hoja.
 * ================================================================= */
#boletoImpresion { display: none; }

@media print {
  body > *:not(#boletoImpresion) { display: none !important; }

  /* El modal deja `overflow: hidden` en el body mientras está abierto, y
   * el botón de imprimir se toca justamente con el modal abierto: sin
   * esto, el navegador imprime la primera página y corta el resto. */
  body {
    display: block !important;
    overflow: visible !important;
    padding: 0 !important;
    background: #fff !important;
  }
  /* El fondo del portal queda abajo del `body` y asoma por el borde si el
   * diálogo imprime los gráficos de fondo. Va en blanco, igual que el del
   * panel (ver `body.printing-boleto` en panel.css): el mismo documento no
   * puede salir sobre dos papeles de distinto color. */
  html { background: #fff !important; }

  /* Sólo mostrarla y nombrarle la página: el ancho, la tipografía y
   * cuánto mide el margen los trae el documento (`appBoletoCss()` en
   * lib/boleto.php).
   *
   * El `page` va acá y no adentro del documento porque el navegador toma
   * el nombre de la página de la PRIMERA caja de la hoja, que es ésta —el
   * `.boleto-hoja` del documento cuelga adentro y llega tarde—. Es la
   * única palabra que los dos proyectos comparten con la plantilla, y por
   * eso es la misma de los dos lados. */
  #boletoImpresion { display: block !important; page: boleto-nexel; }

  /* El papel y el margen los fija la `@page boleto-nexel` del documento,
   * que es lo que hace que salga igual acá y en el panel (ver
   * `appBoletoCss()` en lib/boleto.php). Éste queda como red para los
   * navegadores que no soportan páginas con nombre —Safari anterior al
   * 16.4, que en un portal que se usa desde el teléfono todavía aparece—,
   * y va con los MISMOS valores: si la red entra a jugar, el papel no
   * puede cambiar. */
  @page { size: A4 portrait; margin: .5cm; }
}

/* --------- Lista de "Gestionar servicios" --------- */
/* Un renglón por servicio contratable, dentro del modal que abre el botón
 * del menú y el del pie de la ficha (ver `appGestionServiciosModal()` en
 * lib/acciones.php).
 *
 * La misma superficie gris clara que usan los botones de los modales, sin
 * borde: lo que separa un servicio del siguiente es el aire y el cambio de
 * fondo, no una línea. Con borde, tres servicios se leen como una tabla. */
.servicios-lista {
  display: flex; flex-direction: column; gap: 10px;
  margin-bottom: 4px;
}
/* El `display: flex` de arriba le gana al `[hidden] { display: none }` del
 * navegador, que es una regla de la hoja del user-agent y pierde contra
 * cualquier clase. Sin esta línea, `lista.hidden = true` deja el atributo
 * puesto y el bloque EN PANTALLA: el modal dibuja la lista y la
 * confirmación a la vez, una debajo de la otra.
 *
 * Es el mismo remiendo que ya lleva `.aviso-acciones[hidden]` más arriba,
 * por el mismo motivo. Cualquier bloque de estos que reciba un `display`
 * propio necesita su par. */
.servicios-lista[hidden],
.servicios-confirmar[hidden] { display: none; }
/* Y lo mismo con el "Cerrar" de la lista, que el paso 2 apaga: `.auth-btn`
 * trae `display: inline-flex` y le gana igual al `[hidden]`. Acá alcanza
 * con la especificidad del atributo — no hace falta `!important`. */
[data-servicios-cerrar][hidden] { display: none; }
.servicio-item {
  background: #f1f5f9;
  border-radius: var(--radius-md);
  padding: 12px 14px;
}
/* Cada servicio es un BOTÓN: se toca para contratarlo. Hereda la caja de
 * `.servicio-item` y sólo agrega lo que un `<button>` trae de fábrica y
 * hay que desarmar —borde, fuente y centrado— más las señales de que se
 * puede tocar.
 *
 * El texto va a la izquierda porque adentro hay un párrafo de dos o tres
 * renglones: centrado se lee como un cartel y no como una opción de una
 * lista. */
.servicio-item-btn {
  display: block; width: 100%;
  border: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background .15s, transform .1s;
}
.servicio-item-btn:hover  { background: #e2e8f0; }
.servicio-item-btn:active { transform: scale(.99); }
/* El foco visible, que en un `<button>` sin borde desaparece: es la única
 * señal de por dónde va quien navega con el teclado. */
.servicio-item-btn:focus-visible {
  outline: 2px solid var(--acento);
  outline-offset: 2px;
}
/* El mismo ítem dentro de la confirmación, donde ya NO se toca: es el
 * recordatorio de qué se está por contratar. Sin hover ni cursor. */
.servicio-item-elegido { margin-bottom: 4px; }
/* Nombre y precio en la misma línea y pegados a los extremos: son los dos
 * datos que deciden, y enfrentarlos evita tener que buscar el precio
 * abajo. `gap` para que un nombre largo no se pegue al número al
 * envolver. */
.servicio-item-cab {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
}
.servicio-item-nombre  { font-size: .95rem; color: var(--text); }
/* El precio en el acento: es lo que el ojo va a buscar. */
.servicio-item-precio  { font-size: .95rem; font-weight: 700; color: var(--acento);
                         white-space: nowrap; }
.servicio-item-desc    { margin: 6px 0 0; font-size: .85rem; line-height: 1.45;
                         color: var(--muted); }

/* El cartel de "buscando", mientras el modal espera a /api-servicios. */
.servicios-cargando {
  padding: 18px 0; text-align: center;
  font-size: .88rem; color: var(--muted);
}

/* --------- Paso 3: configurar el servicio --------- */
/* El formulario lo define el back office por prestación, así que acá no
 * hay un campo escrito: sólo el molde de cualquiera de ellos. */
.servicios-configurar[hidden] { display: none; }
.servicios-campos   { display: flex; flex-direction: column; gap: 12px;
                      margin: 4px 0 8px; }
.servicios-campos:empty { margin: 0; }
.servicio-campo     { display: flex; flex-direction: column; gap: 5px; }
.servicio-campo-label { font-size: .82rem; font-weight: 700; color: var(--text); }
/* El asterisco de obligatorio en el acento, como el resto de lo que el
 * portal marca como requerido. */
.servicio-campo-req { color: var(--acento); }
.servicio-campo-ctrl {
  width: 100%;
  padding: 11px 12px;
  border: 1px solid #dfe3ee;
  border-radius: var(--radius-md);
  font: inherit; font-size: .9rem;
  background: #fff; color: var(--text);
  outline: none;
}
.servicio-campo-ctrl:focus { border-color: var(--acento); }
.servicio-campo-ayuda { font-size: .78rem; color: var(--muted); }
/* El campo obligatorio que quedó vacío al intentar enviar. */
/* El halo sale del token y no de un rgba suelto: así sigue al acento si
 * el proyecto lo cambia. */
.campo-invalido { border-color: var(--acento) !important;
                  box-shadow: 0 0 0 2px color-mix(in srgb, var(--acento) 22%, transparent); }

/* =======================================================================
 * Nexo — el chat del asistente (ver lib/nexo.php)
 *
 * El chasis es el de las hojas de gesto (`.hoja-modal` / `.hoja-card`):
 * mismo velo, misma tarjeta subiendo desde abajo, mismo cierre. Lo de acá
 * es lo que una hoja no tiene — un hilo que crece y una barra de escritura
 * anclada al pie — y por eso la tarjeta pasa a ser una columna de altura
 * fija: el que scrollea es el hilo, no la tarjeta.
 * ===================================================================== */

/* ---------------------------------------------------------------
 * Una ventana flotante, no una pantalla
 * ---------------------------------------------------------------
 * En el teléfono las hojas de gesto suben desde abajo pegadas a los tres
 * bordes, y está bien: son UN gesto sobre la página —ofertar, pedir una
 * visita— que empieza y termina. El chat no es un gesto sino un lugar
 * donde uno se queda, y pegado a los bordes se lee como si hubiera
 * reemplazado la página: no queda pista de que atrás sigue el aviso que se
 * estaba mirando.
 *
 * Cinco píxeles alcanzan para decirlo. No son aire decorativo: son el
 * borde de la página de atrás asomando por los cuatro lados, que es todo
 * lo que hace falta para que esto se lea como algo apoyado encima.
 *
 * El padding vive en el MODAL y no como margen de la tarjeta porque el
 * modal es quien tiene la altura real de la ventana (`--vv-alto`, ver la
 * cabecera de `.hoja-modal`): con `box-sizing: border-box` el alto
 * disponible ya viene descontado y la tarjeta pide `height: 100%` sin
 * calcular nada.
 *
 * En escritorio no hace falta —ahí el modal ya flota centrado con 20px y
 * 420 de ancho— así que el bloque de abajo repone esos valores. Y hay que
 * reponerlos a mano: este `.nexo-modal` no está dentro de ninguna media
 * query y va después, así que le gana al `.hoja-modal` de la de 640. */
.nexo-modal { padding: 5px; }

.nexo-card {
  display: flex;
  flex-direction: column;
  /* Las cuatro esquinas redondeadas. Con las de abajo rectas la tarjeta se
   * sigue leyendo como apoyada en el borde, aun con los 5px. */
  border-radius: var(--radius-lg);
  /* Más apretado que el resto de las hojas (16/18/20). Con el teclado
   * abierto la tarjeta se queda con la mitad de la pantalla —el modal
   * sigue a `--vv-alto`— y ahí cada píxel de padding es una línea de
   * conversación menos. Una hoja de gesto muestra un formulario corto y
   * puede darse el lujo del aire; un chat no.
   *
   * Abajo `max()` y no una suma: sobre el indicador del iPhone manda el
   * `env()` —el campo no puede quedar debajo— pero en todo lo demás mandan
   * los 10px, en vez de sumar 10 a 34 y perder media pulgada.
   *
   * Arriba va CERO y el aire se lo pone la cabecera (ver más abajo): si lo
   * pusiera acá, quedaría afuera de la franja del título y el título se
   * vería hundido dentro de ella. */
  padding: 0 10px max(10px, env(safe-area-inset-bottom, 0px));
  /* Todo el alto que el modal dejó libre. Antes era 82dvh —el recorte que
   * dejaba ver algo de la página de atrás— y ahora ese trabajo lo hace el
   * margen de los 5px, que además lo hace por los cuatro lados. */
  height: 100%;
  max-height: 100%;
  overflow: hidden;   /* el scroll es del hilo */
}
@media (min-width: 640px) {
  .nexo-modal { padding: 20px; }
  .nexo-card  { height: 560px; max-height: 80%; }
}

.nexo-hilo {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;   /* que el rebote no arrastre la página */
  /* Arriba alcanza con separar la primera burbuja de la sombra del título
   * (eran 14px). A los costados casi nada: el ancho de la burbuja es lo que
   * decide cuántas palabras entran por línea.
   *
   * Abajo eran 4px, y sumados a los 8 de la barra daban 12 contra el campo:
   * dos píxeles más que el `gap` de 10 que separa una burbuja de la otra. El
   * ojo no lee esa diferencia, así que el campo quedaba como un renglón más
   * del hilo en vez de una franja aparte. Con 14 el total son 22 —el doble
   * del gap— y la barra vuelve a leerse como otra cosa.
   *
   * El aire va ACÁ y no en el `padding-top` de la barra: la sombra de la
   * costura cuelga del borde de la barra (`bottom: 100%`), así que crecerla
   * por ahí se lleva la sombra para arriba y deja las burbujas
   * desapareciendo en una banda en blanco por encima del campo. Puesto en el
   * scroll, el colchón viaja con la conversación —`scrollHeight` lo cuenta,
   * el pie del hilo sigue llegando al fondo— y la sombra se queda pegada
   * donde empieza lo fijo. */
  padding: 8px 2px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.nexo-msg { display: flex; }
.nexo-msg-yo  { justify-content: flex-end; }
.nexo-msg-bot { justify-content: flex-start; }

.nexo-burbuja {
  max-width: 84%;
  padding: 10px 13px;
  border-radius: 16px;
  font-size: .92rem;
  line-height: 1.45;
  white-space: pre-wrap;   /* respeta los saltos que mande el modelo */
  word-break: break-word;
}
.nexo-msg-bot .nexo-burbuja {
  background: var(--bg);
  color: var(--text);
  border-bottom-left-radius: 5px;
}
.nexo-msg-yo .nexo-burbuja {
  background: var(--primario);
  color: #fff;
  border-bottom-right-radius: 5px;
}

/* La fecha y la hora, al pie de cada burbuja.
 *
 * Lo más chico que se sostiene: es un dato de consulta, no de lectura.
 * Tiene que estar cuando se lo va a buscar y desaparecer cuando no, y
 * eso lo hace el tamaño tanto como el color.
 *
 * `nowrap` porque la burbuja hereda `pre-wrap` y el sello es una sola
 * unidad: en un mensaje de dos palabras la burbuja se ensancha hasta que
 * entre, en vez de partir la hora abajo de la fecha.
 *
 * El color es el del texto de su propia burbuja al 25% —negro sobre el
 * gris, blanco sobre el azul— y va escrito a mano, no con `opacity`: así
 * se destiñe el sello solo y no arrastra nada más. */
.nexo-sello {
  display: block;
  margin-top: 2px;
  font-size: .6rem;
  line-height: 1.2;
  text-align: right;
  white-space: nowrap;
}
.nexo-msg-bot .nexo-sello { color: rgba(0, 0, 0, .25); }
.nexo-msg-yo  .nexo-sello { color: rgba(255, 255, 255, .25); }

/* Los tres puntitos mientras el endpoint piensa. */
.nexo-pensando { display: inline-flex; gap: 4px; align-items: center; padding: 13px; }
.nexo-pensando span {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--muted);
  animation: nexoLatido 1.2s infinite ease-in-out;
}
.nexo-pensando span:nth-child(2) { animation-delay: .15s; }
.nexo-pensando span:nth-child(3) { animation-delay: .3s; }
@keyframes nexoLatido {
  0%, 60%, 100% { opacity: .25; transform: translateY(0); }
  30%           { opacity: 1;   transform: translateY(-3px); }
}
/* Quien pidió menos movimiento no necesita ver saltar los puntitos para
 * entender que está esperando: el cambio de opacidad alcanza. */
@media (prefers-reduced-motion: reduce) {
  .nexo-pensando span { animation-duration: 0s; opacity: .5; }
}

/* Los avisos que acompañan a una respuesta. Van fuera de la burbuja: son
 * del catálogo, no de lo que Nexo dijo. */
.nexo-avisos { display: flex; flex-direction: column; gap: 8px; }

.nexo-aviso {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--borde, rgba(16, 22, 54, .1));
  border-radius: 12px;
  text-decoration: none;
  color: inherit;
}
.nexo-aviso:active { transform: scale(.99); }
.nexo-aviso-foto {
  width: 54px; height: 54px;
  border-radius: 9px;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--bg);
}
.nexo-aviso-txt { min-width: 0; }
.nexo-aviso-titulo {
  font-size: .86rem; font-weight: 600; color: var(--text);
  /* Dos líneas y corta: los títulos del catálogo llegan a tener veinte
   * palabras y la tarjeta no puede crecer con ellos. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.nexo-aviso-precio { font-size: .86rem; font-weight: 700; color: var(--acento); margin-top: 2px; }

.nexo-ver-todos {
  align-self: flex-start;
  font-size: .85rem; font-weight: 600;
  color: var(--primario);
  text-decoration: none;
  padding: 4px 2px;
}

/* Las frases de ejemplo de la pantalla en blanco. */
.nexo-sugerencias {
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 8px 0 2px;
}
.nexo-sugerencias[hidden] { display: none; }
.nexo-chip {
  background: var(--bg);
  border: none;
  color: var(--text);
  font-size: .8rem;
  padding: 7px 11px;
  border-radius: 999px;
}

/* Barra de escritura, anclada al pie de la tarjeta. */
.nexo-barra {
  display: flex; align-items: center; gap: 8px;
  /* Lo justo para despegar el campo de la sombra que cae encima. */
  padding-top: 8px;
  flex-shrink: 0;
}
.nexo-input {
  flex: 1;
  background: var(--bg);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 11px 15px;
  /* 16px o más: abajo de eso Safari en iPhone hace zoom al enfocar el
   * campo y la pantalla queda corrida. */
  font-size: 16px;
  color: var(--text);
}
.nexo-input:focus {
  outline: none;
  border-color: var(--primario);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primario) 15%, transparent);
}
.nexo-input:disabled { opacity: .6; }
.nexo-enviar {
  width: 42px; height: 42px;
  flex-shrink: 0;
  border: none; border-radius: 50%;
  background: var(--primario);
  color: #fff;
  font-size: .95rem;
  display: inline-flex; align-items: center; justify-content: center;
}
.nexo-enviar:disabled { opacity: .5; }

/* -----------------------------------------------------------------
 * Las dos costuras — dónde termina lo fijo y empieza lo que scrollea
 * -----------------------------------------------------------------
 * La tarjeta tiene tres franjas y sólo la del medio se mueve: el título
 * arriba, el hilo en el medio y la barra de escritura abajo. Sin nada que
 * las separe las tres se leen como una sola superficie blanca, y cuando el
 * hilo scrollea el texto aparece y desaparece en un borde que no está
 * dibujado en ninguna parte.
 *
 * Una sombra por costura lo dice sin agregar una línea: la de arriba cae
 * del título hacia el hilo y la de abajo sube de la barra hacia el hilo.
 * Las dos apuntan al medio, que es la manera de decir que lo de afuera
 * está fijo y encima.
 *
 * ═══ POR QUÉ NO ES UN `box-shadow` ═══
 *
 * Porque tiene que APAGARSE hacia los costados: fuerte donde está el
 * contenido y desvaneciéndose hacia los dos laterales, para que se lea
 * como una sombra que cae y no como una regla de lado a lado. Un
 * `box-shadow` es parejo a lo largo de todo el borde y no sabe hacer eso.
 *
 * Son dos degradés cruzados sobre un pseudo-elemento:
 *   · el `background` hace la caída VERTICAL — opaco contra la franja fija,
 *     transparente a los 10px;
 *   · el `mask-image` hace la caída HORIZONTAL — recorta los dos extremos
 *     en degradé, así la sombra llega al costado ya apagada.
 *
 * El pseudo va en la franja FIJA y se asoma sobre el hilo (`top: 100%` /
 * `bottom: 100%`), nunca dentro del hilo: ahí adentro scrollearía con las
 * burbujas y la sombra viajaría con el contenido.
 *
 * `left/right: 0` es el ancho del CONTENIDO, no el de la tarjeta — los
 * 18px de padding lateral quedan afuera. Es a propósito y evita un número
 * mágico atado a `.hoja-card`: la sombra empieza donde empieza lo que
 * tapa.
 *
 * La de abajo cuelga de los chips cuando están —son parte de la franja
 * fija— y de la barra cuando no. Por eso los dos selectores.
 * ----------------------------------------------------------------- */
.nexo-card .hoja-head,
.nexo-sugerencias,
.nexo-barra {
  position: relative;
  /* Las tres franjas por encima del hilo: sin esto el hilo, que viene
   * después en el documento, pinta arriba de las sombras. */
  z-index: 1;
}

/* ---------------------------------------------------------------
 * El aire del título vive ACÁ y no en la tarjeta
 * ---------------------------------------------------------------
 * `.hoja-head` centra sus dos hijos —el título y la X— en su propia caja,
 * y eso funciona. Lo que se veía torcido era otra cosa: la franja blanca
 * que lee el ojo no es la cabecera sola, sino desde el borde de la tarjeta
 * hasta la sombra, y ahí adentro el título estaba pegado abajo. Con el
 * padding arriba en la tarjeta quedaban ~20px de blanco sobre el texto y
 * ~10px debajo: centrado en su caja, hundido en la franja.
 *
 * Mover esos 10px de la tarjeta a la cabecera (5 y 5) mete el aire ADENTRO
 * de la franja y lo reparte. La altura total no cambia —eran 10+32, ahora
 * son 5+32+5— así que no se pierde ni un píxel de conversación, que es lo
 * contrario de lo que costaría empujar el título con un margen.
 *
 * Y la sombra, que cuelga de `top: 100%`, baja con el padding: sigue
 * cayendo justo donde termina lo fijo. */
.nexo-card .hoja-head { padding: 5px 0; }

/* El ícono, un pelo más arriba.
 *
 * Su caja ya está centrada contra el texto —medido: un tercio de píxel de
 * diferencia— pero no se ve centrado, y las dos cosas pueden ser ciertas a
 * la vez. La tinta del robot cruza la línea de base (el cuerpo baja) y la
 * de "Nexo" no baja de ahí; encima su masa está abajo, porque arriba sólo
 * tiene la antena. Centrar dos siluetas así por el alto de su recuadro las
 * deja ópticamente desparejas.
 *
 * Un píxel es la corrección, y es óptica: no hay número que la derive. */
.nexo-card .hoja-title i { position: relative; top: -1px; }

.nexo-card .hoja-head::after,
.nexo-sugerencias:not([hidden])::before,
.nexo-sugerencias[hidden] + .nexo-barra::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: 12px;
  pointer-events: none;   /* es decoración: no se come un toque del hilo */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 15%,
                                                #000 85%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0%, #000 15%,
                                                #000 85%, transparent 100%);
}

/* El mismo azul tinta de las otras sombras del proyecto (ver `.hoja-card`
 * y `.hoja-backdrop`), que es más cálido que un gris neutro y no ensucia
 * el blanco de la tarjeta.
 *
 * El 22% reemplaza al 13% original: con aquel, la franja fija y el hilo
 * se leían como una sola superficie salvo que uno fuera a buscar el
 * borde. La caída sigue siendo la misma —opaca contra lo fijo, nada a
 * los 12px— así que lo que cambia es cuánto se ve, no cuánto invade. */
.nexo-card .hoja-head::after {
  top: 100%;
  background: linear-gradient(to bottom, rgba(16, 22, 54, .22), rgba(16, 22, 54, 0));
}
.nexo-sugerencias:not([hidden])::before,
.nexo-sugerencias[hidden] + .nexo-barra::before {
  bottom: 100%;
  background: linear-gradient(to top, rgba(16, 22, 54, .22), rgba(16, 22, 54, 0));
}

/* =================================================================
 * Publicar — el asistente y la lista de publicaciones propias
 * ================================================================= */

/* Acá vivía la tira de pasos (1 Qué · 2 Datos · 3 Fotos) que iba arriba
 * de todo. Se sacó: cada pantalla del asistente se presenta sola con su
 * título, sin contar en cuál de tres va. Ver appPubPasos() en
 * lib/publicaciones.php. */

/* --------- pasos 1 y 2: el rubro y la operación --------- */

/* Las dos primeras pantallas del asistente tienen la MISMA forma
 * —tarjetas grandes, una por respuesta— y por eso comparten estas
 * reglas. Son las dos preguntas de pocas respuestas, y contestarlas con
 * un desplegable sería tres toques para elegir entre dos o tres cosas.
 *
 * Lo que cambia entre una y otra es de qué están hechas: la del rubro
 * son `<a>` (elegir no escribe nada, sólo cambia qué se pregunta
 * después) y la de la operación son `<button type=submit>` de un mismo
 * formulario, porque contestarla CREA EL BORRADOR. Ver publicar.php. */
.pub-rubros { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* La operación, que desde 2026-09-28 tiene TRES respuestas: en dos
 * columnas la tercera queda sola y descolgada, y "Alquiler temporal" no
 * entra en media pantalla de teléfono sin partirse. Apiladas entran
 * enteras y se leen como una lista de opciones, que es lo que son. */
.pub-rubros.is-columna { grid-template-columns: 1fr; }
.pub-rubros.is-columna .pub-rubro {
  /* En una sola columna la tarjeta no necesita 22px arriba y abajo: con
   * el ícono al lado del texto, en vez de encima, la fila queda del alto
   * de un renglón doble y las tres entran sin scrollear.
   *
   * Grid y no flex —pisando el `display:flex` de la regla base— porque
   * el ícono tiene que ocupar las dos filas del texto (rótulo arriba,
   * bajada abajo) y quedar centrado contra las dos. */
  display: grid; grid-template-columns: 34px 1fr;
  align-items: center; text-align: left;
  gap: 2px 14px; padding: 16px 18px;
}
.pub-rubros.is-columna .pub-rubro-icon  { font-size: 1.5rem; grid-row: span 2; }
.pub-rubros.is-columna .pub-rubro-label { align-self: end; }
.pub-rubros.is-columna .pub-rubro-sub   { align-self: start; }
.pub-rubro {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 22px 12px;
  background: var(--bg);
  border-radius: var(--radius-md);
  text-align: center;
  transition: box-shadow .15s, transform .1s;
  /* El reset del <button>: sin esto el de la operación viene con borde,
   * fondo del sistema y la fuente del navegador, y las dos pantallas
   * dejan de verse iguales. */
  border: none;
  font-family: inherit;
  cursor: pointer;
  width: 100%;
}
.pub-rubro:hover  { box-shadow: var(--shadow-md); }
.pub-rubro:active { transform: scale(.985); }
.pub-rubro-icon   { font-size: 1.9rem; color: var(--primario); }
.pub-rubro-label  { font-size: .95rem; font-weight: 700; color: var(--text); }
.pub-rubro-sub    { font-size: .75rem; color: var(--muted); }
/* La que ya está elegida, cuando se vuelve a esta pantalla desde las
 * fotos. No la deshabilita —volver a tocarla es válido y sigue de
 * largo—: sólo dice cuál está puesta, que es lo que uno viene a
 * comprobar cuando retrocede. */
.pub-rubro.is-elegida {
  background: var(--acento-soft);
  box-shadow: inset 0 0 0 2px var(--acento);
}
.pub-rubro.is-elegida .pub-rubro-icon { color: var(--acento); }

/* --------- paso 3: el formulario del rubro --------- */

.pub-form   { display: flex; flex-direction: column; gap: 14px; }

/* Las secciones del formulario (ver lib/publicar-form.php).
 *
 * Son las PESTAÑAS del modal de alta del panel apiladas en un scroll:
 * la primera abierta y con encabezado fijo, y las demás como `.pub-mas`,
 * el mismo <details> que antes envolvía a "Más datos".
 *
 * La abierta NO es un <details> con `open`: una flechita al lado del
 * único bloque obligatorio invita a esconder justo lo que hay que
 * contestar. Por eso son dos markups y no uno con un atributo. */
.pub-seccion {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.pub-seccion-head {
  /* Mismo alto y mismo peso que el <summary> de `.pub-mas`, para que la
   * primera sección y las plegadas se lean como la misma pieza. Lo único
   * que las separa es que ésta no tiene chevron. */
  padding: 13px 14px;
  font-size: .88rem; font-weight: 700; color: var(--text);
  display: flex; align-items: center; gap: 8px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.pub-seccion-head i { color: var(--primario); }
.pub-seccion-cuerpo {
  padding: 16px 14px;
  display: flex; flex-direction: column; gap: 14px;
}

/* La cola de la primera sección: hoy sólo el select de operación, y sólo
 * en la edición — en el alta ahí adentro hay un <input type="hidden"> y
 * nada más.
 *
 * `display: contents` y no un div normal: el envoltorio desaparece de la
 * caja y su contenido pasa a ser hijo directo del flex de `.pub-form`.
 * Sin eso, el div con el hidden adentro sigue contando como ítem del
 * flex y deja un `gap` de 14px de aire donde no hay nada. Tampoco sirve
 * `:empty`, porque con el hidden adentro el div no está vacío. */
.pub-seccion-cola { display: contents; }
.pub-campo  { display: flex; flex-direction: column; gap: 5px; }
.pub-label  { font-size: .82rem; font-weight: 700; color: var(--text); }
.pub-req    { color: var(--acento); margin-left: 2px; }
.pub-ctrl {
  width: 100%;
  padding: 12px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: .95rem;
}
.pub-ctrl:focus { outline: 2px solid var(--primario); outline-offset: -1px; }
.pub-campo.is-mal .pub-ctrl { border-color: #dc2626; }
.pub-ayuda  { font-size: .75rem; color: var(--muted); line-height: 1.4; }
.pub-error  { font-size: .78rem; color: #dc2626; font-weight: 600; }

/* El precio con su moneda pegada: son un solo dato. */
.pub-precio { display: flex; gap: 8px; }
.pub-moneda {
  flex-shrink: 0; width: 76px;
  padding: 12px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  font-family: inherit; font-size: .95rem; font-weight: 700;
  color: var(--text);
}

/* La unidad (m2, km, HP) adentro del campo, contra el borde derecho. El
 * padding del input le hace lugar para que el número no se le meta
 * debajo. */
.pub-conSufijo { position: relative; }
.pub-conSufijo .pub-ctrl { padding-right: 52px; }
.pub-sufijo {
  position: absolute; right: 12px; top: 50%;
  transform: translateY(-50%);
  font-size: .8rem; color: var(--muted); pointer-events: none;
}

/* "Más datos". Un <details> nativo: se pliega sin una línea de JS y el
 * navegador ya le da el teclado y el rol correctos. */
.pub-mas {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.pub-mas > summary {
  padding: 13px 14px;
  cursor: pointer;
  font-size: .88rem; font-weight: 700; color: var(--text);
  display: flex; align-items: center; gap: 8px;
  list-style: none;
}
.pub-mas > summary::-webkit-details-marker { display: none; }
.pub-mas > summary::after {
  content: "\f078";                    /* chevron-down de Font Awesome */
  font-family: "Font Awesome 6 Free"; font-weight: 900;
  margin-left: auto;
  font-size: .7rem; color: var(--muted);
  transition: transform .15s;
}
.pub-mas[open] > summary::after { transform: rotate(180deg); }
.pub-mas-sub  { font-size: .72rem; font-weight: 500; color: var(--muted); }
.pub-mas-cuerpo {
  padding: 4px 14px 16px;
  display: flex; flex-direction: column; gap: 14px;
}

.pub-acciones { display: flex; gap: 10px; margin-top: 4px; }
.pub-acciones .auth-btn { margin-top: 0; }

/* --------- paso 3: las fotos --------- */

.pub-fotos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.pub-foto {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
}
.pub-foto img { width: 100%; height: 100%; object-fit: cover; }
/* La primera es la portada y se dice, no se deduce: en una grilla sin
 * orden evidente nadie sabe cuál va a salir en el listado. */
.pub-foto:first-child::after {
  content: "Portada";
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 3px 6px;
  background: rgba(16, 22, 54, .72);
  color: #fff; font-size: .65rem; font-weight: 700;
  text-align: center;
}
.pub-foto-x {
  position: absolute; top: 4px; right: 4px;
  width: 24px; height: 24px;
  border: none; border-radius: 50%;
  background: rgba(16, 22, 54, .72);
  color: #fff; font-size: .7rem;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.pub-foto-x:disabled { opacity: .5; }

.pub-subir {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 18px;
  border: 2px dashed var(--border);
  border-radius: var(--radius-md);
  color: var(--primario);
  font-size: .9rem; font-weight: 700;
  cursor: pointer;
}
.pub-subir:hover { border-color: var(--primario); }
.pub-subir.is-subiendo { opacity: .6; pointer-events: none; }

/* El cartel de "está en revisión". Mismo tono que el resto de los avisos
 * informativos del portal, en la franja suave del acento. */
.pub-nota {
  display: flex; align-items: flex-start; gap: 10px;
  margin-top: 14px; padding: 12px 14px;
  border-radius: var(--radius-md);
  background: var(--bg);
  font-size: .82rem; color: var(--text); line-height: 1.45;
}
.pub-nota i { color: var(--acento); margin-top: 2px; }

/* El aviso de que el formulario viene prellenado desde las fotos. Va en
 * el primario y no en el acento del resto de las notas: las otras dicen
 * "esperá" o "falta algo" —cosas que pasan a pesar de uno— y ésta dice
 * qué acaba de hacer el sistema. Es la única nota de la pantalla, así
 * que no compite con ninguna. Ver lib/vision.php. */
.pub-nota-vision {
  background: color-mix(in srgb, var(--primario) 8%, #fff);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primario) 20%, transparent);
}
.pub-nota-vision i { color: var(--primario); }

/* El botón de "Continuar" mientras se leen las fotos. No se deshabilita
 * —sigue siendo un link con `href`, y si el JS no corriera tiene que
 * poder tocarse— así que lo que se apaga es el puntero, y el texto lo
 * cambia el script. El pulso es lo único que dice que algo está pasando
 * en un paso que puede tardar unos segundos. */
.pub-acciones .auth-btn.is-analizando {
  pointer-events: none;
  animation: pubPulso 1.2s ease-in-out infinite;
}
@keyframes pubPulso { 50% { opacity: .62; } }

/* --------- la lista de publicaciones propias --------- */

.pub-nuevo { margin-bottom: 12px; }

.pub-item { display: flex; flex-direction: column; gap: 10px; }

/* El aviso a medio cargar, el que dejó el asistente sin terminar. Va
 * atenuado y con filete punteado a la izquierda: en una lista de
 * publicaciones de verdad tiene que leerse como lo que es —algo
 * pendiente, no un aviso más— sin llegar a parecer un error. El punteado
 * es el mismo vocabulario que `.pub-subir`, que también marca un hueco
 * que falta llenar. Ver mi-cuenta.php. */
.pub-item.is-sin-terminar {
  border-left: 3px dashed var(--border);
  padding-left: 11px;
}
.pub-item.is-sin-terminar .act-foto { opacity: .65; }
.pub-item.is-sin-terminar .act-titulo { color: var(--muted); }

.pub-revision {
  display: flex; align-items: center; gap: 7px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  font-size: .78rem; font-weight: 600; color: var(--muted);
}
.pub-revision i { color: var(--acento); }

.pub-stats {
  display: flex; flex-wrap: wrap; gap: 14px;
  font-size: .78rem; color: var(--muted);
}
.pub-stats i { margin-right: 3px; }

.pub-botones {
  display: flex; flex-wrap: wrap; gap: 6px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}
.pub-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 12px;
  border: none; border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text);
  font-family: inherit; font-size: .8rem; font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.pub-btn::-webkit-details-marker { display: none; }
.pub-btn:hover { background: var(--border); }
/* El que hay que tocar en la fila. Hoy sólo lo usa "Seguir cargando" del
 * aviso sin terminar: esa tarjeta existe para eso, y con los tres
 * botones iguales no se distinguiría de qué se trata. */
.pub-btn.is-principal {
  background: var(--acento);
  color: #fff;
}
.pub-btn.is-principal:hover { background: var(--acento-h); }
/* Descartar. El rojo va sólo en el texto: rellenar el botón lo pondría
 * al mismo peso visual que la acción que sí hay que hacer. */
.pub-btn.is-mal { color: #b91c1c; }
.pub-btn.is-mal:hover { background: #fee2e2; }

/* El menú de los estados. `position: relative` sobre el <details> y el
 * panel absoluto encima. */
.pub-menu { position: relative; margin-left: auto; }
.pub-menu-panel {
  position: absolute; right: 0; bottom: calc(100% + 6px);
  z-index: 20;
  min-width: 190px;
  padding: 6px;
  background: var(--surface);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 36px rgba(16, 22, 54, .22);
}
.pub-menu-item {
  width: 100%;
  display: flex; align-items: center; gap: 9px;
  padding: 10px 11px;
  border: none; border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font-family: inherit; font-size: .84rem; font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.pub-menu-item:hover  { background: var(--bg); }
.pub-menu-item i      { width: 16px; text-align: center; color: var(--muted); }
.pub-menu-item.is-mal { color: #dc2626; }
.pub-menu-item.is-mal i { color: #dc2626; }
.pub-menu-item:disabled { opacity: .5; }

/* El campo de texto que aparece cuando la marca o el modelo no estan en
 * el catalogo. Va debajo de su select, separado apenas: se lee como la
 * continuacion de ese control y no como un campo mas del formulario. */
.pub-ctrl-libre { margin-top: 6px; }
.pub-ctrl-libre[hidden] { display: none; }

/* Las caracteristicas: casillas en grilla, agrupadas por categoria. La
 * etiqueta entera es el area clickeable —no solo el cuadradito— que en
 * un telefono es la diferencia entre marcarla y errarle. */
.pub-caracs-grupo  { margin-bottom: 14px; }
.pub-caracs-titulo {
  font-size: .75rem; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
  margin-bottom: 7px;
}
.pub-caracs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 6px;
}
.pub-carac {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  font-size: .82rem; color: var(--text);
  cursor: pointer;
}
.pub-carac input { flex-shrink: 0; accent-color: var(--primario); }
.pub-carac i     { color: var(--muted); width: 15px; text-align: center; }
.pub-carac:has(input:checked) { background: var(--border); font-weight: 600; }

/* La pantalla de cierre del asistente. Centrada y con poco adentro: lo
 * unico que tiene que hacer es confirmar que quedo cargada y decir que
 * sigue. */
.pub-fin { text-align: center; }
.pub-fin-tilde {
  width: 64px; height: 64px;
  margin: 4px auto 14px;
  border-radius: 50%;
  background: #16a34a;
  color: #fff; font-size: 1.7rem;
  display: flex; align-items: center; justify-content: center;
}
.pub-fin-titulo { font-size: 1.15rem; font-weight: 800; color: var(--text); margin: 0 0 4px; }
.pub-fin-sub    { font-size: .88rem; color: var(--muted); margin: 0 0 16px; }
.pub-fin .pub-nota { text-align: left; }
.pub-fin-acciones {
  display: flex; flex-direction: column; gap: 8px;
  margin-top: 16px;
}
.pub-fin-acciones .auth-btn { margin-top: 0; }

/* La fila observada: hay algo concreto para corregir, no es una espera.
 * Cambia de gris a la franja del acento para que se distinga de un
 * "en revision" comun sin gritar como un error del sistema. */
.pub-revision.is-observada {
  align-items: flex-start;
  background: #fff7ed;
  color: #9a3412;
  font-weight: 500;
  line-height: 1.45;
}
.pub-revision.is-observada i      { color: #ea580c; margin-top: 2px; }
.pub-revision.is-observada strong { display: block; }

/* El estado de moderacion de cada foto: un disco arriba a la izquierda,
 * con un glifo y sin texto.
 *
 * Un disco y no una franja con rotulo porque son miniaturas de 96px en
 * grilla: el texto de una franja entra a la fuerza, se corta, y encima
 * tapa justo la parte de abajo de la foto. El glifo se lee de un vistazo
 * y el rotulo completo va en el `title`.
 *
 * Arriba a la IZQUIERDA para no chocar con el boton de quitar, que vive
 * en la derecha desde siempre.
 *
 * Los tres estados llevan sello, incluida la aprobada: sin el, "sin
 * marca" tendria que interpretarse como "esta todo bien", y eso es
 * justo lo que no se entiende cuando otra foto al lado si tiene marca. */
.pub-foto-sello {
  position: absolute; top: 4px; left: 4px;
  width: 22px; height: 22px;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .62rem;
  color: #fff;
  box-shadow: 0 1px 4px rgba(16, 22, 54, .35);
  pointer-events: none;
}
.pub-foto-sello.is-ok     { background: #16a34a; }
.pub-foto-sello.is-espera { background: #64748b; }
.pub-foto-sello.is-mal    { background: #dc2626; }

/* La rechazada se atenua ademas de sellarse: en una grilla de seis
 * miniaturas, un disco de 22px solo no alcanza para que se vea cual es
 * la que no esta saliendo. */
.pub-foto.is-mal img { opacity: .45; filter: grayscale(.4); }

/* Texto solo para lectores de pantalla: el sello es un glifo y sin esto
 * no diria nada en voz alta. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* La portada la marca la primera APROBADA, no la primera a secas. */
.pub-foto:first-child::after { content: none; }
.pub-foto.is-ok:first-child::after { content: 'Portada'; }

/* La nota del motivo, en el tono de la objecion. */
.pub-nota-mal {
  background: #fff7ed;
  color: #9a3412;
}
.pub-nota-mal i { color: #ea580c; }

/* Las fotos que todavia no se publican, al lado del total. */
.pub-stat-pend { color: var(--acento); font-weight: 700; }

/* --- Reordenar las fotos arrastrando ------------------------------- */

/* La miniatura levantada: sigue al dedo, va por encima del resto y deja
 * de capturar el puntero para que `elementFromPoint` encuentre a la que
 * esta debajo y no a ella misma. */
.pub-foto.is-arrastrando {
  z-index: 5;
  pointer-events: none;
  transform-origin: center;
  box-shadow: 0 10px 28px rgba(16, 22, 54, .35);
  opacity: .92;
}
.pub-foto.is-arrastrando img { transform: scale(1.04); }

/* Mientras se ordena, el navegador no interpreta el gesto como scroll ni
 * ofrece el menu de "guardar imagen" al mantener apretado. */
.pub-fotos.is-ordenando {
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}
.pub-fotos.is-ordenando .pub-foto { transition: transform .12s; }

/* En la grilla, el cursor dice que la miniatura se puede tomar. */
.pub-foto { cursor: grab; }
.pub-foto.is-arrastrando { cursor: grabbing; }
/* La imagen no se arrastra por su cuenta: sin esto el navegador inicia su
 * propio drag de la <img> y pisa el nuestro. */
.pub-foto img { -webkit-user-drag: none; user-drag: none; }

/* --- Con qué cuenta se opera ---------------------------------------
 * Aparece en /mi-cuenta sólo si la persona es operadora de alguna
 * empresa. Con una sola cuenta no se dibuja nada. */
.cuenta-rol {
  padding: 12px 14px;
  margin-bottom: 10px;
  border-radius: var(--radius-md);
  background: var(--bg);
}
.cuenta-rol-k {
  font-size: .72rem; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
  margin-bottom: 3px;
}
.cuenta-rol-v {
  display: flex; align-items: center; gap: 8px;
  font-size: .95rem; font-weight: 700; color: var(--text);
}
.cuenta-rol-v i { color: var(--primario); }
.cuenta-rol-tag {
  padding: 1px 8px; border-radius: 99px;
  background: var(--primario); color: #fff;
  font-size: .66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em;
}

/* El selector es un <details>: se pliega sin JS y el navegador ya le da
 * el teclado y el rol correctos. */
.cuenta-rol-sel > summary {
  list-style: none;
  cursor: pointer;
  margin-top: 0;
}
.cuenta-rol-sel > summary::-webkit-details-marker { display: none; }
.cuenta-rol-lista {
  display: flex; flex-direction: column; gap: 6px;
  margin-top: 8px;
}
.cuenta-rol-item {
  width: 100%;
  display: flex; align-items: center; gap: 10px;
  padding: 11px 13px;
  border: none; border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text);
  font-family: inherit; font-size: .88rem; font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.cuenta-rol-item:hover      { background: var(--border); }
.cuenta-rol-item.is-activa  { background: var(--border); }
.cuenta-rol-item > span     { flex: 1; display: flex; flex-direction: column; }
.cuenta-rol-item small      { font-size: .72rem; font-weight: 500; color: var(--muted); }
.cuenta-rol-item > i:first-child { color: var(--primario); width: 16px; text-align: center; }

/* --------- paginado de /listado ---------
 *
 * Los links que hacen rastreable el catálogo. Una persona casi nunca los
 * usa —el scroll infinito ya trajo las tandas antes de que llegue hasta
 * acá— pero son el único camino al aviso 31 para quien no ejecuta el JS,
 * o sea para un buscador. Ver la nota de `data-paginado` en listado.php.
 *
 * Por eso el estilo es sobrio: tienen que estar y verse bien cuando
 * alguien llega, no competir por atención con las cards de arriba.
 *
 * `.scroll-sentinel` queda ENTRE la lista y esto: cuando el scroll pega
 * una tanda el bloque se oculta por `hidden`, así que el hueco no queda. */
.paginado {
  display: flex; flex-wrap: wrap;
  align-items: center; justify-content: center;
  gap: 6px;
  padding: 18px 12px 4px;
}
.paginado[hidden], .paginado-resumen[hidden] { display: none; }

.paginado-num, .paginado-paso {
  display: inline-flex; align-items: center; gap: 6px;
  min-width: 38px; height: 38px;
  padding: 0 12px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-size: .88rem; font-weight: 600;
  text-decoration: none;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}
.paginado-num:hover, .paginado-paso:hover {
  border-color: var(--primario);
  color: var(--primario);
}

/* La página actual NO es un link: es un <span>, porque linkear a donde ya
 * estás no lleva a ninguna parte y además le daría al buscador un link
 * más por página sin destino nuevo. */
.paginado-num.is-actual {
  background: var(--primario);
  border-color: var(--primario);
  color: #fff;
  cursor: default;
}

.paginado-salto {
  padding: 0 2px;
  color: var(--muted);
  font-size: .88rem;
}

.paginado-resumen {
  margin: 0;
  padding: 0 12px 18px;
  text-align: center;
  font-size: .78rem;
  color: var(--muted);
}

/* En pantallas angostas los números sobran: con cinco de ventana más la
 * primera, la última y dos elipsis, la fila se parte en tres renglones.
 * Quedan Anterior y Siguiente, que es lo que se usa, y el resumen de
 * abajo sigue diciendo en qué página está. */
@media (max-width: 420px) {
  .paginado-num:not(.is-actual), .paginado-salto { display: none; }
}
