/* css/escort-ui.css — scoped UI kit for the escort app shell (escort UX/UI, S0).
   Scope rule: every selector is rooted at `.escort-shell` or an `.escort-*`
   class, so nothing here can leak into the rest of the site. The kit only
   takes effect when a page opts in by loading this stylesheet AND rendering
   an `.escort-shell` root (S1 opts the dashboard panel in via
   <body class="escort-shell">). Brand colors are reused from
   /css/global.css (--morbo-* tokens); the kit's own layout tokens
   (--escort-*) live INSIDE the scope, never at :root. */

.escort-shell {
    /* Radii */
    --escort-radius-sm: 10px;
    --escort-radius: 14px;
    --escort-radius-lg: 20px;
    --escort-radius-full: 999px;
    /* Spacing */
    --escort-space-1: 8px;
    --escort-space-2: 16px;
    --escort-space-3: 24px;
    --escort-space-4: 32px;
    /* Elevation + touch target */
    --escort-shadow: 0 8px 24px rgba(0,0,0,.45);
    --escort-tap: 44px;
    /* Alturas de las capas fijas (fix defecto 1 del S1: una barra de 239px
       tapaba el contenido porque la reserva era un padding literal suelto).
       Barra y nav se dimensionan con ESTOS tokens y todo consumidor que deba
       reservarles espacio lee los mismos (el panel los usa en su padding
       móvil en css/dashboard.css): una sola fuente de verdad, sin deriva.
       Presupuesto de --escort-bar-h (peor caso a >= 320px, safe-area 0),
       T5 — barra de IDENTIDAD del prototipo (.topbar):
         10px + 10px de padding vertical + 1px de borde
         + avatar de 38px (es el ítem más alto de la barra: nombre y enlace
           van apilados al lado, 15px * 1.25 ≈ 19px + 12px * 1.25 ≈ 15px)
       = 59px; el token redondea a 60px. Antes eran 96px porque la identidad
       apilaba nombre + badges + enlace en tres líneas y el chip de estado
       viajaba en una línea propia. */
    --escort-bar-h: 60px;
    /* 1px de borde superior + ítem de 44px (--escort-tap). */
    --escort-nav-h: 45px;
    /* Superficie de la nav inferior (nav B, T1: la línea impulso del
       prototipo). Token propio, NO --morbo-surface: el prototipo pide
       #0b0b0d y el kit no redefine colores compartidos. La barra de guardado
       del panel (css/dashboard.css) sigue en --morbo-surface: la diferencia
       es de 6/255 y T4 unifica el chrome del panel. */
    --escort-nav-bg: #0b0b0d;
    /* Dorado del FAB de Crear (degradado del prototipo nav B). */
    --escort-fab-bg: linear-gradient(135deg, #E5C55B, #f3dfa0);
    /* Closed type scale */
    --escort-type-title: 20px;
    --escort-type-body: 15px;
    --escort-type-small: 13px;
    --escort-type-micro: 12px;

    font-family: "Poppins", sans-serif;
    color: var(--morbo-text);
}

/* ---- 1. Bottom navigation (mobile-first; desktop hides it) ---------------- */

.escort-bottom-nav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 900;
    min-height: var(--escort-nav-h);
    display: flex;
    justify-content: space-around;
    align-items: stretch;
    background: var(--escort-nav-bg);
    border-top: 1px solid rgba(255,255,255,.08);
    padding-bottom: env(safe-area-inset-bottom);
}

/* Contenido entre las dos barras (decisión sellada #6): la reserva del panel
   NO se duplica acá. css/dashboard.css ya calcula el padding inferior del
   panel con ESTE token (--escort-nav-h + la barra de guardado de Servicios +
   aire + safe-area) y un padding propio del kit, con mayor especificidad,
   pisaría esa aritmética y devolvería el defecto que costó caro (la última
   fila del panel detrás de la barra fija). El kit es dueño del ALTO y de la
   CAPA (z-index 900); cada vista es dueña de su offset.
   T2 (rediseño del panel) ya cumplió el contrato en cada superficie, SIEMPRE
   con el alto real de la barra = --escort-nav-h + env(safe-area-inset-bottom)
   (el padding-bottom que la nav se agrega a sí misma acá arriba):
   - catálogo (css/inicio.css): footer + FAB de Telegram;
   - feed (css/feed.css): escenario/paneles y barra CTA;
   - perfil clásico (css/perfil.css): .cta-bar y la reserva de #perfilDetalle;
   - perfil inmersivo (css/perfil-inmersivo.css): .cta-bar y .panel-card.
   El único consumidor que ya venía reservando solo (dashboard: nav + barra de
   guardado + aire + safe-area, UNA vez cada uno) sigue igual: el panel no
   necesita ninguna regla extra del kit. */

/* Desktop hides for the mobile chrome live at the END of this file (section
   11): both rules have the same specificity as their base declarations, so a
   hide placed before a base rule is dead code. See the note there. */

/* Nav B (T1): 5 slots en línea impulso — etiqueta 10.5px (prototipo),
   color muted en reposo y acento en el ítem actual con la LÍNEA superior del
   prototipo (::after 34x2). `position: relative` es el ancla de esa línea. */
.escort-nav-item {
    position: relative;
    min-height: var(--escort-tap);
    min-width: var(--escort-tap);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    padding: 6px var(--escort-space-1);
    background: transparent;
    border: 0;
    color: var(--morbo-text-muted);
    font-size: 10.5px;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
}

.escort-nav-item > i {
    font-size: 20px;
}

.escort-nav-item.activo,
.escort-nav-item[aria-current="page"] {
    color: var(--morbo-accent);
}

/* Línea impulso del ítem actual: el indicador del prototipo nav B. Decorativa
   (el estado real viaja en aria-current) y sin capturar puntero. */
.escort-nav-item.activo::after,
.escort-nav-item[aria-current="page"]::after {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 34px;
    height: 2px;
    border-radius: var(--escort-radius-full);
    background: var(--morbo-accent);
    pointer-events: none;
}

/* Five slots (4 destinations + the create action, S3-b) must still fit a
   320px phone: labels ellipsize instead of squashing the column or
   overflowing the bar. The items keep their 44px (--escort-tap) floor. */
.escort-nav-item > span {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Create FAB (nav B, T1): el CENTRO de la barra. Una ACCIÓN, no un destino —
   un <button> que comparte los primitivos de .escort-nav-item (columna,
   target de 44px vía --escort-tap, anillo de foco del kit) y suma la
   identidad del prototipo: círculo dorado elevado (-18px) sobre la barra,
   etiqueta en acento. Desde la decisión del dueño (2026-10) el toque abre el
   sheet de las DOS acciones (Crear publicación / Crear historia): el FAB es
   disparador — aria-haspopup="menu" + aria-expanded — y ya no publica. */
.escort-nav-item--crear {
    font-family: inherit;
    color: var(--morbo-accent);
    /* El FAB sobresale por encima del borde de la nav: el slot no recorta. */
    overflow: visible;
}

/* Círculo del FAB: el ícono vive acá (no suelto en el botón) para que el
   degradado y la elevación tengan su propia caja, como en el prototipo. */
.escort-nav-item__fab {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 42px;
    height: 42px;
    margin-top: -18px;
    border-radius: var(--escort-radius-full);
    background: var(--escort-fab-bg);
    color: var(--morbo-on-accent);
    box-shadow: 0 8px 22px rgba(229, 197, 91, .3);
}

.escort-nav-item__fab > i {
    font-size: 20px;
}

/* ---- 1bis. Sheet de Crear: las dos acciones (decisión del dueño, 2026-10) ---
   Sheet compacto ANCLADO sobre la nav inferior (sube desde la barra), con la
   gramática impulso del kit: superficie oscura del chrome (--escort-nav-bg,
   que es la de la barra que lo sostiene), hairline de 1px, radios de la
   escala (14px, token md) y el dorado SOLO en los íconos y en el realce del
   ítem presionado. z-index 901: un punto por encima de la nav (900) y muy por
   debajo de los overlays full-screen de las páginas (visores ≥ 5000).
   El alto de la nav se descuenta con el MISMO token que el resto del chrome
   (--escort-nav-h) más la safe-area que la barra se agrega a sí misma, así el
   sheet se apoya exactamente sobre el borde superior de la barra, también en
   teléfonos con notch. */

/* Backdrop transparente a pantalla completa: no oscurece el contenido (esto
   es un cambio de intención, no un modal), solo capta el tap-fuera para
   cerrar. Misma capa de la nav (900): el sheet le queda encima. */
.escort-crear-menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: 900;
    background: transparent;
    border: 0;
}

.escort-crear-menu {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--escort-nav-h) + env(safe-area-inset-bottom, 0px));
    z-index: 901;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--escort-space-1);
    background: var(--escort-nav-bg);
    border: 1px solid rgba(255, 255, 255, .1);
    border-bottom: 0;
    border-radius: var(--escort-radius) var(--escort-radius) 0 0;
    box-shadow: var(--escort-shadow);
    animation: escortCrearMenuSube .18s ease;
}

/* Ítem del sheet: mismo piso táctil que la nav (44px), ícono en acento y
   realce dorado MUY tenue al presionar (tinte, nunca un segundo primary). */
.escort-crear-menu__item {
    min-height: var(--escort-tap);
    display: flex;
    align-items: center;
    gap: var(--escort-space-2);
    padding: 0 var(--escort-space-2);
    background: transparent;
    border: 0;
    border-radius: var(--escort-radius-sm);
    color: var(--morbo-text);
    font-family: inherit;
    font-size: var(--escort-type-body);
    text-align: left;
    cursor: pointer;
}

.escort-crear-menu__item > i {
    width: 1.25em;
    font-size: var(--escort-type-small);
    color: var(--morbo-accent);
}

.escort-crear-menu__item:not(:disabled):active {
    background: rgba(229, 197, 91, .1);
}

@keyframes escortCrearMenuSube {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .escort-crear-menu {
        animation: none;
    }
}

/* ---- 1ter. Identidad del panel: estado + plan (T1, decisión sellada #4) ---- */

/* El shell agrega SU parte del header adentro de la barra de identidad que el
   panel ya trae (.escort-mobile-bar): un bloque propio que el controlador del
   panel no reescribe (renderIdentidad trabaja sobre #identidadPerfil). Fila
   que no crece; el nombre de al lado es quien elipsea. */
.escort-shell-identidad {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: 0 0 auto;
}

/* Chip de estado del perfil (aprobado / pendiente / suspendido).
   T5 pasada 2 (revisión de fidelidad, dueño en el teléfono): el chip se leía
   como "APROBADO" gigante en verde brillante al lado del nombre — una píldora
   en versalitas 10px/700 con borde saturado que gritaba más que el nombre y
   que la corona del plan. El prototipo pide lo contrario: una píldora chica y
   discreta (11px/600, tinte apagado, borde fino) que informe sin competir.
   Cambios de tratamiento (el MARKUP y las clases son del shell, intactos:
   js/escort-shell.js sigue siendo el único dueño de los chips):
     - 11px/600 en sentence case (murió el text-transform: uppercase y su
       letter-spacing .06em, que era la mitad del grito);
     - padding 2px 8px y borde 1px con tinte .22 (antes .35): el contorno
       separa del fondo sin dibujar un anillo;
     - colores APAGADOS (verde/rojo desaturados, champán para pendiente) en vez
       de los tonos plenos. Contraste medido sobre el fondo del panel #070707:
       verde #86d5a4 ≈ 9.6:1, champán #cfb571 ≈ 8.5:1, rojo #e7949c ≈ 6.8:1 —
       AA de sobra en los tres, sin el brillo que el dueño marcó.
   Sigue siendo PASIVO: es un ESTADO, no un botón. */
.escort-estado {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: var(--escort-radius-full);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: normal;
    text-transform: none;
    white-space: nowrap;
}

.escort-estado--aprobado {
    color: #86d5a4;
    background: rgba(52, 210, 111, .08);
    border: 1px solid rgba(52, 210, 111, .22);
}

.escort-estado--pendiente {
    color: #cfb571;
    background: rgba(229, 197, 91, .08);
    border: 1px solid rgba(229, 197, 91, .22);
}

.escort-estado--suspendido {
    color: #e7949c;
    background: rgba(255, 93, 108, .08);
    border: 1px solid rgba(255, 93, 108, .22);
}

/* Corona de membresía: SOLO planes pagos (espejo de MEMBRESIAS_CON_BADGE de
   js/perfil.js). Píldora dorada sólida con texto sobre acento — el mismo
   lenguaje del CTA del sitio, no un badge de estado. */
.escort-plan {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    /* 4px/10px y 11px/700 son los valores del prototipo (.plan): la píldora
       dorada del plan paga el mismo cuerpo que la del estado, no más chico. */
    padding: 4px 10px;
    border-radius: var(--escort-radius-full);
    background: var(--escort-fab-bg);
    color: var(--morbo-on-accent);
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
}

.escort-plan > i {
    font-size: 10px;
}

/* En teléfonos angostos (<= 359px) el header del panel no puede sostener
   nombre + estado + plan sin aplastar el nombre: el estado sigue siendo el
   dato indispensable (dice si el perfil publica), el plan se retira. */
@media (max-width: 359px) {
    .escort-shell-identidad .escort-plan {
        display: none;
    }
}

/* ---- 1bis. Mobile top bar (S1 consumer: the dashboard panel) ----------------
   T5 — FIDELIDAD AL PROTOTIPO (dueño: "nada que ver"). La barra del panel es
   la barra de IDENTIDAD del prototipo (docs/prototipos/escort-panel-nuevo.html
   .topbar):

     [avatar 38px con aro de acento] [nombre] [píldora de estado] .... [plan]
                                     [enlace al perfil público, apagado]

   Tres decisiones que esta regla implementa:

   1. NO HAY HAMBURGUESA. El prototipo no la tiene: la navegación es la barra
      inferior y el sidebar es de escritorio (ver `.escort-mobile-bar
      .escort-menu-btn` abajo). El markup del ☰ sigue en el panel porque el
      sidebar, su scrim y sus deep links no se tocan.
   2. UNA sola píldora de estado, al lado del nombre. El nombre y el enlace los
      dibuja js/dashboard.js dentro de #identidadPerfil; el estado y el plan
      los inyecta js/escort-shell.js en .escort-shell-identidad. Para que esas
      cuatro piezas caigan en ESTA grilla, los dos contenedores del medio se
      disuelven con `display: contents` (no generan caja; sus hijos sí son
      ítems de la grilla). Sin ese truco el chip de estado solo podía vivir al
      final de la barra, o el dueño volvía a ver el estado duplicado.
   3. El plan se queda con la última columna (`justify-self: end`): es el dato
      que el prototipo pega al borde derecho.

   Grilla de dos filas: fila 1 = nombre + estado + plan (alineados por su
   línea base visual), fila 2 = enlace/pista. El nombre es la ÚNICA pista que
   puede encogerse (minmax(0, max-content) + ellipsis): el avatar, la píldora
   y el plan jamás se aplastan. */
.escort-mobile-bar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 800;
    display: grid;
    grid-template-columns: auto minmax(0, max-content) auto 1fr;
    grid-template-rows: auto auto;
    align-items: center;
    column-gap: var(--escort-space-1);
    row-gap: 0;
    min-height: var(--escort-bar-h);
    background: var(--morbo-surface);
    border-bottom: 1px solid rgba(255,255,255,.08);
    padding: calc(10px + env(safe-area-inset-top, 0px)) var(--escort-space-2) 10px;
}

/* El ☰ no pertenece a esta barra (decisión 1 del bloque de arriba). El botón
   y su cableado se conservan para escritorio; acá simplemente no se pinta. */
.escort-mobile-bar .escort-menu-btn {
    display: none;
}

/* Los contenedores del medio no generan caja: sus hijos son ítems de la
   grilla de la barra (ver decisión 2 del bloque de arriba). */
.escort-mobile-bar .escort-identity,
.escort-mobile-bar .escort-identity__cuerpo,
.escort-mobile-bar .escort-shell-identidad {
    display: contents;
}

/* Avatar: el mismo círculo del prototipo, con el aro dorado y sin depender de
   una imagen que pueda faltar (el placeholder --vacia hereda tamaño y forma). */
.escort-mobile-bar .escort-identity__avatar {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
    width: 38px;
    height: 38px;
    border: 2px solid var(--morbo-accent);
    border-radius: var(--escort-radius-full);
    object-fit: cover;
}

/* Nombre: una sola línea con ellipsis (nunca empuja la píldora ni el plan). */
.escort-mobile-bar .escort-identity__name {
    grid-column: 2;
    grid-row: 1;
    min-width: 0;
    max-width: 100%;
    font-size: var(--escort-type-body);
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Estado + plan: los dos chips del shell, en la fila del nombre. El estado
   queda pegado a la derecha del nombre (columna 3) y el plan cierra la barra
   (columna 4). */
.escort-mobile-bar .escort-shell-identidad .escort-estado {
    grid-column: 3;
    grid-row: 1;
    justify-self: start;
}

.escort-mobile-bar .escort-shell-identidad .escort-plan {
    grid-column: 4;
    grid-row: 1;
    justify-self: end;
}

/* Tercera línea del prototipo (.quien small): el enlace al perfil público o,
   sin portada, la pista — apagados, 12px, una sola línea. */
.escort-mobile-bar .escort-identity__link,
.escort-mobile-bar .escort-identity__hint {
    grid-column: 2 / -1;
    grid-row: 2;
    min-width: 0;
    max-width: 100%;
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-micro);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.escort-menu-btn {
    flex: 0 0 auto;
    min-height: var(--escort-tap);
    min-width: var(--escort-tap);
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--escort-radius-sm);
    background: transparent;
    color: var(--morbo-text);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
}

/* ---- 1ter. Nav scrim (S1: cierre del menú off-canvas) --------------------- */

/* Scrim del menú lateral del panel: se interpone ENTRE el contenido y el
   sidebar del consumidor. Apilado: z 940 queda por ENCIMA de la barra (800)
   y la nav inferior (900) — con el menú abierto el fondo entero es una
   superficie de cierre — y por DEBAJO del sidebar off-canvas (z 960 en
   css/dashboard.css, que vive adentro del panel: su ✕ siempre gana el
   hit-test) y del bottom sheet (950). Inerte con el menú cerrado:
   visibility hidden no intercepta puntero ni foco. La visibilidad NO la
   decide el scrim: la manda el raíz del shell con .menu-abierto (la misma
   clase que traba el scroll) — una sola fuente de estado. */
.escort-nav-scrim {
    position: fixed;
    inset: 0;
    z-index: 940;
    background: rgba(0,0,0,.55);
    opacity: 0;
    visibility: hidden;
    transition: opacity .25s ease, visibility .25s ease;
}

.escort-shell.menu-abierto .escort-nav-scrim {
    opacity: 1;
    visibility: visible;
}

/* Con el menú abierto la página de atrás no scrollea. Solo <=767px: en
   escritorio el sidebar es permanente y una clase remanente (p.ej. tras
   rotar la pantalla con el menú abierto) no puede trabar el scroll. */
@media (max-width: 767px) {
    .escort-shell.menu-abierto {
        overflow: hidden;
    }
}

/* ---- 3. Identity header --------------------------------------------------- */

.escort-identity {
    display: flex;
    align-items: center;
    gap: var(--escort-space-2);
    padding: var(--escort-space-2) 0;
}

.escort-identity__avatar {
    flex: 0 0 auto;
    width: 56px;
    height: 56px;
    border-radius: var(--escort-radius-full);
    object-fit: cover;
}

/* CSS-only avatar placeholder (fix defecto 2 del S1): sin foto exhibible la
   identidad renderiza ESTO — un <span> con la inicial del nombre artístico,
   sin <img>, sin src, sin 404 posible (/assets/img/default-profile.jpg no
   está trackeado en el repo). Es un modificador: hereda el tamaño de
   .escort-identity__avatar (56px base / 44px en la barra). */
.escort-identity__avatar--vacia {
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255,255,255,.08);
    border: 1px dashed rgba(255,255,255,.3);
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-body);
    font-weight: 600;
}

.escort-identity__name {
    margin: 0;
    font-size: var(--escort-type-title);
    font-weight: 600;
    color: var(--morbo-text);
}

/* La ÚNICA línea de pista que la barra muestra cuando no hay portada
   (reemplaza al estado vacío duplicado: imagen fallback + .escort-empty). */
.escort-identity__hint {
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-micro);
}

.escort-identity__badges {
    display: flex;
    flex-wrap: wrap;
    gap: var(--escort-space-1);
    margin-top: 4px;
}

/* S1: text column beside the avatar (name + badges + public link stack). */
.escort-identity__cuerpo {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    min-width: 0;
}

.escort-identity__link {
    color: var(--morbo-accent);
    font-size: var(--escort-type-micro);
    text-decoration: none;
}

.escort-identity__link:hover,
.escort-identity__link:focus-visible {
    text-decoration: underline;
}

/* ---- 4. Badges ------------------------------------------------------------ */

.escort-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 10px;
    border-radius: var(--escort-radius-full);
    border: 1px solid rgba(255,255,255,.12);
    background: rgba(255,255,255,.06);
    color: var(--morbo-text-soft);
    font-size: var(--escort-type-micro);
    font-weight: 500;
}

/* T4 (línea impulso del panel): los dos chips de estado de la identidad
   pasan a las PÍLDORAS del prototipo — verde tintado para aprobado y dorado
   para pendiente (antes: verde sólido oscuro / contorno muted). El estado es
   el dato más importante del header, así que gana color propio; el texto
   sigue llevando el mismo nombre accesible. Contraste medido sobre #070707:
   #34d26f ≈ 8.9:1 y #E5C55B ≈ 11.6:1 (AA de sobra). */
.escort-badge--ok {
    border: 1px solid rgba(52, 210, 111, .35);
    background: rgba(52, 210, 111, .12);
    color: #34d26f;
    font-weight: 600;
}

.escort-badge--pending {
    border: 1px solid rgba(229, 197, 91, .35);
    background: rgba(229, 197, 91, .1);
    color: var(--morbo-accent);
    font-weight: 600;
}

/* ---- 5. Progress bar ------------------------------------------------------ */

.escort-progress {
    width: 100%;
    height: 6px;
    border-radius: var(--escort-radius-full);
    background: rgba(255,255,255,.1);
    overflow: hidden;
}

.escort-progress__bar {
    height: 100%;
    width: 0; /* the width is set inline by JS while work advances */
    border-radius: var(--escort-radius-full);
    background: var(--morbo-accent);
    transition: width .25s ease;
}

.escort-progress--indeterminate .escort-progress__bar {
    width: 40%;
    animation: escort-progress-indeterminate 1.2s ease-in-out infinite;
}

@keyframes escort-progress-indeterminate {
    from { transform: translateX(-100%); }
    to { transform: translateX(250%); }
}

/* ---- 6. Spinner (CSS-only) ------------------------------------------------ */

.escort-spinner {
    width: 22px;
    height: 22px;
    border-radius: var(--escort-radius-full);
    border: 2px solid rgba(255,255,255,.15);
    border-top-color: var(--morbo-accent);
    animation: escort-spinner-rotate 1s linear infinite;
}

@keyframes escort-spinner-rotate {
    to { transform: rotate(360deg); }
}

/* Reduced motion: no looping animation for progress/spinner (kept readable). */
@media (prefers-reduced-motion: reduce) {
    .escort-progress--indeterminate .escort-progress__bar {
        animation: none;
        width: 100%;
        opacity: .6;
    }

    .escort-spinner {
        animation: none;
        opacity: .6;
    }

    .escort-progress__bar {
        transition: none;
    }
}

/* ---- 7. Bottom sheet (photo actions; inert until a slice opens it) -------- */

/* S2a consumer: the dashboard photo sheet. The title is optional per open —
   the sheet is labelled by aria-labelledby when it carries one. */
.escort-sheet__titulo {
    margin: 0 0 var(--escort-space-1);
    padding: 0 var(--escort-space-2);
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-small);
    font-weight: 600;
}

.escort-sheet__backdrop {
    position: fixed;
    inset: 0;
    z-index: 950;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    background: rgba(0,0,0,.6);
}

.escort-sheet {
    width: 100%;
    max-width: 480px;
    border-radius: var(--escort-radius-lg) var(--escort-radius-lg) 0 0;
    background: var(--morbo-surface);
    box-shadow: var(--escort-shadow);
    padding: var(--escort-space-3) var(--escort-space-2)
        calc(var(--escort-space-2) + env(safe-area-inset-bottom));
}

.escort-sheet__item {
    display: flex;
    align-items: center;
    gap: var(--escort-space-2);
    width: 100%;
    min-height: var(--escort-tap);
    padding: var(--escort-space-1) var(--escort-space-2);
    background: transparent;
    border: 0;
    border-radius: var(--escort-radius-sm);
    color: var(--morbo-text);
    font-size: var(--escort-type-body);
    text-align: left;
    cursor: pointer;
}

/* Destructive action variant (S2a): the sheet's delete action carries the
   brand danger color — same token the site uses for danger buttons. */
.escort-sheet__item--peligro {
    color: var(--morbo-danger);
}

/* ---- 8. Photo gallery ------------------------------------------------------ */

.escort-gallery {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--escort-space-1);
}

/* Desktop gains one column (the base above stays mobile-first). */
@media (min-width: 768px) {
    .escort-gallery {
        grid-template-columns: repeat(4, 1fr);
    }
}

.escort-gallery__item {
    position: relative;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: var(--escort-radius-sm);
    background: var(--morbo-surface);
}

.escort-gallery__badge {
    position: absolute;
    left: var(--escort-space-1);
    bottom: var(--escort-space-1);
    padding: 2px 8px;
    border-radius: var(--escort-radius-full);
    background: var(--morbo-accent);
    color: var(--morbo-on-accent);
    font-size: var(--escort-type-micro);
    font-weight: 600;
}

/* Add-photo tile (S5): the FIRST cell of the gallery grid and the photos
   section's ONLY upload affordance (the old dashed upload-box is gone — one
   affordance, one hint line). Same square cell as the photos; the dashed
   border keeps the "add something here" cue. The tile is NOT a photo:
   counts, portada badge, action sheet and delete flow all read the
   profile's array and ignore this DOM child. */
.escort-gallery__item--agregar {
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px dashed rgba(255,255,255,.3);
    background: transparent;
    color: var(--morbo-text-muted);
    font-size: 22px;
}

/* While a batch uploads the whole section is .escort-blocked; the tile also
   goes disabled — keyboard activation ignores pointer-events. */
.escort-gallery__item--agregar:disabled {
    opacity: .5;
    cursor: default;
}

/* Photo cell (S2c-c follow-up, nested-button fix): non-interactive wrapper
   that owns the tile's square slot in the grid and hosts the reorder handle
   as a SIBLING of the tile button — nested interactive content is invalid
   HTML, and iOS Safari retargets the inner button's tap to the outer one
   (the tactile reorder would never start). The cell carries no pointer
   semantics of its own: it only catches the mouse-DnD drop zone, which keeps
   covering the whole tile area. */
.escort-gallery__celda {
    position: relative;
    aspect-ratio: 1 / 1;
}

/* The photo tile fills its cell completely: same square footprint (same
   radius/clipping from .escort-gallery__item) as when it was the direct grid
   child. The add tile keeps its own .escort-gallery__item geometry. */
.escort-gallery__celda .escort-gallery__item {
    display: block;
    width: 100%;
    height: 100%;
}

/* Reorder handle (S2c-c): the ONLY extra control beside a tile — a real
   <button> (focusable, announceable) pinned to the cell's top-right corner
   (same visual corner as before the fix: the tile fills the cell), shown
   ONLY inside the explicit reorder mode (S2c-d — see 8bis: outside it the
   handle is display:none, so a touch starting on a tile can never be stolen
   from the page scroll). It is the anchor of BOTH drag paths: Pointer
   Events (touch) and HTML5 DnD (mouse, draggable lives only here).
   touch-action: none makes the gesture belong to the reorder from
   pointerdown — the page never scrolls mid-drag. */
.escort-gallery__handle {
    position: absolute;
    top: var(--escort-space-1);
    right: var(--escort-space-1);
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(var(--escort-tap) - var(--escort-space-1));
    height: calc(var(--escort-tap) - var(--escort-space-1));
    border: 0;
    border-radius: var(--escort-radius-full);
    background: rgba(0, 0, 0, .55);
    color: var(--morbo-text);
    font-size: var(--escort-type-body);
    cursor: grab;
    touch-action: none;
    /* Safari/iOS: sin esto el long-press levanta el callout/menú y el gesto
       se cancela. OJO: NO se agrega -webkit-user-drag: none — en Blink/WebKit
       esa propiedad le GANA al draggable=true del handle y suprime el drag
       nativo, dejando muerto el camino HTML5 DnD (mouse) en Chrome/Safari de
       escritorio (hallazgo del verificador independiente de f9beef3). iOS
       Safari no hace DnD nativo por touch, así que el callout es lo único que
       hacía falta. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

/* Drag states (S2c-c): the dragged tile dims with a dashed accent ring; the
   tile under the pointer/cursor (the drop target) gets the solid ring. Both
   are toggled by JS over .escort-gallery__item. */
.escort-gallery__item--arrastrando {
    opacity: .6;
    outline: 2px dashed var(--morbo-accent);
    outline-offset: -2px;
}

.escort-gallery__item--destino {
    outline: 2px solid var(--morbo-accent);
    outline-offset: -2px;
}

/* ---- 8bis. Explicit reorder mode (S2c-d) ---------------------------------- */

/* Owner defect: the always-visible handle carries touch-action: none, so any
   touch STARTING on it could never scroll the page — with a handle on every
   tile, accidental captures were constant (thresholds can't fix what
   touch-action decided at touch start). Reordering now lives behind an
   explicit mode toggled by a real button; outside it the handles are
   display:none — invisible, unfocusable, untappable. Both gates are needed:
   the base .escort-gallery__handle sets display:flex, which would beat the
   UA's [hidden] rule, and the mode class is the CSS-level backstop when JS
   toggles the class but a handle slips through without the attribute. */
.escort-gallery__handle[hidden],
.escort-gallery:not(.escort-gallery--reorden) .escort-gallery__handle {
    display: none;
}

/* Mode bar: sits right above the gallery (hand-authored in
   pages/dashboard.html, driven by js/dashboard.js). Collapses completely
   with < 2 photos: every piece is [hidden] and the bar has no padding of its
   own. Mobile-first: button first, gesture hint wraps beside/below it. */
.escort-reorden {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--escort-space-1) var(--escort-space-2);
    margin: 0 0 var(--escort-space-1);
}

.escort-reorden[hidden],
.escort-reorden__btn[hidden],
.escort-reorden__hint[hidden] {
    display: none;
}

.escort-reorden__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--escort-tap);
    padding: 0 var(--escort-space-2);
    border: 1px solid rgba(255, 255, 255, .25);
    border-radius: var(--escort-radius-full);
    background: transparent;
    color: var(--morbo-text);
    font-family: inherit;
    font-size: var(--escort-type-small);
    font-weight: 600;
    cursor: pointer;
}

/* Active state = the mode is on and this same button is the "done" exit. */
.escort-reorden__btn[aria-pressed="true"] {
    border-color: transparent;
    background: var(--morbo-accent);
    color: var(--morbo-on-accent);
}

.escort-reorden__hint {
    margin: 0;
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-micro);
}

/* ---- 8ter. Active stories list (S3-b: the Historias block inside the
   panel's unified #contenido section since S1 feed-composicion #179) -- */

/* One row per active story: [signed thumb | type + countdown | delete].
   Mobile-first single column; desktop (768px+) gains a second column. The
   thumbnail box keeps its footprint even before (or without) an <img> — a
   null signed-URL resolution renders the kit's VISIBLE placeholder (S3-d),
   never a broken image, never an empty cell. */
.escort-historias {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--escort-space-1);
}

@media (min-width: 768px) {
    .escort-historias {
        grid-template-columns: repeat(2, 1fr);
    }
}

.escort-historias__item {
    display: flex;
    align-items: center;
    gap: var(--escort-space-2);
    padding: var(--escort-space-1);
    border: 1px solid rgba(255, 255, 255, .1);
    border-radius: var(--escort-radius);
    background: var(--morbo-surface);
}

.escort-historias__miniatura {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(var(--escort-tap) + 12px);
    height: calc(var(--escort-tap) + 12px);
    overflow: hidden;
    border-radius: var(--escort-radius-sm);
    background: rgba(255, 255, 255, .06);
}

.escort-historias__miniatura img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* S3-d (#86): the visible placeholder of a thumbnail whose signed URL could
   not be resolved (null or adapter failure). It mirrors the kit's
   empty-avatar language — dashed modifier — and carries micro copy inside
   the 56px box; the tipo chip (in the item body) stays visible regardless. */
.escort-historias__miniatura--vacia {
    border: 1px dashed rgba(255, 255, 255, .3);
    background: transparent;
}

.escort-historias__sin-miniatura {
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-micro);
    line-height: 1.2;
    text-align: center;
    padding: 0 2px;
}

.escort-historias__cuerpo {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}

.escort-historias__expira {
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-micro);
}

/* Per-item delete: the destructive action of the brand over the kit's touch
   target (44px), icon-only visually — the accessible name travels in
   aria-label. */
.escort-historias__borrar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--escort-tap);
    min-width: var(--escort-tap);
    border: 0;
    border-radius: var(--escort-radius-sm);
    background: transparent;
    color: var(--morbo-danger);
    font-size: var(--escort-type-body);
    cursor: pointer;
}

/* ---- 8-bis. Historias: publicación desde el panel (S3-c) ---------------------
   The publish entry of the stories section: the button measures the kit's
   touch target (its visual weight comes from the consumer's .btn-primary),
   and the hint carries the WHY copy — the owner requirement: the disabled
   state must explain itself (adapting to the profile state) instead of
   failing late. The hint is JS-driven; :empty keeps a stale render from
   taking vertical space. */
.escort-historias__publicar {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--escort-space-1);
    margin-bottom: var(--escort-space-2);
}

.escort-historias__btn {
    min-height: var(--escort-tap);
}

.escort-historias__hint {
    margin: 0;
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-micro);
}

.escort-historias__hint:empty {
    display: none;
}

/* ---- 8-ter. Historias: previsualización con confirmación explícita (S3-e) ---
   The preview of the chosen file BEFORE anything is uploaded, mirroring the
   catalog modal's language (#previewHistoria): object-URL <img> for images
   and <video controls muted playsInline> for video, contained without
   distortion, plus the file name and the two explicit actions. While a
   selection is pending the publish/open-picker button is hidden — nothing
   implicit can upload. The container starts hidden in the markup; JS is the
   only one toggling it. */
.escort-historias__previsualizacion {
    display: flex;
    flex-direction: column;
    gap: var(--escort-space-1);
    padding: var(--escort-space-1);
    border: 1px solid rgba(255, 255, 255, .1);
    border-radius: var(--escort-radius);
    background: var(--morbo-surface);
}

.escort-historias__preview {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: var(--escort-radius-sm);
    background: rgba(0, 0, 0, .4);
}

.escort-historias__preview img {
    display: block;
    max-width: 100%;
    max-height: 48vh;
    object-fit: contain;
}

.escort-historias__preview video {
    display: block;
    max-width: 100%;
    max-height: 48vh;
    object-fit: contain;
}

.escort-historias__nombre-archivo {
    margin: 0;
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-micro);
    text-align: center;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.escort-historias__acciones {
    display: flex;
    flex-direction: column;
    gap: var(--escort-space-1);
}

/* Same trap as the reorder handle and bar (8bis above): these base classes
   declare display: flex, and an AUTHOR rule always beats the user agent's
   [hidden] { display: none } — so `el.hidden = true` flipped the attribute
   while the block stayed painted. Owner defect: after a failed publish was
   cancelled, the stale Confirmar/Cancelar buttons stayed on screen, inert.
   Any hidden-toggled class that declares display needs this explicit rule;
   the generic sweep lives in tools/tests/dashboard-historias.test.mjs. */
.escort-historias__previsualizacion[hidden],
.escort-historias__preview[hidden],
.escort-historias__acciones[hidden] {
    display: none;
}

/* ---- 9. Empty state -------------------------------------------------------- */

.escort-empty {
    padding: var(--escort-space-3) var(--escort-space-2);
    color: var(--morbo-text-muted);
    font-size: var(--escort-type-small);
    text-align: center;
}

/* ---- 10. Blocking state + focus ring ---------------------------------------- */

/* Applied over a region while an upload runs: dimmed and untouchable. */
.escort-blocked {
    opacity: .5;
    pointer-events: none;
}

.escort-focusable:focus-visible {
    outline: 2px solid var(--morbo-accent);
    outline-offset: 2px;
}

/* ---- 11. Desktop: hide the mobile chrome ---------------------------------- */

/* Source order is load-bearing here: these single-class rules carry the same
   specificity as their base declarations above, so the LAST one wins. Put
   them at the end of the file (a hide declaration that sits before its base
   rule is dead code — that exact bug shipped once: .escort-mobile-bar stayed
   visible on desktop). tools/tests/dashboard-shell.test.mjs pins this order. */
@media (min-width: 768px) {
    .escort-bottom-nav {
        display: none;
    }

    .escort-mobile-bar {
        display: none;
    }

    .escort-nav-scrim {
        display: none;
    }
}
