﻿/* ══ ÍTEM 876 · LV-31 — TIPOGRAFÍA PROPIA: Inter (interfaz) + Cinzel (wordmark) ══
   SELF-HOSTED (regla de la casa, cero CDN): WOFF2 en wwwroot/fonts/ con sus
   licencias OFL al lado. Subsets latin + latin-ext (acentos y eñes completos).
   · Inter v20 de Google Fonts es FUENTE VARIABLE: UN archivo por subset cubre
     los pesos 100-900 (por eso font-weight es un RANGO, no un número).
   · font-display: swap ⇒ mientras carga se pinta el fallback (la pila vieja,
     el look de siempre) y luego "swapea" — el kiosco de la RPi nunca queda
     con texto invisible aunque el WOFF2 tarde.
   · Los unicode-range son los EXACTOS que sirve Google: el navegador solo
     descarga el subset que la página usa de verdad.
   · Los 10 theme-*.css YA nombraban 'Inter' de primera en su body desde
     siempre (sin que existiera): estos @font-face la hacen realidad en los
     11 vestidos (10 temas + el base de esta hoja) sin tocar los temas.
   ⚠️ LV-31: SOLO font-family. Ni colores, ni tamaños, ni pesos, ni márgenes. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/inter-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/inter-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Cinzel: SOLO el wordmark y títulos EN MAYÚSCULAS (romano lapidario — sus
   minúsculas salen como versalitas; jamás en cuerpo, formularios ni botones).
   Hoy lo usa únicamente .app-brand (MainLayout.razor.css). */
@font-face {
    font-family: 'Cinzel';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/fonts/cinzel-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Cinzel';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/fonts/cinzel-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

html, body {
    font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* GUARDA CONTRA SCROLL HORIZONTAL (ítem 13): la app NUNCA
   debe tener scroll horizontal — un elemento mas ancho que el viewport hacia
   que el hamburguer flotante (position:fixed) se viera "despegado" al arrastrar
   la pagina de lado en el telefono. `overflow-x: clip` contiene cualquier
   desborde SIN crear un contenedor de scroll (a diferencia de `hidden`, no
   rompe `position: sticky` del top bar ni del sidebar). El culpable real —
   tablas anchas — ya va contenido con .table-responsive (overflow-x:auto). */
html, body { overflow-x: clip; max-width: 100%; }
main { min-width: 0; }        /* deja que el flex-item se encoja en vez de forzar ancho */
.content { overflow-wrap: break-word; }

/* Blazor circuit error banner — GLOBAL (not scoped to a layout) so EVERY layout
   hides it by default and only shows it on a real unhandled error. Previously this
   lived in MainLayout.razor.css (scoped), so PublicLayout (/c/{token}) showed the
   banner permanently with no error behind it. */
#blazor-error-ui {
    color-scheme: light only;
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

/* ═══ ÍTEM 196b (batch 30489): reconnect modal — GLOBAL, ya no scoped ═══════
   El contenido del <dialog> ahora lo inyecta ReconnectModal.razor.js (HTML
   crudo limpio para crawlers). El CSS scoped (.razor.css) marca los elementos
   del MARKUP con el atributo b-xxx, y los nodos creados por JS no lo traen —
   así que estos estilos viven aquí en global (misma lección que el banner
   #blazor-error-ui de arriba). Copiados 1:1 del ReconnectModal.razor.css. */
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
    display: none;
}

#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
    display: block;
}

#components-reconnect-modal {
    background-color: white;
    width: 20rem;
    margin: 20vh auto;
    padding: 2rem;
    border: 0;
    border-radius: 0.5rem;
    box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
    opacity: 0;
    transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
    animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
}

    #components-reconnect-modal[open] {
        animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
        animation-fill-mode: both;
    }

#components-reconnect-modal::backdrop {
    background-color: rgba(0, 0, 0, 0.4);
    animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
    opacity: 1;
}

@keyframes components-reconnect-modal-slideUp {
    0% {
        transform: translateY(30px) scale(0.95);
    }

    100% {
        transform: translateY(0);
    }
}

@keyframes components-reconnect-modal-fadeInOpacity {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@keyframes components-reconnect-modal-fadeOutOpacity {
    0% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}

.components-reconnect-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
}

#components-reconnect-modal p {
    margin: 0;
    text-align: center;
}

#components-reconnect-modal button {
    border: 0;
    background-color: #6b9ed2;
    color: white;
    padding: 4px 24px;
    border-radius: 4px;
}

    #components-reconnect-modal button:hover {
        background-color: #3b6ea2;
    }

    #components-reconnect-modal button:active {
        background-color: #6b9ed2;
    }

.components-rejoining-animation {
    position: relative;
    width: 80px;
    height: 80px;
}

    .components-rejoining-animation div {
        position: absolute;
        border: 3px solid #0087ff;
        opacity: 1;
        border-radius: 50%;
        animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
    }

        .components-rejoining-animation div:nth-child(2) {
            animation-delay: -0.5s;
        }

@keyframes components-rejoining-animation {
    0% {
        top: 40px;
        left: 40px;
        width: 0;
        height: 0;
        opacity: 0;
    }

    4.9% {
        top: 40px;
        left: 40px;
        width: 0;
        height: 0;
        opacity: 0;
    }

    5% {
        top: 40px;
        left: 40px;
        width: 0;
        height: 0;
        opacity: 1;
    }

    100% {
        top: 0px;
        left: 0px;
        width: 80px;
        height: 80px;
        opacity: 0;
    }
}

/* ═══ ÍTEM 383: pantalla de reconexión PROPIA de
   las superficies NO ATENDIDAS (kiosko/TV/check-in) ═══════════════════════════
   ReconnectModal.razor.js le añade la clase .bsp-unattended al <dialog> cuando
   la ruta es /kiosk o /tv — el modal de 20rem se convierte en una pantalla
   COMPLETA porcelana con el branding de la casa (poste + raya de barbería),
   sin jerga técnica y sin botones (el kiosko se repone solo: reintentos
   infinitos en bsp-circuit.js + sonda /health en ReconnectModal.razor.js).
   En pantallas atendidas (sin la clase) nada de esto aplica. */
#components-reconnect-modal.bsp-unattended {
    width: 100vw;
    height: 100vh;
    max-width: 100vw;
    max-height: 100vh;
    margin: 0;
    border-radius: 0;
    background-color: #F7F5F0; /* porcelana theshop (la misma del wash del kiosko) */
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ══ 🔵 P-12 (Lupe, vuelta 9) — EL DIALOGO CERRADO SE SEGUIA MAQUETANDO ═══════
   El `display: flex` de arriba GANA al `display: none` que el navegador le da a
   un <dialog> cerrado ⇒ el elemento medía 100vw x 100vh AUNQUE no estuviera
   abierto. Medido por Lupe en /kiosk/checkin a 390 px: `open=false`,
   `opacity:0`, y aun asi 390x844 en y=1531 ⇒ `scrollWidth 390` vs
   `clientWidth 375` = 15 px de SCROLL HORIZONTAL (los 100vw incluyen la barra)
   y ~844 px de pagina fantasma. En el BAR-01 (1024x600) eso son ~600 px de
   porcelana en blanco si un cliente arrastra. El scroll horizontal en el kiosco
   va contra la ley de la casa.
   El arreglo es UNA linea: cuando el <dialog> no tiene [open], nada se maqueta.
   `showModal()`/`close()` (ReconnectModal.razor.js:231/235) manejan ese atributo,
   asi que el estado ABIERTO no cambia en absoluto. */
#components-reconnect-modal.bsp-unattended:not([open]) { display: none; }

#components-reconnect-modal.bsp-unattended::backdrop {
    background-color: #F7F5F0;
}

/* La rueda se muestra SIEMPRE que el diálogo esté abierto — la pantalla propia
   no distingue estados show/retrying/failed: para el cliente solo existe
   "un momento…". Los anillos van en navy (el azul default no es de la casa). */
#components-reconnect-modal.bsp-unattended .components-rejoining-animation {
    display: block;
}

#components-reconnect-modal.bsp-unattended .components-rejoining-animation div {
    border-color: #14224E;
}

.bsp-reconnect-brand {
    max-width: 560px;
    padding: 1rem;
    text-align: center;
}

/* ══ 💈 EL POSTE DEL MODAL DE RECONEXIÓN — ÍTEM 1230 (Héctor 2026-09-08) ══
   📜 La historia, corta, porque explica por qué aquí hubo TRES postes:
     · 2026-08-22 — era un PNG QUIETO con la ruedita gris de Blazor girando al
       lado. Verbatim: *"no veo el postesito girando… lo que sí veo son unos
       círculos apareciendo y desapareciendo."* ⇒ se dibujó un poste EN CSS que
       sí giraba, y se quitó la ruedita.
     · 2026-09-08 — Héctor vio ese dibujado y lo mandó borrar: *"hay que borrar el
       feo en movimiento, no quiero que vuelva a salir en ningún lugar"* ·
       *"prefiero el bonito en movimiento"*.
   ⇒ AHORA ES EL POSTE DEL SOCIO (`pole-spin-house.webp`, 37×160 nativo), el
     mismo de la capa de arranque del TV (`TvLayout.razor`) y del slide de la
     casa. ⚠️ **ENMIENDA R-425**: ya no queda NINGÚN poste de CSS vivo en la app
     — el bloque `.pb-*` se borró entero (ver su lápida más abajo en este mismo
     fichero, junto a `.tv-house-pole`).
   💰 Y NO CUESTA RED, que era la objeción CORRECTA del comentario viejo
     (*"esta pantalla sale JUSTO cuando la conexión se cayó — pedir un archivo
     con la red mala es lo contrario de ayudar"*): `ReconnectModal.razor.js` solo
     pinta el ANIMADO cuando la página YA se trajo ese fichero; si no, pinta el
     QUIETO de 4 446 B, precargado por la superficie que lo use.
   ♿ `prefers-reduced-motion` se resuelve en el MARKUP con `<picture>` + `media`
     (un WebP animado no se para desde CSS) — el navegador baja UNO SOLO.
   📐 El poste se pinta a su TAMAÑO NATIVO (37×160): la regla de la casa es que
     el asset se exporta al tamaño que se pinta, no que el CSS lo estire. Si
     alguien cambia estas medidas, HAY QUE RE-EXPORTAR EL WEBP. */
.bsp-reconnect-pole {
    /* `.components-reconnect-container` es flex-column con `gap:1rem` y
       `align-items:center` ⇒ el aire y el centrado ya vienen dados; el poste
       mide su tamaño real — solo falta que la caja NO se encoja. */
    flex: none;
}
/* `<picture>` no pinta nada: solo elige el `src`. El que se dibuja y el que se
   dimensiona es el `<img>` de dentro (misma nota que `TvLayout.razor`). */
.bsp-reconnect-pole picture { display: block; }
.bsp-reconnect-pole img { width: 37px; height: 160px; display: block;
    filter: drop-shadow(0 5px 10px rgba(0,0,0,.35)); }

.bsp-reconnect-title {
    color: #14224E; /* navy de la casa */
    font-weight: 800;
    margin: 0;
}

.bsp-reconnect-body {
    color: #14224E;
    opacity: .8;
    font-size: 1.15rem;
    margin: 0;
}

/* Raya de barbería — reusa el gradiente del tema (var), con fallback propio */
.bsp-reconnect-stripe {
    width: min(340px, 70vw);
    height: 8px;
    border-radius: 4px;
    background: var(--barber-stripe, repeating-linear-gradient(135deg,
        #C8102E 0px, #C8102E 12px,
        #FFFFFF 12px, #FFFFFF 24px,
        #14224E 24px, #14224E 36px));
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding-top: 1.1rem;
}
/* ══ ÍTEM 1153(c) (Jack 2026-09-04) — EL BOTÓN FLOTANTE DE ACCESIBILIDAD NO TAPA
   EL FINAL DE LA PÁGINA. `.a11y-trigger-btn` (AccessibilityPanel.razor.css) es
   `position:fixed; bottom:1.5rem; right:1.5rem; 3.25rem` ⇒ ocupa la esquina
   inferior derecha (a 390: x 314–366, y 768–820 de 844) y, con la página al
   final, se montaba sobre la última línea (medido en la landing de tienda a 390:
   548 px² de solape con «© 2026 BarberShopPro…»). Un espacio inferior en el
   `<article class="content">` de MainLayout (el ÚNICO layout que pinta el panel)
   de 5.5rem = 1.5rem de margen + 3.25rem de botón + aire, deja que TODO el
   contenido pueda desplazarse por encima del botón. Solo en anchos donde el
   contenido llega hasta el borde (≤ 991.98px); en escritorio el `.content` está
   centrado a 960 px y el botón cae fuera de él. Mientras se scrollea, un botón
   fijo pasa por encima de lo que haya en esa esquina — eso es inherente al
   control (y es arrastrable en vertical, accessibility-manager.js). */
@media (max-width: 991.98px) {
    article.content { padding-bottom: 5.5rem; }
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

/* ÍTEM 534 — CAUSA RAÍZ del reporte "en el móvil se sobrepone el nombre del textbox
   sobre el ejemplo del textbox". Aquí vivían dos reglas (del scaffold de Identity) que
   volvían VISIBLE el placeholder dentro de `.form-floating`:

       .form-floating > .form-control::placeholder { color: var(--bs-secondary-color);
                                                     text-align: end; }

   Bootstrap pone ese placeholder en `color: transparent` A PROPÓSITO: en un campo
   flotante la etiqueta ocupa el interior del input mientras está vacío, así que el
   ejemplo TIENE que desaparecer. Al re-habilitarlo, etiqueta (izquierda) y ejemplo
   (derecha) se pintaban los dos dentro de la misma caja: en escritorio cabían, y en
   el móvil —caja angosta— se montaban uno encima del otro.

   Se eliminan en vez de parchearlas: las 13 pantallas de Account ya NO usan
   `form-floating` (etiqueta normal encima del campo, el patrón del resto de la app),
   y dejarlas aquí volvería a romper cualquier `form-floating` que se añada después. */

/* ===== BarberShopPro — mobile-first ===== */

:root {
    --bcp-radius: 14px;
    --bcp-shadow: 0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.08);
    --bcp-gap: 1rem;

    /* ══ ÍTEM 968 · LA PILA PEGAJOSA DE ARRIBA — UN SOLO NÚMERO ═══════════════
       🔑 `--bsp-header-h` = alto del `.app-header`. ANTES vivía escrito a mano
       CUATRO veces (`MainLayout.razor.css` :18 alto, :131 alto del sidebar,
       :133 su `top`, y `NavMenu.razor.css` :142 el scroll del menú) — una copia
       más y es la ley 13. Ahora las cuatro leen de aquí.
       📏 Y `3.5rem` no es un número inventado: Kahlo lo midió en las CINCO
       escalas de accesibilidad (56 · 64,39 · 72,8 · 84 · 98 px ⇒ alto÷raíz =
       3,5000 / 3,4995 / 3,4998 / 3,5000 / 3,5000). En `rem` la banda de
       suplantación sigue al encabezado SOLA, sin una línea de JS.

       🎭 `--bsp-bimp-h` = alto REAL de la banda de suplantación (`.bimp-banner`),
       en px, escrito por `bspStickyPile` (js/sidebar-toggle.js).
       ⚠️ NO HAY NÚMERO FIJO POSIBLE: medido, la banda va de 47 a 185,75 px según
       la escala y el largo del nombre — factor 2,26×. El alto de la pila es un
       DATO, no una constante; por eso hace falta el observador de tamaño.
       ✅ El `0px` de aquí NO es un default sin dueño (ley 13): es el valor
       VERDADERO cuando no hay banda, que es el estado de todo el mundo salvo un
       SuperAdmin suplantando. Si el JS no corriera, el sidebar se comporta como
       HOY (degrada al estado actual, nunca a uno peor). */
    --bsp-header-h: 3.5rem;
    --bsp-bimp-h: 0px;
}

lord-icon { display: inline-block; vertical-align: middle; }
/* Sidebar nav icons (Lordicons) ship with a dark/colored palette that's invisible on the
   dark sidebar of every theme. `brightness(0) invert(1)` forces them to solid WHITE while
   keeping the hover animation. Safe because every theme's sidebar is dark. */
.nav-lordicon { width: 24px; height: 24px; filter: brightness(0) invert(1); }
.nav-divider { border-color: rgba(255,255,255,.2); margin: .5rem 1rem; }

/* Layout content gets comfortable touch padding on phones */
.content { padding-inline: 1rem; max-width: 960px; margin-inline: auto; }

/* ANCHO OPT-IN (ítem 63, opción B — ampliado): una
   pagina que renderiza <div class="page-wide"></div> ensancha su .content
   contenedor a ~1400px. Empezo con las pantallas de LISTA/tabla (/admin/users
   y hermanas); despues los FORMULARIOS grandes
   tambien van wide (Profile, CompanyEdit, StoreEdit, EmployeeEdit,
   CustomerEdit, Support) — "aprovechar el ancho del dispositivo". En tablet/
   movil no cambia nada (la pantalla es mas angosta que el max-width). Usa
   :has() (navegadores modernos). */
.content:has(.page-wide) { max-width: 1400px; }

/* ══ ÍTEM 636 — ANCHO FLUIDO PARA LAS PANTALLAS DE TRABAJO (el dueño 2026-08-07) ══
   Verbatim: "si la pantalla tiene espacio para que la página sea un poco más
   grande, ¿por qué no ajustamos el tamaño de la página al tamaño de la pantalla?"
   MEDIDO: 960 / 1920 = exactamente LA MITAD del monitor. En su escritorio (1920
   con el sidebar abierto ⇒ 1655 px de <main>) sobraban 695 px de fondo.

   🔑 EL CRITERIO — el tope de 960 no está mal, estaba mal APLICADO:
   · TEXTO Y FORMULARIOS  ⇒ SÍ se topan. Un párrafo a 1900 px se lee mal.
     `.content` sigue en 960 por defecto y NO se toca. `page-wide` (1400) tampoco.
   · REJILLAS, CARRUSELES Y LISTAS DE TRABAJO ⇒ usan el ancho que haya. Ahí el
     ancho no es estética: es CUÁNTAS COLUMNAS se ven sin arrastrar.

   ⚖️ POR QUÉ FLUIDO Y NO OTRO NÚMERO FIJO (medido, no opinado):
   1. A 1920 un tope de 1400 deja 255 px sin usar y CUESTA 2 barberos en la
      agenda (medido: 960→4 barberos completos, 1400→7, fluido→9 de 19).
      Con la barbería de 7 renteros + dueño (8 columnas = 1362 px con el gutter)
      el tope de 1400 TODAVÍA corta la última; fluido las muestra las 8.
   2. Aquí NADA se estira al ensanchar: `.appt-card-data` es flex-wrap
      empaquetado a la izquierda con gap fijo — se midió que los anchos de sus
      columnas son IDÉNTICOS a 960, 1400 y 1655 (93/84/244/84/99/84/84/84). No
      existe el modo de fallo "columnas absurdamente separadas" en este markup;
      lo único que cambia es cuántas caben por fila. Un tope, en cambio, SOLO
      puede quitar columnas — nunca mejora nada.
   3. El "margen sano" ya existe: el `padding-inline` de `.content` (+ el
      padding-left de 3rem del layout que libra la pestaña del hamburguesa).

   ⚠️ ACTUALIZACIÓN 2026-08-19 (ítem 804): la card de cita YA NO son 8 columnas,
   son 9 — se le añadió SERVICIOS (los nombres) y la de dinero se re-rotuló a
   "Total de servicios". Los 8 números de arriba son la medición HISTÓRICA del
   636 y se dejan como está: documentan por qué el ancho es fluido, no cuántas
   columnas hay hoy. Medición nueva a 390×844 (viewport de prueba de la casa):
   fila útil 306 px, CERO desbordamiento horizontal, y el bloque de datos mide
   242 px igual que antes con 1-2 servicios (el "+N" evita la fila extra). A
   1280 la fila de 9 columnas cabe en UNA línea con un servicio y pasa a DOS
   con nombres largos — es densidad, no rotura; el ancho fluido sigue siendo la
   razón de que quepan. Ver `Appointments.razor`, bloque del 804.

   📱 MÓVIL: no cambia NADA. A 390 px el viewport ya es más angosto que 960, así
   que el `max-width` no ata; quitarlo no mueve un pixel (verificado midiendo el
   DOM a 390×844 y 844×390, antes y después). R-323 intacto.
   ⛔ KIOSKO y TV quedan FUERA a propósito: tienen sus propias reglas de ancho
   (`kiosk-wrap`, `tv-queue`) y no llevan este marcador.

   Va DESPUÉS de `page-wide` a propósito: misma especificidad (0,2,0), así que si
   una página trajera los dos marcadores gana el fluido por orden de fuente. */
.content:has(.page-fluid) { max-width: none; }

.page-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: .75rem; flex-wrap: wrap; margin-bottom: 1rem;
}
.page-head h1 { margin: 0; font-size: 1.5rem; }

.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: .5rem; }

/* Home */
/* FULL-PAGE background: the artwork fills the whole main area behind the Home content (quick
   actions + recent float over it). Scoped to Home via `main:has(.home-page)` (no JS). A dimming
   overlay keeps text/cards readable; dark themes override it with --hero-overlay. */
main:has(.home-page) {
    background: var(--hero-overlay, linear-gradient(rgba(15,23,42,0.30) 0%, rgba(15,23,42,0.50) 100%)),
                url('/images/businesscardspro-background.png') center center / cover no-repeat;
}
/* Phone portrait: swap to the portrait artwork. */
@media (max-width: 640.98px) and (orientation: portrait) {
    main:has(.home-page) {
        background: var(--hero-overlay, linear-gradient(rgba(15,23,42,0.30) 0%, rgba(15,23,42,0.50) 100%)),
                    url('/images/businesscardspro-background-mobile.png') center center / cover no-repeat;
    }
}
/* Give the artwork some breathing room at the top before the cards, and make Home text readable
   over the (dark-dimmed) image. The white action/recent cards already contrast on their own. */
.home-page { padding-top: 1.5rem; }

/* == ÍTEM 80: cards/banners ARRIBA de la foto en TODOS
   los tamaños == (antes solo en teléfono: "abajo uno tiene que mirar
   toda la foto primero y eso distrae"). Orden: avisos → cards → foto.
   Tablet se comporta como laptop; móvil conserva su hero bajito. */
.home-flow { display: flex; flex-direction: column; }
.home-flow > .home-find { order: 1; margin-top: .25rem !important; }
.home-flow > .home-cap  { order: 2; margin-top: .5rem !important; }
.home-flow > h5         { order: 3; margin-top: .75rem !important; }
.home-flow > .qc-grid   { order: 4; }
.home-flow > .home-hero { order: 9; margin-top: 1rem; }
@media (max-width: 640.98px) {
    .home-flow > .home-hero { min-height: 300px !important; }
    /* Hero mas bajito en el cel: el saludo no necesita 250px de aire */
    .home-hero .home-hero-pad { padding-top: 130px !important; }
}
.home-page .section-head h2,
.home-page .section-head a { color: #fff; }
.home-page .text-muted,
.home-page .empty-state p { color: rgba(255,255,255,.92) !important; }

.quick-actions {
    display: grid; grid-template-columns: repeat(3, 1fr);
    gap: var(--bcp-gap); margin-bottom: 1.5rem;
}
.quick-action {
    display: flex; flex-direction: column; align-items: center; gap: .5rem;
    padding: 1rem .5rem; background: #fff; border-radius: var(--bcp-radius);
    box-shadow: var(--bcp-shadow); text-decoration: none;
    /* Card is always white, so force dark text (matches Lucid's #0F172A) instead of
       `inherit` — dark themes set a light body color that was unreadable on white. */
    color: #0F172A;
    text-align: center; font-size: .85rem; font-weight: 600;
}
.quick-action:active { transform: scale(.97); }

/* Card grid + thumbnails */
.card-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: var(--bcp-gap);
}
.card-thumb {
    display: block; background: #fff; border-radius: var(--bcp-radius);
    overflow: hidden; box-shadow: var(--bcp-shadow); text-decoration: none; color: inherit;
}
.card-thumb:active { transform: scale(.98); }
.card-thumb-img { aspect-ratio: 16 / 10; background: #f1f3f5; }
.card-thumb-img img { width: 100%; height: 100%; object-fit: cover; }
.card-thumb-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.card-thumb-body { padding: .6rem .75rem; }
.card-thumb-name { font-weight: 600; }
.card-thumb-company { font-size: .85rem; color: #6c757d; margin-bottom: .35rem; }

.category-badge { background: #e7f1ff; color: #1b6ec2; font-weight: 600; }
.notif-badge { background: #dc3545; color: #fff; font-size: .7rem; margin-left: .35rem; }

/* Empty states */
.empty-state {
    display: flex; flex-direction: column; align-items: center; gap: .75rem;
    text-align: center; padding: 2.5rem 1rem; color: #6c757d;
}

/* Filters */
.filters { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1rem; }

/* Capture */
.capture-photos { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 1rem; }

/* Camera-style upload button (replaces the native "Choose File") */
.camera-btn {
    position: relative;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .35rem; padding: 1.1rem 1rem; cursor: pointer;
    border: 2px dashed #cbd5e1; border-radius: var(--bcp-radius);
    color: #334155; background: rgba(0, 0, 0, .02);
    transition: border-color .15s ease, background .15s ease;
}
.camera-btn:hover { border-color: #3b82f6; background: rgba(59, 130, 246, .05); }
.camera-btn i { font-size: 1.9rem; line-height: 1; }
.camera-btn span { font-size: .9rem; font-weight: 500; }
.camera-btn input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.capture-preview, .card-image {
    width: 100%; max-width: 420px; border-radius: var(--bcp-radius);
    box-shadow: var(--bcp-shadow); margin-bottom: .5rem;
}
.card-images { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }

/* Detail */
.card-image-placeholder {
    width: 100%; max-width: 420px; aspect-ratio: 7 / 4;
    display: flex; align-items: center; justify-content: center;
    background: #f1f3f5; border-radius: var(--bcp-radius);
    box-shadow: var(--bcp-shadow); margin-bottom: .5rem;
}
.detail-share { margin-bottom: .75rem; max-width: 420px; }
.detail-name { margin: 0 0 1rem; font-size: 1.5rem; }
.card-details { display: grid; grid-template-columns: auto 1fr; gap: .35rem 1rem; margin: 1rem 0; }
.card-details dt { font-weight: 600; color: #6c757d; }
.card-details dd { margin: 0; }
.category-row { margin: 1rem 0; }
.card-qr { text-align: center; margin: 1.5rem 0; }
.qr-image { width: 180px; height: 180px; }
.qr-image-lg { width: 260px; height: 260px; max-width: 80vw; }
.detail-actions { display: flex; gap: .75rem; flex-wrap: wrap; margin-top: 1rem; }

/* Categories */
.new-category { display: flex; gap: .5rem; margin-bottom: 1rem; }
.category-list { list-style: none; padding: 0; margin: 0; }
.category-list li {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    padding: .75rem .25rem; border-bottom: 1px solid #eee;
}
.category-list li.inactive { opacity: .55; }
.category-actions { display: flex; gap: .35rem; }

/* Notifications */
.notification-list { list-style: none; padding: 0; margin: 0; }
.notification-list li { border-bottom: 1px solid #eee; }
.notification-list li a {
    display: block; padding: .85rem .25rem; text-decoration: none; color: inherit;
}
.notification-list li.unseen { background: #f0f7ff; }

/* Profile QR + scan landing */
.profile-qr, .scan-landing {
    display: flex; flex-direction: column; align-items: center; gap: 1rem;
    text-align: center; padding: 1.5rem 0;
}
.profile-qr-url { font-size: .8rem; color: #6c757d; word-break: break-all; max-width: 90vw; }

/* Share modal */
.modal-backdrop-bcp { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 1040; }
.share-modal {
    position: fixed; left: 50%; bottom: 0; transform: translateX(-50%);
    width: 100%; max-width: 480px; background: #fff; z-index: 1050;
    border-radius: var(--bcp-radius) var(--bcp-radius) 0 0;
    padding: 1rem; max-height: 85vh; overflow-y: auto;
}
.share-modal-header { display: flex; align-items: center; justify-content: space-between; }
.share-modal-header h3 { margin: 0; font-size: 1.2rem; }
.share-target { font-weight: 600; margin: .5rem 0; }
.share-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: .5rem; }
.share-action { display: flex; align-items: center; justify-content: center; gap: .5rem; }
.share-results { list-style: none; padding: 0; margin: .5rem 0 0; }
.share-results li {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    padding: .6rem .25rem; border-bottom: 1px solid #eee;
}
.share-link { margin: .25rem 0 .5rem; }

/* Public share landing (/c/{token}) */
.public-content { max-width: 520px; margin: 0 auto; padding: 1.25rem 1rem 2.5rem; }
.public-card { display: flex; flex-direction: column; }
.public-card-images { display: flex; flex-direction: column; gap: .75rem; margin-bottom: 1rem; }
.public-card-image {
    width: 100%; border-radius: var(--bcp-radius); box-shadow: var(--bcp-shadow);
}
.public-card-name { font-size: 1.5rem; margin: 0 0 .5rem; }
.public-invite {
    margin-top: 1.75rem; padding: 1.25rem; text-align: center;
    background: #f1f3f5; border-radius: var(--bcp-radius);
}
.public-invite h2 { font-size: 1.2rem; margin: 0 0 .5rem; }
.public-invite p { color: #6c757d; margin: 0 0 1rem; }

/* Tablet / desktop */
@media (min-width: 768px) {
    .page-head h1 { font-size: 1.85rem; }
    .filters { flex-direction: row; }
    .filters .form-control, .filters .form-select { flex: 1; }
    .capture-photos { flex-direction: row; }
    .capture-slot { flex: 1; }
    .share-modal {
        bottom: auto; top: 50%; transform: translate(-50%, -50%);
        border-radius: var(--bcp-radius);
    }
}

/* ================================================================
   Accessibility utility classes — applied to <html> by
   accessibility-manager.js. !important so they win over theme
   and Bootstrap specificity.
   ================================================================ */

/* ── Font scale ────────────────────────────────────────────────── */
html.a11y-font-115 { font-size: 115% !important; }
html.a11y-font-130 { font-size: 130% !important; }
html.a11y-font-150 { font-size: 150% !important; }
/* Item 31: un nivel MAS grande para leer sin espejuelos */
html.a11y-font-175 { font-size: 175% !important; }

/* ── Items 30/54: forms RESILIENTES a fonts grandes y
   pantallas angostas — labels/textos largos hacen wrap en vez de montarse ── */
label.form-label, .form-check-label { overflow-wrap: anywhere; }
/* ÍTEM 534: se retira `.form-floating > label { white-space: normal; ... }`. Era un
   intento previo de aliviar el mismo choque etiqueta/ejemplo dejando que la etiqueta
   flotante hiciera wrap, pero una etiqueta flotante de dos líneas se desborda del alto
   fijo del campo y tapa MÁS el ejemplo. Con el arreglo real (etiqueta encima del campo)
   ya no queda ningún `form-floating` en la app y la regla sobra. */
.form-control, .form-select { min-width: 0; }
/* La rejilla de Bootstrap no deja que una columna se desborde sobre la vecina */
.row > [class*="col-"] { min-width: 0; }

/* ── FIX QA (Tito 2026-07-22): la flecha del <select> y su reservado ──────────
   Bootstrap fija la flecha en 16px y reserva 2.25rem a la derecha; con fuentes de
   accesibilidad (175%) ese reservado en px/rem NO acompaña al valor y "Todas las
   tiendas/compañías" se truncaba a ~390px. Se expresa TODO en `em` (escala con la
   fuente del propio control): la flecha crece proporcional y el reservado se ciñe
   a su tamaño real, devolviéndole ancho al valor. En 100% queda idéntico a
   Bootstrap (1em≈16px, .75em≈12px). */
.form-select {
    background-size: 1em .75em;
    background-position: right .55em center;
    padding-right: 1.9em;
}
/* Con fuentes de accesibilidad (115–175%) el max-width FIJO en px de los <select>
   de filtro (ej. inline style="max-width:185px" en Localidades/Reportes) NO escala
   y "Todas las tiendas/compañías" se truncaba. Bajo escala a11y se libera el tope
   (los filtros viven en filas .d-flex flex-wrap → el select crece y hace wrap si
   hace falta, mostrando el valor completo). Sólo afecta a usuarios con fuentes
   grandes; el layout al 100% queda intacto. */
html[class*="a11y-font-1"] select.form-select { max-width: none !important; }

/* ── Batch F A1 (fix definitivo QA Lupe): el <select> de State se AUTO-AJUSTA a
   su opción MÁS ANCHA ("DC — District of Columbia") y NUNCA trunca, en cualquier
   viewport (móvil/tablet/desktop) y a CUALQUIER fuente de accesibilidad
   (100/115/130/150/175%). Va dentro de una columna col-auto (col-12 col-md-auto):
   en móvil ocupa todo el ancho (apilado como los demás campos); de md (768px) en
   adelante el select pasa a inline-block width:auto y la columna col-auto crece a
   su contenido, así el nombre completo del estado siempre cabe. El width:auto
   escala con el font-size, por eso resiste el panel de accesibilidad. */
.form-select.bsp-state-select { width: 100%; }
@media (min-width: 768px) {
    /* .form-select.bsp-state-select (0,2,0) gana sobre Bootstrap y cualquier
       tema que ponga width en .form-select (0,1,0), sin depender del orden de
       carga */
    .form-select.bsp-state-select { display: inline-block; width: auto; max-width: 100%; }
}

/* ── Highlight links ───────────────────────────────────────────── */
html.a11y-highlight-links a:not(.nav-link):not(.btn):not(.page-link):not(.breadcrumb-item),
html.a11y-highlight-links .btn-link {
    text-decoration:  underline !important;
    text-decoration-thickness: 2px !important;
    text-underline-offset: 2px !important;
    font-weight:      700 !important;
    outline:          1px dashed currentColor !important;
    outline-offset:   1px !important;
}

/* ── Highlight headings ────────────────────────────────────────── */
html.a11y-highlight-headings h1,
html.a11y-highlight-headings h2,
html.a11y-highlight-headings h3,
html.a11y-highlight-headings h4,
html.a11y-highlight-headings h5,
html.a11y-highlight-headings h6 {
    border-left:  4px solid var(--accent-color, #3B82F6) !important;
    padding-left: 0.6rem !important;
    margin-left:  -0.6rem !important;
}

/* ── Enhanced focus ────────────────────────────────────────────── */
html.a11y-focus-enhanced *:focus,
html.a11y-focus-enhanced *:focus-visible {
    outline:        3px solid var(--accent-color, #3B82F6) !important;
    outline-offset: 3px !important;
    box-shadow:     0 0 0 5px rgba(59, 130, 246, 0.25) !important;
}

/* ── Text spacing (WCAG 1.4.12) ────────────────────────────────── */
html.a11y-text-spacing p,
html.a11y-text-spacing li,
html.a11y-text-spacing td,
html.a11y-text-spacing th,
html.a11y-text-spacing label,
html.a11y-text-spacing span,
html.a11y-text-spacing h1,
html.a11y-text-spacing h2,
html.a11y-text-spacing h3,
html.a11y-text-spacing h4,
html.a11y-text-spacing h5,
html.a11y-text-spacing h6 {
    line-height:     1.75 !important;
    letter-spacing:  0.05em !important;
    word-spacing:    0.1em !important;
}

/* ── Reduce motion ─────────────────────────────────────────────── */
html.a11y-reduce-motion *,
html.a11y-reduce-motion *::before,
html.a11y-reduce-motion *::after {
    animation-duration:       0.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:      0.01ms !important;
    scroll-behavior:          auto !important;
}

/* ── HC themes: sidebar overrides via class ────────────────────── */
.sidebar.theme-hc-light,
.top-row.theme-hc-light {
    background:   #000000 !important;
    border-bottom: 3px solid #0040CC !important;
    color:         #FFFFFF !important;
}

.sidebar.theme-hc-dark,
.top-row.theme-hc-dark {
    background:   #000000 !important;
    border-bottom: 3px solid #FFFFFF !important;
    color:         #FFFFFF !important;
}

/* ── Card cropper overlay ──────────────────────────────────────── */
#bcp-cropper-overlay {
    position: fixed; inset: 0; z-index: 1080;
    background: rgba(0, 0, 0, 0.85);
    display: none; align-items: center; justify-content: center;
}
.bcp-cropper-box {
    display: flex; flex-direction: column;
    width: 100%; max-width: 640px; height: 100%; max-height: 100vh; padding: 1rem;
}
.bcp-cropper-hint { color: #fff; text-align: center; margin-bottom: .5rem; font-weight: 600; }
.bcp-cropper-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.bcp-cropper-stage img { max-width: 100%; max-height: 100%; display: block; }
.bcp-cropper-actions { display: flex; gap: .5rem; justify-content: center; padding-top: 1rem; }
.bcp-cropper-actions .btn { flex: 1; max-width: 160px; }
.bcp-cropper-actions .bcp-cropper-rotate { flex: 0 0 auto; max-width: 52px; font-size: 1.25rem; }
/* == Estilos de los CRUDs (BarberShopPro) == */
.crud-header-icon { width: 38px; height: 38px; }
.crud-avatar {
    display: inline-flex; width: 40px; height: 40px; border-radius: 50%;
    object-fit: cover; align-items: center; justify-content: center;
}
.crud-avatar-initials {
    background: linear-gradient(135deg, #14224E, #14224E);
    color: #FFFFFF; font-weight: 700; font-size: 14px;
}
.crud-store-thumb {
    width: 72px; height: 48px; object-fit: cover; border-radius: 8px;
    border: 2px solid var(--accent-color, #14224E);
}
/* ══ QA-876-H1 (Lupe, 2026-08-27) — LOS DÍGITOS DEL DINERO VUELVEN A HACER
   COLUMNA. Inter trae dígitos PROPORCIONALES; la pila vieja (Segoe/Arial) los
   traía tabulares de gratis, así que al cambiar la tipografía las cifras entre
   filas dejaron de alinearse sin que nadie tocara una tabla.
   MEDIDO en /reports/day-close antes del arreglo: `1` = 9.75px contra `4` =
   15.30px en la misma clase, y "$1,111.11" = 84.28px contra "$8,888.88" =
   115.92px — 31.6px de diferencia entre dos filas de la misma columna.
   ⚠️ Va AQUÍ y no en `body`: `.crud-price` es la clase del DINERO (75 usos, todos
   cifras). El texto normal no quiere dígitos tabulares — le abre huecos feos
   alrededor del `1`. `CheckoutPanel` y `.st-tnum` ya venían protegidos aparte. */
.crud-price { color: var(--bsp-price-ink, #8F6608); font-weight: 700; font-variant-numeric: tabular-nums; }
/* ══ ÍTEM 1152 (Jack, 2026-09-04) — LA TINTA DEL PRECIO SALE DE `--bsp-price-ink`, UNA POR TEMA.
   El dorado #B8860B medía 3,25:1 sobre blanco y 2,99 sobre crema (AA texto = 4,5) en los
   temas CLAROS (theshop, lucid, hc-light) — y en los OSCUROS medía 4,67–6,08 (pasa). Por eso
   la tinta NO es un hex único: cada `theme-*.css` declara `--bsp-price-ink` en su `:root`
   (claros: #8F6608 = 5,16 blanco / 4,73 crema; oscuros: #B8860B, sin cambio visual). La hoja
   del tema carga DESPUÉS de este fichero, así que su `:root` gana; el fallback de aquí es el
   valor claro. Medido en píxeles (texto transparente, peor ventana) con
   tools/arnes-1152-jack/medir-1152.py — el mismo script en la copia PRE y en la FIX.
   Lo que NO cambia a propósito: `.enc-stars` (estrellas decorativas, no texto) y los BORDES
   dorados; el TV (LV-46/47) no usa esta clase. */

/* 🔎 QA-876-H1, LA MITAD QUE FALTABA (hallazgo de Tito al reproducirlo).
   En la MISMA columna del Cuadre conviven celdas con `.crud-price` y celdas de
   dinero SIN ella — la fila de Impuestos la lleva; las del desglose (Estatal,
   Municipal, Federal, sin desglosar) y las de reembolsos/crédito/faltante, no.
   📌 NO SE ENUMERAN LAS LÍNEAS ([[LEY 19]] ①: aquí había once números y los once
   apuntaban a nada — el fichero creció 9 líneas por encima y la lista caducó sin
   que nada fallara). Se cuentan así, y esto sigue valiendo mañana:
       grep -rn 'text-end crud-price' --include=DayCloseReport.razor .   # las que la llevan
       grep -rn 'class="text-end"'    --include=DayCloseReport.razor .   # las que NO
   (sin la ruta a propósito: `wwwroot/app.css` se sirve TAL CUAL al público y no
    hace falta regalarle a nadie el árbol de carpetas.)
   ⚠️ Para ESTA cuenta NO sirve `tools/solo_codigo.py`: blanquea los literales, y
   el nombre de una clase CSS vive dentro de una cadena ⇒ devuelve 0 donde el grep
   crudo devuelve decenas. **Sub-reporta, que es el lado peligroso.**
   Arreglar solo `.crud-price`
   dejaba media columna tabular y media proporcional — o sea, la columna seguía
   sin cuadrar, que es justo el síntoma que se reportó.
   La regla se ancla en la COLUMNA, no en el texto: una celda de tabla alineada
   a la derecha es, por convención de la casa, una columna de números. No es
   global — `body` y el texto corrido se quedan con los dígitos proporcionales
   de Inter, que es como deben leerse. Las celdas de esas columnas que llevan
   texto ("—", un nombre) no tienen dígitos, así que no cambian de ancho. */
.table td.text-end, .table th.text-end { font-variant-numeric: tabular-nums; }
/* ══ MINI-LOTE POST-PUBLISH #2/#4 (el dueño 2026-08-24) — TÍTULO DE COLUMNA EN
   2 LÍNEAS. La clave del diccionario lleva un \n en el punto EXACTO del corte
   (verbatims: "Código de salida"/"del Kiosco" · "Orden"/"del menú") y esta
   regla lo respeta SOLO en los th marcados con la Class="bsp-th-2l" del
   QuickGrid — en cualquier otro sitio donde se pinte esa clave, el \n del
   HTML colapsa a un espacio normal y no rompe nada.
   ⚠️ `pre` y NO `pre-line`, MEDIDO: con pre-line el auto-layout de la tabla
   encogía la columna a min-content y "Kiosk Exit Code" salía en 3 líneas;
   con `pre` el ÚNICO corte es el \n del diccionario — exactamente las 2
   líneas del verbatim, y la columna mide su línea más larga. */
th.bsp-th-2l .col-title-text { white-space: pre; overflow: visible; text-overflow: clip; line-height: 1.2; }
/* ══ ÍTEM 1107 (Fabio 2026-09-03) — COLUMNA DE ACCIONES FIJA A LA DERECHA.
   Medido: aun con "Código postal" y "Correo electrónico" ya en 2 líneas, una
   tabla ancha (Companies, y cualquier rejilla parecida) puede seguir
   desbordando .table-responsive a ciertos anchos/idiomas — el objetivo real
   de Héctor era "que los botones se vean", no una columna concreta. Con
   Class="bsp-col-actions-sticky" en la TemplateColumn de acciones del
   QuickGrid, Editar/Borrar quedan pegados al borde derecho del contenedor
   con scroll (.table-responsive es el ancestro con overflow-x, así que
   `right:0` se ancla ahí y no a la ventana) y NO se van con el scroll.
   Mismo patrón de sticky+sombra que YA usa `.sch-time-gutter` (línea ~4577)
   para el gutter de horas de la agenda — ahí a la izquierda, aquí a la
   derecha; es el ÚNICO precedente de columna sticky en una rejilla de la
   casa, y no existía ya aplicado a una columna de acciones en NINGUNA tabla
   (grep de "sticky" en Components/: agenda de Schedule.razor y barras de
   acción de formulario, nada de QuickGrid). Ayudante NUEVO, aplicado SOLO en
   Companies este lote — Employees/Customers/Stores quedan como barrido
   pendiente (mismo QuickGrid, mismo patrón, otro turno).
   Fondo OPACO explícito (si no, las columnas que pasan por debajo al hacer
   scroll se transparentarían sobre los botones) — mismo trade-off ya
   aceptado en `.sch-time-gutter`: esa celda no participa del tinte de
   `.table-hover` porque su fondo es fijo, no el que compone Bootstrap por
   fila; visualmente pasa desapercibido porque son solo 2 botones pequeños.
   🔴 MEDIDO (Tito, primera vuelta): con solo `.bsp-col-actions-sticky {background:...}`
   el fondo salía TRANSPARENTE de verdad — la columna de al lado (Código
   postal) se veía A TRAVÉS de los botones, mezclada ("B032771"). Causa
   encontrada por DOM, no supuesta: Bootstrap pinta la celda con
   `.table>:not(caption)>*>*{background-color:var(--bs-table-bg);
   box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,...)}` — especificidad
   (0,1,1) porque `:not(caption)` cuenta como el tipo de su argumento; una
   sola clase mía es (0,1,0) y PIERDE aunque app.css cargue después. `td`+clase
   iguala la especificidad (0,1,1) y app.css cargando después del bootstrap.min.css
   de `App.razor:17/20` desempata a mi favor — verificado de nuevo por DOM
   tras el cambio: `background-color` deja de ser `rgba(0,0,0,0)`. */
td.bsp-col-actions-sticky, th.bsp-col-actions-sticky {
    position: sticky; right: 0; z-index: 2;
    background-color: var(--surface-color, #fff);
    box-shadow: -2px 0 6px rgba(11, 18, 32, .06); white-space: nowrap; }
thead th.bsp-col-actions-sticky { z-index: 3; }
/* ══ ÍTEM 1122 (Vera, hallazgo 2026-09-04, sobre Companies; barrido 1107b lo
   confirmó en Ads/Customers/Employees/ServiceTypes/Services/Stores/
   SurveyQuestions) — EDITAR/BORRAR ≥44PX TÁCTIL en la columna de acciones.
   Medido: el `.btn-sm` de Bootstrap da 31px de alto (LEY 24). Aquí SÍ crece
   el botón de verdad — a diferencia del `::after` invisible que usa esta
   casa para círculos/glifos sueltos (agrandar el CÍRCULO se ve mal, ítem
   463/1105), un botón rectangular con ícono+texto no tiene ese problema:
   crecer el padding es exactamente como se ve un botón `.btn-lg` normal.
   Scopeado a la MISMA celda que ya lleva `bsp-col-actions-sticky` (ítem
   1107) — no hace falta tocar la clase de cada botón uno por uno.
   `.bsp-row-actions44` es el mismo alto SIN el sticky/fondo/sombra, para la
   vista de tarjeta "1 por persona" de Customers (ítem 64): ahí no hay
   `.table-responsive` que desborde — es un flex, no una tabla.
   🔴 ÍTEM 1170 (Vera 2026-09-05) — ESTA LISTA DE PANTALLAS ES UN INVENTARIO, Y
   CADUCÓ: `/admin/users` (Components\Pages\Admin\Users.razor) NO estaba en ella
   y sus 8 botones seguían en 31 px (32 de blanco táctil, medido con dedo y
   sondeo de la LEY 24 a 390/768/1366/1920). Es la pantalla con MÁS botones por
   fila de la casa, y dos de ellos son Bloquear y Borrar. Se le puso
   `bsp-row-actions44` en la celda de acciones — NO `bsp-col-actions-sticky`,
   porque fijar la columna la movería de sitio y el ítem 1155 acaba de poner
   pisos de ancho en Nombre/Correo en esa misma tabla.
   ⚠️ Al siguiente que añada una rejilla con acciones: la pregunta no es
   "¿falta ésta?" sino "¿cuántas tablas con columna de acciones hay y cuántas
   llevan la clase?" (LEY 14). NO he barrido las demás en esta vuelta.
   ✅ ÍTEM 1122b (Tito 2026-09-06) — ESE BARRIDO, HECHO, con detector que cuenta
   `btn-sm` DENTRO de un bloque <table>/<QuickGrid> (188 .razor leídos): de los
   "22 ficheros" del ensayo 05b, 8 tenían botones de acción dentro de una tabla
   sin la clase (Roles, LegalDocuments, BlocksPage, BusinessHoursPage,
   DayCloseReport, CompanyEdit, StoreEdit, EmployeeEdit — los dos últimos con
   flechas que ya llevaban `bsp-touch44`) y 14 tenían el `btn-sm` FUERA de la
   tabla (filtros/exportar/enlaces de contacto públicos): esos NO son de este
   ítem y quedan anotados en la ficha 1122b, no escondidos. Los botones
   SOLO-ÍCONO (Roles, BusinessHours) llevan además `bsp-touch44`: esta regla da
   el ALTO, no el ancho. */
.bsp-col-actions-sticky .btn, .bsp-row-actions44 .btn { min-height: 44px; }
/* ══ ÍTEM 873 · R-477 lote 0 (Tito 2026-09-14) — el switch «¿paga por adelantado?»
   del par (cliente, barbero). Vive en la ficha del cliente y en el modal de la
   cita (CustomerPrepaySwitch.razor). Select y botón a 44 px de alto en toda
   pantalla: se toca de pie, con el celular, casi siempre (873). */
.cpp-box { border: 1px solid var(--bs-border-color, #dee2e6); border-radius: .5rem; padding: .75rem; max-width: 620px; }
.cpp-box.cpp-compact { padding: .5rem .75rem; margin-top: .5rem; }
.cpp-box .cpp-select, .cpp-box .cpp-save { min-height: 44px; }
/* ══ ÍTEM 1122b (Tito 2026-09-06) — «← VOLVER» DEL COMPONENTE COMPARTIDO
   `Shared\BackToOrigin.razor` (ítem 508) A 44 PX DE DEDO, SIN TOCAR EL ESCRITORIO.
   Medido por Lupe (acta 508/1155): 78×31 con dedo en TODAS las pantallas que lo
   usan (10 puertas: CompanyEdit, BusinessHoursPage, CustomerEdit, EmployeeEdit,
   InventoryAdjust, Payments, ServiceTypes, Services, StoreEdit ×2). Es el patrón
   de `.gb-cta-44` (ítem 1074) y de `.appt-win-out-btn`: los 44 cuelgan del
   PUNTERO (`pointer: coarse`), no del ancho — así el ratón conserva el `btn-sm`
   de siempre y la tablet del mostrador y el celular reciben el dedo. La clase
   `back-to-origin` YA estaba en el botón: cero cambio de markup. */
@media (pointer: coarse) {
    .back-to-origin { min-height: 44px; display: inline-flex; align-items: center; }
}
/* ══ ÍTEM 1153(a) (Jack 2026-09-04) — LA COLUMNA PEGAJOSA NO PUEDE COMERSE MÁS
   DE LA MITAD DEL CELULAR. Medido en /setup/services a 390 (Playwright propio,
   primera fila): `.table-responsive` deja 308 px visibles y la celda pegajosa
   con Editar+Borrar en línea mide 189 px (ES, 61 %) / 177 px (EN, 57 %) ⇒ el
   nombre se veía al 33 %, el precio al 0 % y el asa ☰ al 1 % incluso con el
   scroll al máximo (la parte visible que quedaba, 119 px, era más angosta que
   la columna del orden). Bajo 576 px los dos botones se APILAN (uno debajo del
   otro): la celda pasa a medir lo que mide el botón más ancho (~95 px), y queda
   más de dos tercios del ancho para el contenido. No cambia el patrón aprobado
   (1107/1122): columna fija a la derecha, botones ≥44 px con ícono+texto —
   solo la dirección, y solo en el celular. OPT-IN por clase (hoy solo Services):
   las otras 7 tablas del 04a conservan su aspecto hasta que se decida extenderlo.
   `!important` en el margen: el `me-1` de Bootstrap ya es `!important`. */
@media (max-width: 575.98px) {
    td.bsp-col-actions-stack-sm .btn { display: block; width: 100%; margin-right: 0 !important; }
    td.bsp-col-actions-stack-sm .btn + .btn { margin-top: .25rem; }
}
/* ══ MINI-LOTE POST-PUBLISH #6/#7 — blanco táctil de 44px para las flechitas
   de ordenar en las fichas (tablet en el mostrador; regla de la casa). */
.bsp-touch44 { min-width: 44px; min-height: 44px; }

/* ══ 🖱️ ÍTEM 1105 — EL ASA DE ARRASTRE DEL ORDEN DEL MENÚ ═══════════════════════
   `Setup\Services.razor`, `bsp-menu-drag.js`. El asa se DIBUJA pequeña (mismo
   tamaño que el ícono de una flecha vecina) pero se TOCA en 44×44, con el mismo
   patrón de LEY 24 de la casa: el blanco táctil vive en el ENVOLTORIO y en un
   `::after`, nunca en el glifo — así el círculo grande no le roba espacio a las
   flechas ni al número que están al lado en la misma celda angosta. */
.bsp-drag-handle-wrap { display: inline-flex; position: relative; cursor: grab; touch-action: none; }
.bsp-drag-handle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.15rem; height: 1.15rem; color: var(--bs-secondary-color, #6c757d);
    font-size: .95rem; line-height: 1; user-select: none;
}
.bsp-drag-handle-wrap::after { content: ""; position: absolute; inset: -.3rem; }
@media (pointer: coarse) {
    .bsp-drag-handle-wrap { min-width: 44px; min-height: 44px; justify-content: center; align-items: center; }
    .bsp-drag-handle-wrap::after { inset: 0; }
}
/* Fila que se está arrastrando: se atenúa (no se oculta — el usuario necesita
   seguir viéndola para saber qué está moviendo). */
tr.bsp-menu-dragging { opacity: .45; }
/* Fila destino: raya arriba/abajo marca DÓNDE caería al soltar (antes/después
   de esa fila), igual que el marcador punteado del 429 en la Agenda. */
tr.bsp-menu-drop-before { box-shadow: inset 0 3px 0 0 var(--bs-primary, #14224E); }
tr.bsp-menu-drop-after { box-shadow: inset 0 -3px 0 0 var(--bs-primary, #14224E); }
/* Fila de otra compañía: no se puede soltar ahí (ítem 1105 — "reordenar solo
   dentro de la misma compañía"). Rojo tenue, no un candado ni un cursor: la
   fila sigue siendo una fila real, solo no es un destino válido AHORA. */
tr.bsp-menu-drop-deny { box-shadow: inset 0 0 0 2px var(--bs-danger, #dc3545); }

/* == Tarjetas de acceso rapido del Home (patron PSP) == */
.qc-grid { display: flex; flex-wrap: wrap; gap: 14px; }
/* ÍTEM 270 cosmético (Lupe): a ~≤1024px el botón flotante de a11y (fijo abajo-
   derecha, 52px + 24px de margen) podía ROZAR la última fila de "Quick access"
   al hacer scroll. Un respiro inferior en la grid reserva ese alto (76px) para
   que el FAB descanse sobre espacio vacío — sin mover el FAB ni tocar el layout
   en pantallas anchas. */
@media (max-width: 1024px) {
    .qc-grid { padding-bottom: 4.75rem; }
}
.qc-card {
    flex: 1 1 200px; max-width: 260px; padding: 0 0 14px 0;
    overflow: hidden; /* la cinta respeta el radio del card */
    background: var(--surface-color, #FFFFFF);
    border: 1px solid var(--border-color, #D8D4C8);
    border-radius: 14px; text-decoration: none;
    color: var(--bs-body-color, #14224E);
    transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
}
.qc-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 6px 18px rgba(0,0,0,.14);
    border-color: var(--accent-color, #C8102E);
    color: var(--bs-body-color, #14224E);
}
/* La CINTA: banda clara de lado a lado en el tope,
   ~1/3 del card, con el icono centrado adentro — visible en TODO tema */
.qc-band {
    /* Ajuste: icono ARRIBA A LA IZQUIERDA y mas
       pequeno que la cinta, para que quepa comodo */
    display: flex; align-items: center; justify-content: flex-start;
    width: 100%; height: 62px; padding-left: 16px;
    background: var(--icon-chip-bg, #F1F5F9);
    border-bottom: 1px solid var(--border-color, rgba(0,0,0,.08));
    margin-bottom: 10px;
}
.qc-icon { width: 34px; height: 34px; display: block; }
.qc-icon-bi { font-size: 1.5rem; color: var(--accent-color, #C8102E); line-height: 1; }
.qc-title { font-weight: 700; padding: 0 16px; }
.qc-sub { font-size: .85rem; opacity: .7; padding: 0 16px; }

/* == Submenus del NavMenu (reorganizacion 2026-07-04) == */
.nav-group-header {
    display: flex; align-items: center; width: 100%;
    background: none; border: none; cursor: pointer;
    padding: 0.55rem 1.5rem 0.35rem 1rem; margin-top: 0.35rem;
    color: var(--text-secondary, #BFDBFE);
    font-size: 0.72rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.06em;
    text-align: left;
}
.nav-group-header:hover { color: #FFFFFF; }
.nav-group-chevron { margin-left: auto; font-size: 0.7rem; }
.nav-sub .nav-link { padding-left: 1.6rem; font-size: 0.92rem; }

/* == Schedule grid == */
.sch-table thead th { background: var(--sidebar-bg, #14224E); color: #fff; }
.sch-appt-start { background: rgba(37, 99, 235, .18); border-left: 3px solid var(--accent-color, #2563EB); }
.sch-appt-cont { background: rgba(37, 99, 235, .08); }

/* == Schedule Pickup/Finish == */
.sch-btn { font-size: .68rem; padding: .1rem .45rem; }
.sch-appt-active { background: rgba(22, 163, 74, .22); border-left: 3px solid #16A34A; }
.sch-appt-done { background: rgba(100, 116, 139, .15); border-left: 3px solid #64748B; }
/* Pole FOTORREALISTA en linea: reemplaza el emoji 💈 en
   toda la UI; escala con el font-size del texto que acompana */
.pole-icon { height: 1.05em; width: auto; vertical-align: -0.14em; }

/* PASO FALTANTE en el wizard de reserva: al rebotar la
   validacion, el paso incompleto se marca en ambar/rojo — se limpia solo al
   completarlo (la clase es condicional al estado real) */
.book-missing { outline: 2px solid #DC2626; background: #FEF3C7; border-radius: 8px;
    padding: 2px 8px; animation: bookMissingPulse 1.2s ease-in-out 2; }
@keyframes bookMissingPulse { 50% { outline-color: #F59E0B; } }

/* == ESCALA DE FOTOS DE LA CASA (Batch F, item 62: "estan
   muy pequenas y no se distinguen bien") ==
   3 tamaños estándar reutilizables:
     .bsp-photo-sm  56px — thumbnail de LISTA/tabla
     .bsp-photo-md  96px — card de índice / detalle medio
     .bsp-photo-lg 150px — perfil / landing
   Las clases viejas (svc-photo, loc-photo, edit-title-photo, bsp-dd-photo) se
   AGRANDARON a esta escala en vez de reemplazarlas (menos churn de markup). */
.bsp-photo-sm { width: 56px; height: 56px; object-fit: cover; border-radius: 10px; flex-shrink: 0; }
.bsp-photo-md { width: 96px; height: 96px; object-fit: cover; border-radius: 14px; flex-shrink: 0; }
.bsp-photo-lg { width: 150px; height: 150px; object-fit: cover; border-radius: 18px; flex-shrink: 0; }

/* Foto del SERVICIO en chips/listas: "los servicios se
   ven bien plain" — thumb cuadradito al lado del nombre; sin foto = tijeras.
   Item 62: 30px -> 48px (que se distinga el estilo sin entrecerrar los ojos) */
.svc-photo { width: 48px; height: 48px; object-fit: cover; border-radius: 8px;
    flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; }
.svc-photo-empty { background: var(--icon-chip-bg, #F1F5F9); color: #94A3B8; }
.svc-photo-sm { width: 34px; height: 34px; border-radius: 6px; }

/* ══ R-472 / ÍTEM 1164 — LV-70 pt.5 (Héctor: «sí», 2026-09-05) ══════════════
   `.svc-photo` la comparten los SERVICIOS y los PRODUCTOS (`/products/…`, en
   `Pos.razor:474` y `Products.razor:256`). En la vuelta 1 la dejé fuera del
   lote porque tocarla movía el encuadre de fotos que NINGUNA regla escrita
   cubre (LEY 2 🟡: no es fallo, es decisión que nadie ha tomado). Eugenio (9)
   midió el coste de dejarla: en Setup›Servicios, peor `visibleTop` = 22,083 %
   ⇒ le corta la coronilla a LAS 153 fotos de retrato del catálogo — porque
   `/services/{id}/photo.jpg` devuelve el HERO DEL MODELO cuando el servicio no
   tiene foto propia (cascada R-423, `Program.cs:1189-1195`).
   Las dos salidas quedaron medidas y Héctor eligió: **clase propia**. La otra
   habría exigido escribir una EXCEPCIÓN en R-472, que hoy dice «toda
   superficie» — y él prefirió no tener excepción.
   Modificador, no clase nueva: la geometría sigue viviendo en `.svc-photo` (un
   solo sitio decide el tamaño) y esto solo mueve el foco. Va DESPUÉS de
   `.svc-photo` a propósito: misma especificidad (0,1,0) ⇒ manda el orden.
   Se marca SOLO en los `<img>` de SERVICIO (Setup›Servicios, EmployeeEdit,
   StoreEdit); los `<span class="svc-photo svc-photo-empty">` son la tijerita de
   «sin foto» — no hay imagen que recortar — y los de PRODUCTO se quedan como
   estaban, que es justo lo que esta partición protege. */
.svc-photo-model { object-position: 50% 0; }

/* == ÍTEM 93: CARD DE SERVICIO DE 2 FILAS ==
   En teléfono real la fila única (foto | nombre | precio) rompía: nombres
   partidos palabra a palabra, precio apretado, texto fuera del rectángulo.
   DISEÑO ELEGIDO: foto GRANDE RECTANGULAR a la izquierda ABARCANDO LAS DOS
   FILAS (esquinas apenas suavizadas — NO circular: el círculo se reserva
   para fotos de PERSONAS/barberos) | columna derecha apilada: fila 1 =
   NOMBRE (todo el ancho restante, wrap natural) + (duración); fila 2 =
   PRECIO. Nada se sale del card. Mismo diseño en TODOS los tamaños.
   La foto creció con el layout (regla del ítem 62): 48px -> 104x78. */
.svc-card { display: flex; align-items: center; gap: .65rem;
    border: 1px solid var(--border-color, rgba(0,0,0,.175));
    border-radius: .5rem; padding: .5rem; margin-bottom: .4rem; min-width: 0; }
/* R-472 / ÍTEM 1164 — LA TARJETA DE SERVICIO DE LA PÁGINA PÚBLICA. Es la que
   ve el cliente antes de reservar, y NUNCA llevó `object-position`: el
   navegador centraba al 50 % y, con 104x78, el recorte empezaba en el 29,1 %
   de la foto — le cortaba la coronilla a LAS 153 fotos de retrato del catálogo
   (medido en el DOM, arnés 1164). Héctor: «lo más importante es su recorte».
   Esta superficie NO estaba en la lista de Kahlo por fichero: ella la nombró
   en prosa («tarjetas de servicio públicas») apuntando al `.razor.css` de la
   galería, y vive aquí. Se encontró grepeando `object-fit:cover`, no
   razonando (LEY 14). Usada por StorePublic.razor y BarberPublic.razor. */
.svc-card-photo { width: 104px; height: 78px; object-fit: cover; object-position: 50% 0;
    border-radius: 8px; flex-shrink: 0; display: inline-flex;
    align-items: center; justify-content: center; font-size: 1.6rem; }
.svc-card-body { flex: 1 1 0; min-width: 0; display: flex;
    flex-direction: column; gap: .2rem; }
.svc-card-name { overflow-wrap: break-word; line-height: 1.3; }
.svc-card-price { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; }
/* BUG-1 QA Batch I: con fonts de accesibilidad 150/175% la foto FIJA de 104px
   + paddings en rem que escalan colapsaban la columna de texto (~64px a
   360px@150% — nombre partido palabra a palabra y el btn-group ×N fuera del
   card; a 175% peor). FIX: a 150/175% el card APILA — la foto encoge (72x54,
   sigue rectangular) y queda arriba junto al checkbox; el TEXTO baja a su
   propia fila A TODO EL ANCHO del card (flex-wrap + basis 100%): a 360px@175%
   nada desborda. Técnica de la casa: clases del accessibility-manager en html. */
html.a11y-font-150 .svc-card,
html.a11y-font-175 .svc-card { flex-wrap: wrap; }
html.a11y-font-150 .svc-card-photo,
html.a11y-font-175 .svc-card-photo { width: 72px; height: 54px; }
html.a11y-font-150 .svc-card-body,
html.a11y-font-175 .svc-card-body { flex: 1 1 100%; min-width: 100%; }

/* Encabezado de categoria en el wizard de reserva (item 5) */
.svc-cat-header {
    font-weight: 600;
    font-size: .82rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--bs-secondary-color, #64748B);
    padding: .5rem .25rem .15rem;
    border-bottom: 2px solid var(--bs-border-color, #E2E8F0);
    margin-top: .35rem;
}
.svc-cat-header:first-child { margin-top: 0; }

/* Time-off en la Agenda: celda gris rayada = barbero no disponible */
.sch-cell-off { background: repeating-linear-gradient(45deg,
    rgba(100, 116, 139, .20) 0 6px, rgba(100, 116, 139, .08) 6px 12px); }

/* == AGENDA POSICIONADA POR HORA EXACTA (item 3) ==
   Cada cita es un bloque absoluto (top/height = minutos * px), no snapped a la
   celda -> dos citas de 15 min consecutivas no se encabalgan. */
.sch-agenda { display: block; }
.sch-agenda-head { display: flex; position: sticky; top: 0; z-index: 3;
    background: var(--sidebar-bg, #14224E); }
.sch-agenda-head-cell { color: #fff; text-align: center; padding: .25rem .2rem;
    line-height: 1.15; font-size: .8rem; }
.sch-agenda-body { display: flex; }
.sch-time-gutter { width: 72px; min-width: 72px; flex: 0 0 72px; }
.sch-barber-col { flex: 1 1 120px; min-width: 120px; max-width: 220px;
    border-left: 1px solid var(--bs-border-color, #E2E8F0); }
.sch-agenda-head .sch-barber-col { border-left: 1px solid rgba(255,255,255,.15); }
.sch-gutter-label { position: absolute; right: 6px; transform: translateY(-50%);
    font-size: .72rem; color: var(--bs-secondary-color, #64748B); white-space: nowrap; }
.sch-grid-line { position: absolute; left: 0; right: 0; height: 0;
    border-top: 1px dashed rgba(100,116,139,.18); }
.sch-grid-line-hour { border-top: 1px solid rgba(100,116,139,.34); }
.sch-off-block { position: absolute; left: 2px; right: 2px; border-radius: 4px;
    background: repeating-linear-gradient(45deg,
        rgba(100, 116, 139, .22) 0 6px, rgba(100, 116, 139, .08) 6px 12px);
    z-index: 1;
    /* ÍTEM 1257: el cursor decía `not-allowed` y ERA VERDAD mientras el clic solo
       soltaba un aviso. Ahora la banda ABRE su detalle (y desde ahí se desbloquea
       o se cambia el tiempo), así que `pointer` — un cursor que promete lo
       contrario de lo que hace el clic es la LEY 19 en una línea de CSS. */
    cursor: pointer; overflow: hidden; }
/* ÍTEM 1257 ① — EL MOTIVO, DENTRO DE LA BANDA. Una sola línea, recortada con
   puntos suspensivos: la agenda se mira de lejos y en el teléfono, y una columna
   de barbero mide 120-220 px (ver .sch-barber-col). Que se corte con elegancia
   es el requisito; que empuje la rejilla, jamás — de ahí el `nowrap` + `hidden`
   (el `title` del bloque sigue llevando el texto entero). */
.sch-off-text { display: block; padding: 1px 5px; font-size: .68rem; line-height: 1.15;
    font-weight: 600; color: var(--bs-secondary-color, #475569);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    pointer-events: none; }
/* ÍTEM 174: celda invisible de 15 min sobre el espacio VACÍO de la agenda —
   click/tap ofrece crear una cita ahí. Debajo del time-off (z1) y de las citas
   (z2): lo ocupado intercepta el click. El hover pinta un "+" sutil (desktop);
   en táctil no hay hover y el tap va directo a la confirmación. */
.sch-slot-hit { position: absolute; left: 0; right: 0; z-index: 0; cursor: pointer; }
.sch-slot-hit:hover { background: rgba(20, 34, 78, .07); }
.sch-slot-hit:hover::after { content: '+'; position: absolute; right: 6px; top: 50%;
    transform: translateY(-50%); font-weight: 700; font-size: .8rem;
    color: var(--primary-color, #14224E); opacity: .65; }
.sch-appt-block { position: absolute; left: 3px; right: 3px; border-radius: 6px;
    padding: 2px 5px; overflow: hidden; font-size: .74rem; z-index: 2;
    box-shadow: 0 1px 2px rgba(0,0,0,.08); }
/* ÍTEM 204 (kiosco BAR-01): fila [BOTÓN | nombre] —
   el botón de atender/terminar va PRIMERO (izquierda) y jamás se encoge ni se
   empuja fuera del bloque; el nombre absorbe el resto y trunca con "…". Antes
   el botón iba después del nombre y en multi-persona quedaba inalcanzable. */
.sch-appt-row { display: flex; align-items: center; gap: 4px; min-width: 0; }
.sch-appt-row .sch-btn { flex-shrink: 0; white-space: nowrap;
    /* compacto vertical: el botón (~16px) cabe ENTERO hasta en el bloque de un
       corte de 15 min (21px) — sin esto el borde inferior quedaba recortado */
    padding-top: 0; padding-bottom: 0; line-height: 1.25; }
.sch-appt-name { font-weight: 600; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; flex: 1 1 auto; min-width: 0; }
/* ÍTEM 164: nota del cliente en el bloque de la agenda — truncada (el tooltip
   lleva el texto completo); discreta pero visible */
.sch-appt-notes { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    font-size: .7rem; opacity: .85; }

/* == ÍTEM 211 (Vera, QA de viewport 2026-07-19): LISTA DE ESPERA RESPONSIVA ==
   La tabla de 9 columnas desbordaba a 1024x600 (BAR-01) y móvil → los botones
   Atender / No se presentó quedaban recortados al borde derecho, alcanzables
   solo con scroll horizontal de la tabla. Réplica del patrón 204: cada turno es
   una fila flex con las ACCIONES a la IZQUIERDA — flex-shrink:0, nunca se
   encogen ni se empujan fuera del viewport; el resto de los datos van en chips
   que hacen wrap y truncan con "…". Cero overflow-x en los 3 viewports. */
.wl-queue { display: flex; flex-direction: column; gap: .45rem; }
.wl-row { display: flex; align-items: center; gap: .6rem; min-width: 0;
    border: 1px solid var(--bs-border-color, #E2E8F0); border-radius: .6rem;
    padding: .5rem .6rem; }
.wl-actions { flex-shrink: 0; display: flex; flex-direction: column; gap: .3rem; }
.wl-actions .wl-btn { white-space: nowrap; font-size: .8rem; }
.wl-turn { flex-shrink: 0; font-weight: 700; min-width: 1.4rem; text-align: center;
    font-size: 1.1rem; color: var(--bs-secondary-color, #64748B); }
.wl-body { flex: 1 1 auto; min-width: 0; }
.wl-name { font-weight: 600; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; }
.wl-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .5rem;
    font-size: .78rem; color: var(--bs-secondary-color, #64748B); margin-top: .2rem; }
.wl-chip { white-space: nowrap; max-width: 100%; overflow: hidden;
    text-overflow: ellipsis; }
.wl-chip-svc { max-width: 15rem; }
/* ══ ÍTEM 619 (el dueño 2026-08-06) — LA FILA DE UNA CITA SE DISTINGUE SOLA ══════
   *"y los de cita identificados **para que el barbero no los pierda por
   equivocacion**"*. Tres señales, no una, porque la fila se mira de reojo con las
   manos ocupadas: filete azul a la izquierda, fondo apenas teñido, y —en la
   casilla donde el walk-in lleva su número de turno— LA HORA DE LA CITA.
   ⚠️ El azul es el mismo `--primary-900` de la casa: no se inventa un color nuevo
   para un estado nuevo. Y el fondo va al 4% para que la fila siga leyéndose igual
   de bien que las demás; teñir de más convierte una lista en un semáforo.
   📱 La casilla del turno crece a 3,4rem SOLO en la fila de cita ("12:30 PM" no
   cabe en 1,4rem) y `white-space:nowrap` impide que la hora se parta en dos
   líneas y descuadre la altura de la fila en un móvil de 390 px. */
.wl-row-appt { border-left: 4px solid var(--primary-900, #14224E);
    background: rgba(20,34,78,.04); }
.wl-turn-appt { min-width: 3.4rem; font-size: .82rem; white-space: nowrap;
    color: var(--primary-900, #14224E); }
/* ÍTEM 342: el turno YA COBRADO ensena su ESTADO con el
   importe, no un boton de cobrar. Verde de exito sobre fondo tenue —
   contraste MEDIDO con getComputedStyle (#0F5132 sobre #E8F5EE = 8.35:1,
   AAA — trampa 4: una captura no prueba un color). flex-shrink:0 +
   nowrap para que jamas se recorte en la columna de acciones (patron 204). */
.wl-paid-tag { display: inline-block; white-space: nowrap; flex-shrink: 0;
    font-size: .8rem; font-weight: 700; line-height: 1.2;
    padding: .3rem .5rem; border-radius: .4rem; text-align: center;
    color: #0F5132; background: #E8F5EE; border: 1px solid #A9D5BD; }

/* == MAPA DE CALOR DE CAPACIDAD (item 10) == */
.sch-cap-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
.sch-cap-dow { text-align: center; font-size: .7rem; font-weight: 600;
    color: var(--bs-secondary-color); padding: 2px 0; }
.sch-cap-cell { position: relative; border: 1px solid rgba(0,0,0,.08);
    border-radius: 6px; min-height: 42px; padding: 3px 4px; background: transparent;
    display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between;
    cursor: pointer; font-size: .74rem; line-height: 1.1; color: inherit; }
.sch-cap-cell:hover { outline: 2px solid rgba(13,110,253,.35); }
.sch-cap-empty { border: none; background: transparent; cursor: default; }
.sch-cap-empty:hover { outline: none; }
.sch-cap-daynum { font-weight: 600; }
.sch-cap-pct { font-size: .66rem; align-self: flex-end; opacity: .85; }
.sch-cap-selected { box-shadow: 0 0 0 2px #0d6efd inset; }
.sch-cap-none { background: rgba(148,163,184,.12); color: var(--bs-secondary-color); }
.sch-cap-green { background: rgba(22,163,74,.20); border-color: rgba(22,163,74,.4); }
.sch-cap-yellow { background: rgba(234,179,8,.28); border-color: rgba(234,179,8,.5); }
.sch-cap-red { background: rgba(220,38,38,.24); border-color: rgba(220,38,38,.5); }
.sch-cap-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px;
    vertical-align: -1px; margin-right: 3px; }

/* == Schedule compacto + header 2 lineas == */
.sch-table th, .sch-table td { padding: .15rem .3rem; font-size: .8rem; }
.sch-barber-th { white-space: normal; word-break: break-word; line-height: 1.15;
    min-width: 76px; max-width: 100px; vertical-align: top; padding: .25rem .2rem; }

/* == Reportes: altura limitada al viewport == */
.rep-chart { height: min(52vh, 340px); }
@media (max-width: 767px) { .rep-chart { height: 42vh; } }

/* == Iconos visibles en temas oscuros ==
   Los lord-icons traen trazos oscuros fijos; en temas oscuros se pierden.
   Cada tema oscuro define --icon-chip-bg (chip claro); los claros no. */
.crud-header-icon {
    background: var(--icon-chip-bg, transparent);
    border-radius: 14px;
    padding: 5px;
}

/* ══ 👆 LA CABECERA "TÍTULO + BOTÓN" ENVUELVE — LAS 17 QUE QUEDABAN ══════════
   Héctor, 2026-08-31: *"arregla lo del boton que se sale en ingles tambien"*.
   El lote anterior arregló Citas con `flex-wrap gap-2` EN EL MARKUP y dejó las
   demás medidas y sin tocar. Esto cierra esas.

   🩸 EL FALLO, MEDIDO (EN · 175% · 390×844, `right` contra `innerWidth`):
       /setup/products +209 · /setup/customers +69 · /setup/employees +69
       /setup/suppliers  ⚠️ · /setup/stores +11 · /admin/users +7
   Y no hay scroll que los rescate: `body` lleva `overflow-x: clip`, que RECORTA
   y NO scrollea ⇒ el botón queda **inalcanzable con el dedo** (enfocable con Tab,
   que es peor: existe y no se ve). Citas se rompía ya a 130%; las demás a 175%.

   🔑 POR QUÉ AQUÍ Y NO EN LOS 17 MARKUPS, y la razón es de método, no de gusto:
   `git commit -- <ruta>` protege de arrastrar otros FICHEROS, **no de arrastrar
   trabajo ajeno DENTRO del mismo fichero**. Con varios agentes vivos sobre este
   repo, tocar 17 `.razor` es 17 oportunidades de sellar lo de otro. Una regla en
   UN fichero que nadie más tiene abierto es cero.
   ⚖️ LA TENSIÓN, DICHA: el lote anterior argumentó —con razón— que la casa YA
   resuelve esto con las utilidades `flex-wrap gap-2` en el markup (Reports,
   BlocksPage, WaitingList, y ahora Citas), y que inventar otro mecanismo deja una
   forma más. **Sigue siendo cierto: para MARKUP NUEVO, la utilidad en el markup
   es lo preferido.** Esta regla es la red para las que ya existen, y cada una que
   migre a la utilidad la vuelve redundante — no la contradice.

   🚪 EL GRUPO INTERIOR NO ES OPCIONAL — MEDIDO: en `/setup/products` los dos
   botones ("Stock adjustment" + "+ Add New") viven en un `div.d-flex.gap-2` que
   mide **324 px dentro de una tarjeta de 306**. Envolviendo solo la fila de
   fuera, Products SEGUÍA saliéndose **+5 px**. Un arreglo que deja una pantalla
   fuera es una anécdota, no un arreglo.

   ✅ VERIFICADO 7/7 dentro de la ventana (EN · 175% · 390): appointments −71 ·
   users −146 · customers −128 · employees −128 · stores −128 · products −71 ·
   suppliers −128, y el título dentro en las 7.
   ✅ ESCRITORIO 1440 × 100%, la MISMA página con y sin la regla: IDÉNTICO
   (appointments x1381/right1399/alto38 · products 1049/1159/66 · users
   1279/1379/38). `flex-wrap` no hace nada mientras quepan.
   💰 EL PRECIO, para que nadie lo lea como regresión: en móvil INGLÉS a 100% la
   cabecera de Citas crece **+25 px** (52 → 77) porque el botón baja a su línea.
   Es el precio de no salirse nunca. En español a 100% no cambia nada, y a 175%
   la fila incluso ACORTA (146 vs 163: sin envolver, era el texto del botón el
   que se partía en tres líneas).

   ⚠️ LO QUE **NO** PROBÉ, y *"no lo vi" no es "no hay"*: las otras 11 cabeceras
   de la lista · el español fuera de Citas · 1024×600 (BAR-01) · y
   `/setup/servicetypes`, donde mi selector no encontró cabecera — eso es
   inconcluso, no limpio. */
.card-body > .d-flex.align-items-center.justify-content-between,
.card-body > .d-flex.align-items-center.justify-content-between > .d-flex {
    flex-wrap: wrap;
    gap: .5rem;
}

/* == La franja del poste de barbero en el tope (recuperada 2026-07-04) ==
   La app vieja la ponia en .navbar; el layout nuevo usa .app-header. *@ */
.app-header {
    border-bottom: 4px solid transparent;
    border-image: var(--barber-stripe) 1;
}

/* qc-band aplicado */

/* == Impresion de reportes paginados (patron PSP, 2026-07-04) ==
   Print/Save-as-PDF: se oculta todo menos .report-printable */
@media print {
    .sidebar, .app-header, .nav-scrollable, .a11y-trigger-btn, .a11y-panel,
    #sidebar-float-wrap, #blazor-error-ui, .report-no-print {
        display: none !important;
    }
    .page, main, .content, article {
        display: block !important; margin: 0 !important; padding: 0 !important; width: 100% !important;
    }
    .report-printable { position: absolute; left: 0; top: 0; width: 100%; }
    a[href]::after { content: none !important; }
    /* ══ ÍTEM 1138-bis (Vera, 2026-09-14, medido en la página viva): la MARCA
       DE AGUA del tema (`theme-theshop.css` `body::before { position:fixed;
       opacity:.06; background-image:url(...) }`) SE IMPRIME — alfa 14 en todo
       el papel del PDF de la carta y las hojas, y por el mismo mecanismo en
       los 6 reportes `.report-printable` (position:fixed se repite en cada
       página impresa). La hoja opaca del 1138 la tapaba; con la hoja
       transparente (1138-bis) queda a la vista: en papel blanco, la foto del
       hero al 6 % en toda la hoja; en sticker transparente, TINTA. Un
       `background: transparent` en html/body no alcanza al pseudo-elemento.
       Cura GLOBAL y única (no por tema, no por pieza): el papel nunca lleva la
       marca de agua. Contado antes de escribirla: de los 10 temas solo
       theme-theshop define body::before, y ni app.css ni ningún tema usa
       body::before/::after para algo que SÍ quiera imprimirse (grep con
       control: `body::before` en wwwroot/css/themes/*.css ⇒ 1/10, ::after 0). */
    body::before, body::after { display: none !important; }
}

/* Iconos fotorrealistas dentro de la cinta (lote 1, 2026-07-04) */
.qc-photo { height: 50px; width: auto; display: block; }

/* ══ ÍTEM 240 (spec Kahlo §1.3): encabezado personalizado de reportes ═══════
   Avatar circular 48px de la entidad protagonista + eyebrow + nombre 20px +
   título + período; borde inferior rojo sutil (la cinta del layout global es
   la firma — no se duplica). Claro/oscuro por tokens del tema. */
.report-header { padding-bottom: .6rem; border-bottom: 2px solid var(--accent-color, #C8102E); margin-bottom: .9rem; }
/* ÍTEM 280: protagonista del reporte (barbero/tienda/compañía) — sesgo arriba */
.report-avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover;
    object-position: 50% 15%; flex: none;
    border: 1px solid var(--bs-border-color, #E2E8F0); background: #fff; }
.report-avatar-mono { display: inline-flex; align-items: center; justify-content: center;
    background: #14224E; color: #fff; font-weight: 700; font-size: 1rem; border: none; }
.report-eyebrow { font-size: 11px; text-transform: uppercase; letter-spacing: .12em;
    color: var(--bs-secondary-color, #64748B); }
.report-entity { font-size: 20px; font-weight: 700; color: var(--bs-body-color, #14224E); line-height: 1.15; }
.report-title { font-size: 14px; color: var(--bs-body-color, #14224E); }
.report-period { font-size: 12.5px; color: var(--bs-secondary-color, #64748B); }
.report-logo { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex: none;
    border: 1px solid var(--bs-border-color, #E2E8F0); }
.min-width-0 { min-width: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   SERVICE TRACKER — VESTIDO DEL MOCKUP APROBADO (Kahlo 2026-07-20, ítems
   242/243): SEMÁFORO DEL SOPORTE (riel 5px + puntito con símbolo ▲/◐/✔ —
   regla daltónicos), pills de prioridad/status/canal, chips porcelana (activo
   navy; HOY activo ROJO), banda de filtros glass, grupos con borde de color y
   contador navy clickeable, densidad compacta, heatmap semáforo (hue = peor
   estado, intensidad = volumen). Colores de la casa: rojo #C8102E, ámbar
   #D97706 (texto AA #B45309), verde #16A34A.
   ═══════════════════════════════════════════════════════════════════════════ */
/* Banda de filtros glass sobria */
.st-filters { background: rgba(255, 255, 255, .72); border: 1px solid var(--bs-border-color, #D8D4C8);
    border-radius: 12px; padding: .55rem .65rem; }
.st-search { flex: 1 1 210px; max-width: 300px; display: flex; align-items: center; gap: .35rem;
    background: #fff; border: 1px solid var(--bs-border-color, #D8D4C8); border-radius: .45rem; padding: .3rem .55rem; }
.st-search input { border: none; outline: none; width: 100%; font: inherit; font-size: .82rem;
    color: #14224E; background: transparent; }
.st-search input::placeholder { color: rgba(20, 34, 78, .45); }
/* Chips: porcelana redonda; activo navy; HOY activo ROJO de la casa */
.st-chip { display: inline-flex; align-items: center; gap: .3rem; border-radius: 999px;
    border: 1px solid var(--bs-border-color, #D8D4C8); background: #fff; color: #14224E;
    padding: .26rem .65rem; font-size: .76rem; }
.st-chip:hover { border-color: #14224E; }
.st-chip.on { background: #14224E; border-color: #14224E; color: #fff; }
.st-chip.st-chip-today.on { background: #C8102E; border-color: #9E0C24; }
/* Sort chips: activo navy-900 + flecha */
.st-schip { display: inline-flex; align-items: center; gap: .25rem; border-radius: 999px;
    border: 1px solid var(--bs-border-color, #D8D4C8); background: #fff; color: #14224E;
    padding: .24rem .6rem; font-size: .74rem; }
.st-schip.on { background: #14224E; border-color: #14224E; color: #fff; }
/* Encabezado de grupo: porcelana + borde izq. de color + contador clickeable */
.st-ghead { width: 100%; display: flex; align-items: center; gap: .5rem; text-align: left;
    background: rgba(20, 34, 78, .06); border: 1px solid var(--bs-border-color, #D8D4C8);
    border-radius: 10px; padding: .4rem .6rem; margin: .55rem 0 .45rem;
    color: #14224E; font-weight: 700; }
.st-ghead-red { border-left: 4px solid #C8102E; }
.st-ghead-amber { border-left: 4px solid #D97706; }
.st-ghead-green { border-left: 4px solid #16A34A; }
.st-gcount { background: #14224E; color: #fff; border-radius: 999px;
    padding: .05rem .5rem; font-size: .74rem; font-weight: 700; cursor: pointer; }
.st-gcount:hover { filter: brightness(1.2); }
/* EL TICKET: card con RIEL del semáforo. QA-Vera (2): la card es BLANCA del
   mockup en TODOS los temas → texto navy EXPLÍCITO (en temas oscuros el color
   heredado era claro ≈1.2:1 — ilegible) */
.st-tcard { position: relative; background: #fff; color: #14224E;
    border: 1px solid var(--bs-border-color, #D8D4C8);
    border-left-width: 5px; border-radius: 10px; padding: .55rem .7rem .6rem; margin-bottom: .55rem;
    box-shadow: 0 2px 10px rgba(20, 34, 78, .05); }
/* Las utilidades BS dentro de la card también en la paleta clara del mockup
   (en temas oscuros sus vars dan colores claros → ilegibles sobre blanco) */
.st-tcard .text-muted { color: rgba(20, 34, 78, .62) !important; }
.st-tcard .text-success { color: #16A34A !important; }
.st-sem-red { border-left-color: #C8102E; }
.st-sem-amber { border-left-color: #D97706; }
.st-sem-green { border-left-color: #16A34A; }
.st-semdot { display: inline-flex; align-items: center; justify-content: center;
    width: 1.05rem; height: 1.05rem; border-radius: 50%; font-size: .6rem; font-weight: 800;
    color: #fff; vertical-align: -2px; margin-right: .3rem; }
.st-semdot-red { background: #C8102E; }
.st-semdot-amber { background: #D97706; }
.st-semdot-green { background: #16A34A; }
.st-tmain { flex: 1 1 380px; min-width: 0; }
.st-tnum { font-family: ui-monospace, Consolas, monospace; font-size: .72rem;
    color: rgba(20, 34, 78, .62); margin-right: .3rem; }
.st-tsubj { font-weight: 700; font-size: .92rem; line-height: 1.25; }
.st-tmeta { font-size: .74rem; color: rgba(20, 34, 78, .62); margin-top: .15rem;
    display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.st-tmeta b { color: #14224E; }
/* QA-Vera (1): la edad vieja SÍ pinta roja — antes .st-tmeta b (0,1,1) le
   ganaba a .st-age-old (0,1,0); este selector compuesto gana siempre */
.st-tmeta b.st-age-old, .st-age-old { color: #C8102E !important; font-weight: 700; }
/* Pills */
.st-pill { display: inline-flex; align-items: center; gap: .25rem; border-radius: 999px;
    padding: .1rem .5rem; font-size: .7rem; font-weight: 700; }
.st-pill-ch { background: rgba(20, 34, 78, .10); color: #14224E; border: 1px solid rgba(20, 34, 78, .22); }
.st-pill-st-red { background: rgba(200, 16, 46, .10); color: #C8102E; border: 1px solid rgba(200, 16, 46, .25); }
.st-pill-st-amber { background: rgba(201, 162, 39, .15); color: #B45309; border: 1px solid rgba(201, 162, 39, .4); }
.st-pill-st-green { background: rgba(22, 163, 74, .12); color: #16A34A; border: 1px solid rgba(22, 163, 74, .3); }
.st-pill-pri-4 { background: #C8102E; color: #fff; }                                                       /* Urgente */
.st-pill-pri-3 { background: rgba(227, 100, 20, .14); color: #E36414; border: 1px solid rgba(227, 100, 20, .35); } /* Alta */
.st-pill-pri-2 { background: rgba(201, 162, 39, .14); color: #B45309; border: 1px solid rgba(201, 162, 39, .35); } /* Media */
.st-pill-pri-1 { background: rgba(154, 163, 178, .18); color: #5A6472; border: 1px solid rgba(154, 163, 178, .4); } /* Baja */
/* Asignación 242g — 3 estados visuales */
.st-asg { display: inline-flex; align-items: center; gap: .3rem; border-radius: 999px;
    padding: .12rem .55rem; font-size: .72rem; font-weight: 600; }
.st-asg-mine { background: #14224E; color: #fff; }
.st-asg-other { background: rgba(20, 34, 78, .10); color: #14224E; border: 1px solid rgba(20, 34, 78, .18); }
.st-asg-none { background: #fff; color: var(--bs-secondary-color, #64748B); border: 1px dashed var(--bs-border-color, #D8D4C8); }
.st-age-old { color: #C8102E; font-weight: 700; }
/* Chip del acuse de Gabriel + notas navy */
.st-tack { display: inline-flex; align-items: center; gap: .3rem; margin-top: .3rem;
    font-size: .72rem; color: #16A34A; font-weight: 600; background: rgba(22, 163, 74, .08);
    border: 1px solid rgba(22, 163, 74, .25); border-radius: 999px; padding: .12rem .55rem; }
.st-tnote { border-left: 3px solid #14224E; padding-left: .5rem; margin-bottom: .3rem; }
.st-amber-txt { color: #B45309; font-weight: 600; }
/* Densidad B — lista compacta: se esconden cuerpo/notas/acuse/cierre/solución */
.st-compact-card { padding: .32rem .55rem; margin-bottom: .3rem; border-radius: 8px; }
.st-compact-card .st-body, .st-compact-card .st-notes, .st-compact-card .st-tack,
.st-compact-card .st-closed, .st-compact-card .st-sol { display: none; }
.st-compact-card .st-tsubj { font-size: .82rem; }
.st-compact-card .st-tmeta { margin-top: .05rem; }
/* HEATMAP SEMÁFORO 243 — hue por peor estado, alphas de la receta de Kahlo */
.st-cal-r1 { background: rgba(200, 16, 46, .14); border-color: rgba(200, 16, 46, .35); }
.st-cal-r2 { background: rgba(200, 16, 46, .26); border-color: rgba(200, 16, 46, .5); }
.st-cal-r3 { background: rgba(200, 16, 46, .40); border-color: rgba(200, 16, 46, .65); }
.st-cal-y1 { background: rgba(217, 119, 6, .16); border-color: rgba(217, 119, 6, .35); }
.st-cal-y2 { background: rgba(217, 119, 6, .28); border-color: rgba(217, 119, 6, .5); }
.st-cal-y3 { background: rgba(217, 119, 6, .42); border-color: rgba(217, 119, 6, .65); }
.st-cal-g1 { background: rgba(22, 163, 74, .14); border-color: rgba(22, 163, 74, .35); }
.st-cal-g2 { background: rgba(22, 163, 74, .26); border-color: rgba(22, 163, 74, .5); }
.st-cal-g3 { background: rgba(22, 163, 74, .40); border-color: rgba(22, 163, 74, .65); }
.st-cal-worst { position: absolute; right: 3px; bottom: 2px; font-size: .62rem; font-weight: 800; }
.st-w-r { color: #9E0C24; }
.st-w-y { color: #B45309; }
.st-w-g { color: #15803D; }
.st-cal-todaycell .sch-cap-daynum { color: #C8102E; font-weight: 800; }

/* == Kiosco tactil (Fase 2, 2026-07-04) == */
.kiosk-wrap { max-width: 640px; margin: 0 auto; }

/* ══ ÍTEM 238-v2 — RECETA APROBADA E-40 (verbatim
   "usar este al 40%, QR con color" = variante E "HighSetup" del mockup de
   Kahlo): foto NÍTIDA de la tienda + VELO GLOBAL PORCELANA AL 40% + WASH
   POR ELEMENTO — cada pieza (saludo, botones, QR, textboxes, keypad) vive
   en su superficie porcelana .90 (AA verificado POR ELEMENTO por Kahlo:
   navy 12:1 AAA, rojo 5.8:1, muted 5.7:1 — ningún texto directo sobre la
   foto). CAPA fixed inset:0 (lección del hotfix: el fondo JAMÁS participa
   del layout). 🚦 TODO detrás del flag PlatformSettings["KioskPhotoBackground"]
   (DEFAULT ON — aprobado; Value='false' = revert): las clases kiosk-wash/*
   solo se emiten con el flag ON, y las reglas de hermanos (.kiosk-photo-bg ~)
   solo aplican cuando la capa existe → flag OFF = kiosko actual EXACTO (el
   "original de revert" — se apaga sin republicar).
   ⚙️ SIN backdrop-filter A PROPÓSITO (decisión anotada): Kahlo lo marcó
   "adorno — si el RPi sufre, quitarlo; la legibilidad la carga el rgba .90
   solo" y el RPi del kiosko no tiene GPU (regla del 223). ══ */
.kiosk-photo-bg { position: fixed; inset: 0; z-index: -1; overflow: hidden; }
.kiosk-photo-bg img { width: 100%; height: 100%; object-fit: cover;
    object-position: 50% 50%; display: block; }
/* Punto focal LANDSCAPE (el RPi real es 1024x600 horizontal): sesgo hacia
   ARRIBA — la cabeza del barbero visible ("un barbero sin cabeza, como un
   zapatero sin pies"). Vertical/tablet: default 50% 50% intacto. */
@media (orientation: landscape) {
    .kiosk-photo-bg img { object-position: 50% 8%; }
}
/* VELO GLOBAL 40% — número calibrado EN VIVO con el control del
   mockup (diseño original 12% mínimo de seguridad; 40 es LA cifra) */
.kiosk-photo-bg::after { content: ""; position: absolute; inset: 0;
    background: rgba(247, 245, 240, .40); }
/* ══ E-40 v2 (decisión tras el hallazgo de Lupe +31px/+52px): el
   wash JAMÁS desplaza el layout — cada elemento interactivo queda EXACTO
   donde está con el flag OFF (el teclado en pantalla del RPi tapa lo que
   baje). Técnica: wrappers SIN padding/borde (los márgenes colapsan a
   través = altura idéntica) y el "colchón" visual es un HALO de BOX-SHADOW
   con spread (pinta fuera del elemento sin ocupar espacio): anillo 1 =
   porcelana .90 (el colchón), anillo 2 = borde de luz 1px, anillo 3 = la
   sombra de elevación. ══ */
.kiosk-wash { background: rgba(247, 245, 240, .90); border-radius: 10px;
    box-shadow: 0 0 0 12px rgba(247, 245, 240, .90),
                0 0 0 13px rgba(255, 255, 255, .65),
                0 12px 32px 8px rgba(20, 34, 78, .16); }
.kiosk-greet-card, .kiosk-qr-block, .kiosk-notify-block, .kiosk-confirm-card { border-radius: 10px; }
/* ÍTEM 249c: la caja de avisos/consent se montaba
   sobre el textbox del email (halo de 12/13px vs mb-2 de 8px). HACIA
   ADENTRO ~20% de caja visual: halo 12/13→4/5px; el div, sus checkboxes
   y los botones de abajo quedan EXACTOS (0px de delta). */
.kiosk-wash.kiosk-notify-block {
    box-shadow: 0 0 0 4px rgba(247, 245, 240, .90),
                0 0 0 5px rgba(255, 255, 255, .65),
                0 8px 24px 6px rgba(20, 34, 78, .14); }
/* Chip de título/display (phone y new): span INTERIOR sin padding vertical —
   el halo da el cuerpo; el h4/div exterior es EXACTO al del flag OFF */
.kiosk-chip-wash { background: rgba(247, 245, 240, .90); border-radius: 8px; padding: 0 .6rem;
    box-shadow: 0 0 0 8px rgba(247, 245, 240, .90),
                0 0 0 9px rgba(255, 255, 255, .65),
                0 6px 18px 4px rgba(20, 34, 78, .14); }
/* ÍTEM 249b: el display del teléfono se montaba sobre
   el título (halo de 9px vs mb-2 de 8px del h4). HACIA ADENTRO ~20%:
   fuente 1.6rem (80% del fs-2 — el strut del div fs-2 + min-height 2.6rem
   mandan la altura de línea → el keypad NO se mueve, 0px de delta) + halo
   reducido 8/9→5/6px. Solo existe con flag ON (OFF ni emite el span). */
.kiosk-display-wash { font-size: 1.6rem; vertical-align: -2px;
    box-shadow: 0 0 0 4px rgba(247, 245, 240, .90),
                0 0 0 5px rgba(255, 255, 255, .65),
                0 4px 14px 3px rgba(20, 34, 78, .14); }
/* Tarjeta del QR estilo WELCOME PACKAGE — ÍTEM 249a (en
   vivo en prod): los anillos eran box-shadow y pintaban 11px FUERA de la
   <img> → la tarjeta se montaba sobre "¿Prefieres usar tu teléfono?" y
   "Escanea para registrarte". HACIA ADENTRO: la caja de la <img> queda
   EXACTA (110x110 inline, 0px de delta) y el anillo vive DENTRO — padding
   8px (ring blanco) + border 3px navy (border-box de Bootstrap) → el QR
   pinta a 88x88 (~20% más chico, nivel H lo aguanta) y la tarjeta ya no
   toca los textos. */
.kiosk-qr-ring { background: #fff; border-radius: 8px;
    padding: 8px; border: 3px solid #14224E; }

/* ══════════════════════════════════════════════════════════════════════════
   ÍTEM 294d — HOME DEL KIOSKO: 2 QR SEPARADOS, cada uno
   en SU PROPIA tarjeta (verbatim: "los puede separar en 2 cards separados, no
   tienen que estar los 2 en 1 solo card!"). Calcado del mockup APROBADO
   MBS\kahlo\mockup-kiosko-2qr.html (v2 corregido + ajuste de gap 44px +
   ajuste #2 de 2 tarjetas separadas) — 1 SOLA columna general (el saludo y
   los 2 botones táctiles arriba NO se tocan), el bloque del QR gana ANCHO,
   no ALTO (regla 238: 1024×600 fijo, cero scroll). Tarjeta A (Reservar cita,
   QR sin cambios) = acento navy/rojo de siempre; tarjeta B (Unirme a la fila,
   NUEVO — ítem 294a) = acento caramelo/dorado para diferenciarlas a simple
   vista. Colores fijos (no theme-driven) — el kiosko es un dispositivo
   público de marca fija, igual que el resto de .kiosk-*. ══════════════════ */
/* ÍTEM 313d (bug REAL probando con un teléfono): con 44px de gap la
   cámara encuadraba los DOS QR y agarraba el PRIMERO cuando él apuntaba al
   SEGUNDO — el cliente terminaba en el flujo equivocado. NO es cosmético.
   190px ≈ 2 pulgadas a 96dpi. Total 172+190+172 = 534px
   de 1024 — el ancho sobra; la restricción del 238 es el ALTO. */
.qr-2cols { display: flex; gap: 190px; align-items: flex-start; justify-content: center; }
/* R-406: con UN solo QR (citas o walk-in apagado) la separacion de 190px del
   item 313d ya no hace falta -- existia para que la camara no confundiera dos
   codigos vecinos. Con uno solo, el hueco solo desplazaria el QR del centro. */
.qr-2cols.qr-1col { gap: 0; }
.qr-col {
    width: 172px; padding: 14px 10px 12px; display: flex; flex-direction: column;
    align-items: center; position: relative;
    background: rgba(247, 245, 240, .92); border-radius: 14px;
    box-shadow: 0 10px 26px 2px rgba(20, 34, 78, .16), 0 0 0 1px rgba(255, 255, 255, .65);
}
/* ÍTEM 305a (verbatim): "el card de unirme a la fila
   ahora ponerlo con los mismos colores que tenemos en el card de reservar
   una cita" — la tarjeta B (Unirme a la fila) tenía un acento
   caramelo/dorado propio (294a); AHORA usa EXACTAMENTE la misma paleta
   navy/rojo que la tarjeta A (Reservar cita) — .qr-col-b/.qr-col-label-b/
   .qr-holder-b quedan como ALIAS 1:1 de -a, ya no acentos distintos.
   Tamaño/altura: ya eran idénticos entre A y B (mismo .qr-col de 172px,
   mismo .qr-holder de 112x112 + padding 6px + borde 3px) — se deja igual,
   solo se confirma que ningún override de -b cambie dimensiones. */
.qr-col-a, .qr-col-b { border: 2px solid #C8102E; }   /* rojo/navy — paleta única de las 2 tarjetas */
/* ÍTEM 313c (verbatim: "visualmente ahora no se ven
   parejos"): el título de un card caía en 2 líneas y el del otro en 3, así que
   los QR quedaban a distinta altura y los cards de distinto tamaño — en inglés
   Y en español. El copy se acortó y emparejó, pero el BUG DE FONDO es que la
   altura dependía del largo del texto: por eso el bloque del título ahora tiene
   ALTURA FIJA de 2 líneas (2 × line-height 1.2). Defensa permanente: ninguna
   traducción futura ni cambio de copy puede volver a descuadrar los QR. */
.qr-col-label { font-size: .8rem; font-weight: 700; line-height: 1.2; margin-bottom: .32rem;
    height: 2.4em; overflow: hidden;
    display: flex; align-items: center; justify-content: center; gap: .3rem; }
.qr-label-icon { display: inline-block; width: 1.25rem; min-width: 1.25rem; text-align: center; }
.qr-col-label-a, .qr-col-label-b { color: #14224E; }
/* ÍTEM 313b (verbatim: "toda la aplicación es nueva no
   necesitamos eso en ese card"): el listón dorado "NUEVO"/"NEW" del QR-B se
   ELIMINA — markup, wrapper y CSS. Se borra de verdad, no se deja comentado.
   ÍTEM 313a: los captions de los 2 cards (.qr-col-caption) también fuera —
   sobraban y sumaban altura, que es justo lo que escasea en 1024×600. */
.qr-holder { position: relative; display: inline-block; background: #fff; padding: 6px; border-radius: 11px; }
.qr-holder-a, .qr-holder-b { border: 3px solid #14224E; }

/* Los interactivos SIEMPRE por encima de los halos vecinos (el halo puede
   solapar el fondo, jamás tapar un control) */
.kiosk-photo-bg ~ .kiosk-wrap .form-control,
.kiosk-photo-bg ~ .kiosk-wrap .btn,
.kiosk-photo-bg ~ .kiosk-wrap .kiosk-key { position: relative; z-index: 1; }
/* Botones outline, teclas del keypad y botón de idioma → porcelana .90
   SOLO con la capa presente (flag ON); hover/active vuelven al tema */
.kiosk-photo-bg ~ .kiosk-wrap .kiosk-btn.btn-outline-primary:not(:hover):not(:active),
.kiosk-photo-bg ~ .kiosk-wrap .btn-outline-secondary:not(:hover):not(:active),
.kiosk-photo-bg ~ .kiosk-wrap .kiosk-key:not(:hover):not(:active),
.kiosk-photo-bg ~ .kiosk-lang:not(:hover):not(:active) {
    background: rgba(247, 245, 240, .90);
}
/* ÍTEM 315 · fila 10 (Kahlo — no reportado antes, es el mismo error en
   la misma pantalla): el botón de idioma llevaba el borde rojo de
   --accent-color, elegido por estética y no por contraste → límite 2.23:1. Se
   cambia SOLO el color del borde al navy del par → 3.54:1. Cero layout (el
   .btn de Bootstrap ya tenía 1px de borde). */
.kiosk-photo-bg ~ .kiosk-lang:not(:hover):not(:active) {
    border-color: var(--ovc-ink);
}
/* El primario mantiene su gradiente rojo (opaco = legible sobre foto);
   sombra de asiento extra para separarlo del fondo vivo.
   ÍTEM 315 · fila 12: el rojo sólido con solo un drop-shadow difuso medía
   1.40:1 de límite (rojo+blanco deja descubierto L 0.30–0.478). Gana el par —
   borde navy + halo porcelana de 2px — CONSERVANDO su asiento rojo → 3.54:1. */
.kiosk-photo-bg ~ .kiosk-wrap .kiosk-btn.btn-primary {
    border-color: var(--ovc-ink);
    box-shadow: 0 0 0 2px var(--ovc-halo),
                0 6px 18px rgba(20, 34, 78, .25), 0 0 12px rgba(200, 16, 46, .20);
}
/* TEXTBOX HighSetup ("la esencia", specs de Kahlo): porcelana .90 + borde
   de luz + sombra suave; focus = blanco .96 + borde navy; placeholder navy
   al 50% (no el gris Bootstrap).
   ÍTEM 315 · fila 11 (Kahlo — tampoco reportado antes): el "borde de luz"
   blanco .75 sobre un relleno porcelana medía 1.04:1 — un textbox SIN límite
   visible. Es el mismo error de las flechas. Pasa al navy del par → 3.54:1;
   el relleno .90, la sombra y el focus quedan EXACTOS (el focus ya era #14224E,
   así que ahora el foco se lee por el fondo blanco .96, no por un salto de
   color de borde). Cero layout: seguía siendo 1px. */
.kiosk-photo-bg ~ .kiosk-wrap .form-control {
    background: rgba(247, 245, 240, .90);
    border: 1px solid var(--ovc-ink);
    box-shadow: 0 4px 16px rgba(20, 34, 78, .14);
}
.kiosk-photo-bg ~ .kiosk-wrap .form-control:focus {
    background: rgba(255, 255, 255, .96);
    border-color: #14224E;
}
.kiosk-photo-bg ~ .kiosk-wrap .form-control::placeholder { color: rgba(20, 34, 78, .50); }

/* ÍTEM 238 bonus: icono realista del kiosco en el picker de tiendas */
.kiosk-picker-icon { height: 2.4em; width: auto; vertical-align: middle; }
/* touch-action (hotfix 7/20): un toque = un click — que el gesto no se
   convierta en scroll/zoom y se robe el tap en la pantalla táctil del kiosko */
.kiosk-btn { min-width: 320px; min-height: 64px; font-size: 1.3rem; touch-action: manipulation; }
/* ══ ÍTEM 313 e/e-bis (foto real del BAR-01) — el
   formulario de cliente nuevo quedaba CORTADO por el teclado en pantalla y el
   botón de Check In era INALCANZABLE: el cliente no podía completar el
   registro. Bloqueo funcional, no cosmético.
   NÚMEROS REALES (MBS\kiosk\BAR-01-RPI-KIOSK-RECETA.md): pantalla 1024×600,
   teclado en 31,384 de 962×204 (y=384 → y=588), Onboard SIN docking = ventana
   X11 flotante que el navegador NO VE (por eso `visualViewport` no aplica).
   --kiosk-kb-top ES LA ÚNICA PALANCA: si mañana cambia la geometría del
   teclado, se toca aquí y nada más. ══ */
:root { --kiosk-kb-top: 384px; }

/* (1) APRETAR: el form entero tiene que caber en la franja de arriba. Se baja
   de form-control-lg y se comprimen márgenes SOLO en este paso (el resto del
   kiosko no se toca). */
/* ÍTEM 313(g): el relleno INTERNO baja (.3→.25rem, 36→34px) y ese aire se le
   pasa al hueco ENTRE campos, más abajo. 34px sigue siendo blanco táctil cómodo
   para el kiosko ([[feedback_ui_simplicity_non_tech_customers]]).
   ══ ÍTEM 463 · 🟢 LV-09 — SE REVIERTE A PROPÓSITO ESTE APRETÓN: 34 → 44px ══
   Los 34px fueron el PEAJE que el 313 pagó para que las 4 filas cupieran sobre
   el teclado (y=384). Con la fila del Apellido y la del Correo fundidas en las
   otras dos, ese peaje ya no hace falta y el alto liberado SE LE DEVUELVE AL
   DEDO: 44px es el mínimo táctil de la casa. NO ES COSMÉTICA — es que el cliente
   acierte el campo a la primera. Un campo elegante que no se acierta es peor que
   uno feo, y aquí no hay que elegir (el presupuesto de alto ahora lo permite:
   el bloque baja de 339 → 286px sobre una franja útil de 352px). */
.kiosk-new-step .form-control-lg { font-size: 1.05rem; padding: .5rem .7rem; min-height: 44px; }
.kiosk-new-step .btn-lg { font-size: 1rem; padding: .55rem 1.4rem; min-height: 44px; }
.kiosk-new-step .mb-2 { margin-bottom: .28rem !important; }
.kiosk-new-step .mb-3 { margin-bottom: .3rem !important; }
.kiosk-new-step .mb-1 { margin-bottom: .15rem !important; }
/* ÍTEM 463 — MEDIDO EN EL DOM REAL, NO EN LA RÉPLICA: el estado de error pinta
   el aviso DOS VECES (ítem 176c: uno arriba del form, porque con el teclado
   abierto el de abajo queda tapado). Cada aviso son 34px + el `margin-bottom`
   de 1rem que trae Bootstrap = 100px de los 79px de holgura que deja el estado
   limpio ⇒ el formulario seguía desplazándose 28px justo en el momento en que
   el cliente está confundido. Comprimir SOLO el margen (nunca el tamaño de
   letra del aviso: en un kiosko el error tiene que leerse) baja el desplazamiento
   de 28 → ~7px. NO llega a 0 porque los dos avisos son irreductibles sin tocar
   producto. Es del mismo tipo de compresión que el 313 y vive en el mismo ámbito. */
.kiosk-new-step .alert { margin-bottom: .35rem; }
/* ══ 🟡 ÍTEM 615-b — UN MISMO ERROR, UN SOLO SITIO (fuera del BAR-01) ═════════
   Lupe midió DOS avisos idénticos en el DOM: uno en y=64.4 (arriba del
   formulario) y otro en y=453.9 (pegado al consentimiento SMS, descuadrado).
   🔑 EL DUPLICADO NO FUE UN DESCUIDO — es el ítem 176c, y su razón está escrita
   12 líneas más arriba: *"con el teclado abierto el de abajo queda tapado"*.
   Esa razón SIGUE SIENDO CIERTA en el BAR-01: el teclado X11 flotante tapa todo
   lo que pase de y=384 y el navegador no puede saberlo. Y 🟢 LV-09 CONGELA esa
   pantalla: quitar el aviso de arriba a 1024×600 movería la botonera 40px.
   ⇒ SE QUITA SOLO DONDE LA RAZÓN YA NO EXISTE: donde no hay teclado invisible
   (la misma compuerta del 615-a), el aviso de abajo YA no queda tapado, así que
   el que sobra es él. Se esconde EL DE ABAJO y no el de arriba a propósito: en
   apilado (celular) el de arriba queda pegado a los campos que se pintan en
   rojo, que es donde el aviso *"corrige los campos marcados en rojo"* sirve; el
   de abajo cae detrás del consentimiento, lejos de lo que hay que corregir.
   📌 EN EL BAR-01 SIGUEN SALIENDO LOS DOS. Es deliberado y es decisión abierta
   del dueño (ítem 176c) — no se pisa de paso.
   🔴 A1 (Lupe, vuelta 2): esta media query lleva el MISMO `min-width: 900px` que
   la del `max-height`, y por la misma razón — sin él, en un iPhone SE (375×667) o
   en cualquier iPhone con las barras del navegador (390×629) volvían a salir LOS
   DOS avisos. Medido: `avisosVisibles: 2` a 390×629. Las dos compuertas son LA
   MISMA condición y tienen que decir siempre lo mismo. ══════════════════════ */
.kiosk-new-step .kiosk-col-prefs > .alert { display: none; }
@media (min-width: 900px) and (min-height: 444px) and (max-height: 684px) {
    .kiosk-new-step .kiosk-col-prefs > .alert { display: block; }
}
.kiosk-new-step .mt-2 { margin-top: .3rem !important; }
.kiosk-new-step h4 { font-size: 1.1rem; }
/* el consentimiento SMS (ítem 154) es texto legal largo — se queda ENTERO
   (Twilio 30513 lo exige) pero comprimido; en español ocupa más, por eso se
   aprieta el interlineado y no solo el tamaño. */
.kiosk-new-step .text-muted.small { font-size: .68rem; line-height: 1.22; }
/* ÍTEM 463: las casillas de aviso también recuperan área táctil (18px de caja
   + 4px de respiro arriba/abajo). Son parte del presupuesto medido del 463
   (22.8 → 34.4px); sale de los 66px de holgura que compra el cambio. */
.kiosk-new-step .form-check-label { font-size: .9rem; padding: 4px 0; }
.kiosk-new-step .form-check-input { width: 18px; height: 18px; }
/* (e) el segundo nombre es una COLUMNA FIJA de la fila del Nombre — alineada
   con los demás campos (antes: max-width suelto, se veía corrido y roto).
   ⚠️ ESTA REGLA SIGUE VIVA Y NO SE TOCA: la usa MobileCheckin.razor:190-193,
   que NO entra en el ítem 463 (el 463 es SOLO el kiosco). En el kiosco la fila
   pasó a grid y el ancho lo pone la columna, no esta regla. */
.kiosk-name-row .kiosk-mi { flex: 0 0 104px; width: 104px; text-align: center; }

/* ══════════════════════════════════════════════════════════════════════════
   ÍTEM 463 · 🟢 LV-09 — EL FORMULARIO DE CLIENTE NUEVO EN
   2 FILAS. Diagnóstico de Kahlo: "el formulario no está mal diseñado: está mal
   PRESUPUESTADO". Gastaba 4 líneas de ALTO —el recurso escaso cuando el teclado
   del BAR-01 tapa todo lo que pase de y=384— dentro de un contenedor de 420px
   en una pantalla de 1024: 600px de ANCHO desperdiciados. Se cambia alto por
   ancho. Mockup congelado: MBS\kahlo\mockup-463-form-kiosco.html
   GRID Y NO FLEX: cada fila necesita proporciones DISTINTAS (1fr/80px/1.1fr vs
   240px/1fr) y con flex-basis eso se pelea con el contenido de cada input.
   TODO va bajo el ámbito .kiosk-new-step — el resto del kiosko no se toca. ══ */
.kiosk-new-step .kiosk-form-box { max-width: 720px; }
.kiosk-new-step .kiosk-form-row-a,
.kiosk-new-step .kiosk-form-row-b { display: grid; gap: 12px; }
/* 🟢 498c: el reparto de esta fila viaja por VARIABLE (igual que el de la fila
   Teléfono·Correo, ítem 323 — un solo canal). Sin definir = EXACTAMENTE lo de
   siempre (`1fr 80px 1.1fr`); en 2 columnas y solo con `field-sizing` soportado,
   la columna la redefine a `1fr auto 1.1fr` para que la Inicial se encoja a su
   único carácter. Se hace AQUÍ y no con una regla nueva más específica a
   propósito: así las reglas de apilado de abajo —que son (0,2,0) y vienen
   DESPUÉS— siguen ganando y el apilado angosto no se entera de nada. */
.kiosk-new-step .kiosk-form-row-a { grid-template-columns: var(--bsp-kiosk-row-a, 1fr 80px 1.1fr); margin-bottom: 12px; }
.kiosk-new-step .kiosk-form-row-b { grid-template-columns: 240px 1fr; margin-bottom: 14px; }
/* la Inicial ya no fija ancho propio: la columna de 80px manda (sigue siendo
   casi el DOBLE del mínimo táctil, y ≥44px en las dos dimensiones). */
.kiosk-new-step .kiosk-form-row-a .kiosk-mi { width: 100%; text-align: center; }
/* ══════════════════════════════════════════════════════════════════════════
   LOTE C · PROMESA ROTA (Lupe, vuelta 29) — "CORRIGE LOS CAMPOS MARCADOS EN
   ROJO" Y NO HABÍA NI UNO MARCADO. Lupe midió los 5 bordes del formulario en
   estado de error: los cinco iguales al estado limpio. El aviso mandaba al
   cliente a buscar algo que no existía — y aquí NO HAY NADIE que le ayude
   (es la tablet, solo, de pie). Misma familia del botón mudo (452) y del salto
   a la pestaña (459), pero en la pantalla DEL CLIENTE.
   SE REUSA EL MECANISMO DE LA CASA: `is-invalid` de Bootstrap, el mismo que ya
   usan CompanyEdit/Profile/Services/StoreEdit vía su helper Inv(). Cero
   invenciones.
   🔴 POR QUÉ ESTA REGLA EXISTE — LO CONGELADO NO SE PUEDE MOVER (🟢 LV-09):
   el formulario está aprobado con 2 filas, campos de 44px y el botón CLAVADO
   en y=334. Marcar en rojo NO PUEDE COSTAR UN SOLO PÍXEL. El `is-invalid` de
   Bootstrap, tal cual viene, SÍ cuesta: mete `padding-right: calc(1.5em+.75rem)`
   y un icono de fondo — en la Inicial (columna de 80px, texto centrado) el
   icono se come el campo entero. Por eso aquí:
   · `background-image:none` + `padding-right` explícito → el texto no se mueve;
   · el grosor de la marca lo pone un `outline` con `outline-offset` NEGATIVO
     (se dibuja DENTRO de la caja): el outline no entra en el flujo ni en
     getBoundingClientRect ⇒ ALTO, ANCHO y POSICIÓN idénticos al estado limpio.
     El borde sigue siendo de 1px: lo que cambia es su COLOR.
   ⚠️ OUTLINE Y NO `box-shadow: inset`: los campos del kiosco YA llevan un
   box-shadow suave del tema (medido en vivo: rgba(20,34,78,.14) 0 4px 16px) y
   un box-shadow nuevo lo SUSTITUIRÍA — el campo con error perdería la sombra y
   se vería "hundido" respecto a los otros cuatro. El outline es una propiedad
   aparte: se suma, no reemplaza.
   El resultado es un anillo rojo de 3px visible de pie a un brazo de distancia
   (que es como se usa un kiosko), sin tocar la geometría de LV-09. ══════════ */
.kiosk-new-step .form-control.is-invalid,
.kiosk-new-step .form-control.is-invalid:focus {
    background-image: none;
    padding-right: .7rem;
    border-color: #dc3545;
    outline: 3px solid #dc3545;
    outline-offset: -3px;
}

/* (b) PANTALLA ANGOSTA (<560px) — se apila SOLO, que es EXACTAMENTE el layout
   que ya existía antes del 463 ⇒ riesgo de regresión en angosto = cero. La
   mejora solo se activa cuando hay ancho que gastar. Un punto de quiebre, sin JS. */
@media (max-width: 559.98px) {
    .kiosk-new-step .kiosk-form-row-a { grid-template-columns: 1fr 80px; gap: 10px; margin-bottom: 10px; }
    .kiosk-new-step .kiosk-form-row-a .kiosk-last { grid-column: 1 / -1; }
    .kiosk-new-step .kiosk-form-row-b { grid-template-columns: 1fr; gap: 10px; }
}

/* ══ ÍTEM 313(g) (sobre una foto del kiosko YA con el 313
   aplicado): "de nuevo veo los campos muy pegados, overlaping" / "si, muy
   apretados". NO se están montando — están APRETADOS: las cajas quedaron ALTAS
   (36px) con solo 4.48px de aire entre ellas (ratio 8:1). Es el precio que se
   pagó en el 313(e-bis) por meter todo el formulario sobre y=384.
   DE DÓNDE SALE EL AIRE — se vio en una captura real: "hay un espacio
   arriba que quizás se pueda usar la mitad de ese espacio". Medido: el
   formulario arrancaba en y=56.8 con la banda 0–52px VACÍA en esta pantalla (el
   padding-top:52px de .kiosk-root es del fix Vera/238, pero ese margen lo
   necesitan las pantallas kbs-*, NO esta; y el botón de idioma vive en x=891-1010,
   o sea ni se cruza con el formulario, que va de x=192 a 832).
   ⚠️ POR ESO EL RECLAMO VA CON `:first-child` Y NO TOCANDO .kiosk-root: cuando
   hay BANNER DE CIERRE DE TIENDA, el <div class="alert"> es el primer hijo de
   .kiosk-wrap, .kiosk-new-step deja de ser :first-child y el margen negativo NO
   aplica — el banner conserva su aire y nada se monta encima. Los dos estados
   verificados a 1024×600 en EN y ES.
   Se recupera la MITAD (26 de 52px) y se reparte: el aire va ENTRE los campos,
   no DENTRO. Los campos bajan 36→34px (siguen siendo blanco táctil cómodo) y
   los huecos suben 4.48→9.92px → ratio 3.4:1 en vez de 8:1. ══ */
.kiosk-new-step:first-child { margin-top: -26px; }
/* ⚠️ ÍTEM 463: tras pasar el formulario a 2 filas de grid, ningún hijo directo
   de .mx-auto lleva ya `mb-2` — el hueco entre filas lo ponen ahora
   .kiosk-form-row-a/-b (12 y 14px). La regla se CONSERVA porque sigue siendo la
   que define el aire del 313(g) si alguna fila vuelve a este patrón, y quitarla
   sería tocar el 313 "de paso". */
.kiosk-new-step > .kiosk-new-scroll > .mx-auto > .mb-2 { margin-bottom: .62rem !important; }
/* 🚫 CERO OVERLAPPING (regla permanente, Frente 14) — MEDIDO con
   los rects INFLADOS por el spread del halo, porque `box-shadow` NO entra en
   getBoundingClientRect y a ojo (o con el rect crudo) esto NO se ve:
   · el título lleva .kiosk-chip-wash, cuyo anillo es de 8px+9px, y su <span>
     sobresale ~2px del <h4>: con el `mb-2` comprimido del 313 se montaba 5.42px
     sobre la primera fila de campos. Se le da margen REAL (.78rem = 12.5px) en
     vez de recortarle el halo — aquí sí sobra presupuesto (a diferencia de las
     pantallas kbs-*, donde hubo que achicar el anillo).
   · el bloque porcelana de avisos tiene anillo de 4px+5px y quedaba a 4.8px de
     los botones = 0.20px de solape. El `mb-3` del consentimiento (que colapsa
     hacia fuera del bloque) sube a .45rem = 7.2px.
   Coste total: ~8px de los 27px de holgura que dejó el reclamo de arriba.
   OJO con la especificidad: el `.kiosk-new-step .mb-2` de más arriba es (0,2,0)
   CON !important, así que un `> h4` a secas (0,1,1) PIERDE — hay que calificarlo
   con la clase para ganarle. */
.kiosk-new-step > .kiosk-new-scroll > h4.mb-2 { margin-bottom: .78rem !important; }
.kiosk-new-step .mb-3 { margin-bottom: .45rem !important; }

/* (2) RED DE SEGURIDAD: como el navegador no puede detectar el teclado, se le
   DICE cuál es la altura útil. El bloque desplaza POR DENTRO y bsp-kiosk.js
   hace scrollIntoView del campo enfocado. OJO ítem 238 ("cero scroll"): esa
   regla es para la PÁGINA — esto es un área contenida que solo se mueve
   cuando el teclado está encima, que es exactamente lo que hace falta.
   🐛 ÍTEM 313(g) — ESTE COMENTARIO ESTABA ROTO Y SE COMÍA LA REGLA. El bloque
   se CERRABA justo aquí arriba y seguía escribiendo prosa hasta un SEGUNDO
   cierre: para el parser CSS esa prosa pasaba a formar parte del selector
   siguiente, así que la regla `.kiosk-new-step { max-height; overflow-y }`
   NUNCA SE APLICÓ (verificado en vivo: no aparecía en `document.styleSheets` y
   el elemento computaba `overflow-y: visible`). O sea la RED DE SEGURIDAD del
   ítem 313 estaba DESCONECTADA desde que se escribió, y solo funcionaba el
   `max-height` inline que pone `bspKiosk.kbFit()`. Se cierra el comentario UNA
   sola vez y la regla vuelve a existir.
   El `max-height` de aquí es solo el PISO de seguridad por si el JS no corre:
   la altura EXACTA la calcula `bspKiosk.kbFit()`, que resta el offset real del
   bloque (el saludo/banner de cierre de arriba cambia de alto), cosa que el
   CSS no puede hacer solo — `calc(384px - ...)` mide desde el propio elemento,
   no desde el borde de la pantalla, y ahí fue donde falló el primer intento
   (el bloque terminaba en y=430, con el botón debajo del teclado). */
.kiosk-new-step {
    overscroll-behavior: contain;
    display: flex; flex-direction: column; overflow: hidden;
}
/* ══ 🔴 ÍTEM 615-a (Lupe 2026-08-06) — LA RED DE SEGURIDAD TAMBIÉN LLEVA
   COMPUERTA. Sin ella este `max-height` (324px) recortaba el formulario en un
   CELULAR de 844px de alto, donde no hay ningún teclado en y=384: los botones
   `Registrarme`/`Volver` caían FUERA del recorte y, con la página sin scroll
   (ítem 238), el cliente no podía terminar de registrarse. MEDIDO a 390×844:
   bloque 26→378 con `height:352px` inline y la botonera en y=400.3 — invisible.
   EL CRITERIO ES EL MISMO QUE EL DE `bspKiosk.kbApplies()` y por la misma razón:
   la constante solo describe un teclado si la BANDA que queda debajo PARECE un
   teclado (banda = alto de pantalla − --kiosk-kb-top, entre 60 y 300px) **Y si
   la pantalla es ANCHA** (≥900px).
     · BAR-01 1024×600 → banda 216 ✅ ancho 1024 ✅
     · celular 390×844 → banda 460 ❌     · celular acostado 844×390 → banda 6 ❌
     · 🔴 A1: celular 390×629 (Safari con barras) → banda 245 ✅ pero ancho 390 ❌
   ══ 🔴 A1 (Lupe, vuelta 2 · 2026-08-07) — EL `min-width` NO ES ADORNO ══════════
   Sin él esta media query MORDÍA en teléfonos reales (iPhone SE 375×667, y
   cualquier iPhone con las barras del navegador puestas): el `max-height` de
   324px recortaba el formulario y `Registrarme` volvía a quedar fuera. **El
   BAR-01 no es solo BAJO: es ANCHO-Y-BAJO, y el ancho es lo que lo separa de un
   teléfono.** Razonamiento completo del 900 en `bsp-kiosk.js → kbApplies()`.
   ⚠️ LOS TRES NÚMEROS VAN A MANO PORQUE UNA MEDIA QUERY NO PUEDE LEER UNA CUSTOM
   PROPERTY — no es un olvido. Son `--kiosk-kb-top` + 60 y + 300 (384+60=444,
   384+300=684) y el umbral de ancho de `kbApplies()`. Si se mueve la constante,
   ESTAS LÍNEAS se mueven con ella (el JS se entera solo, que lee la variable).
   ══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 900px) and (min-height: 444px) and (max-height: 684px) {
    .kiosk-new-step { max-height: calc(var(--kiosk-kb-top) - 60px); }
}

/* ══ FIX QA LUPE (bloqueante 2, 2026-07-25) — LA BOTONERA NO SE MUEVE ═══════
   Antes desplazaba el bloque ENTERO, así que en cuanto el contenido crecía
   (banner de cierre de tienda y/o alerta de validación) el botón de Check In
   se iba con el scroll: medido a 1024×600 con `scrollTop=0` → y=474 con banner,
   y=431 con error de validación y y=574 en el peor caso (ES + banner + error),
   o sea POR DEBAJO del borde de la pantalla de 600px. Y NADA lo traía de vuelta:
   el `scrollIntoView` del focusin no dispara porque cada campo, por separado,
   ya cabe en la franja visible. La única pista era una barra de scroll `thin`
   de 10px… en una pantalla TÁCTIL.
   AHORA: `.kiosk-new-step` es un flex vertical con `overflow:hidden`; solo el
   CONTENIDO desplaza (`.kiosk-new-scroll`, que sigue heredando el max-height
   que le calcula `bspKiosk.kbFit()` a su padre) y la botonera va `flex:0 0 auto`
   pegada al fondo del bloque — que es exactamente la línea del teclado.
   ⚠️ EL ESTADO LIMPIO NO SE NEGOCIA: sin banner ni error el contenido cabe, el
   bloque encoge a su contenido y la botonera queda donde siempre (y=367, sobre
   y=384) en EN y ES. Pinchar no la sube ni la baja cuando no hace falta.
   OJO: al meter este envoltorio, los selectores `>` de arriba (`h4.mb-2` y
   `.mx-auto > .mb-2`) tuvieron que pasar por `.kiosk-new-scroll` — si no, el
   aire del 313(g) se pierde EN SILENCIO. ══ */
.kiosk-new-scroll {
    flex: 1 1 auto; min-height: 0;
    overflow-y: auto; overscroll-behavior: contain;
    scrollbar-width: thin;
}
/* PADDING 0 A PROPÓSITO — PARIDAD EXACTA CON EL ESTADO LIMPIO DEL 313(g).
   El aire anti-solape contra el halo del bloque porcelana ya vive DENTRO de él
   (el `.mb-3` de .45rem del consentimiento, que NO colapsa hacia fuera porque
   `.kiosk-wash` tiene relleno propio) — o sea la botonera nunca tuvo separación
   propia. Medido: con `padding-top:.45rem` el Check In del estado limpio caía a
   y=374.2 en vez de y=367, gastando 7px de la holgura contra el teclado por
   nada. Con 0 la geometría del estado limpio queda IDÉNTICA a la que verificó
   Lupe (y=367). */
.kiosk-new-actions { flex: 0 0 auto; padding-top: 0; }

/* ══════════════════════════════════════════════════════════════════════════
   ÍTEM 498b · 🟢 LV-10 ("me encanta con botones largos,
   vámonos con ese") — EL FORMULARIO DE CLIENTE NUEVO EN 2 COLUMNAS.
   EL PROBLEMA: "vamos a cambiar el layout para liberar más
   espacio abajo". El ítem 313/463 ya había demostrado que el ALTO es el
   recurso escaso (el teclado flotante del BAR-01 tapa todo lo que pase de
   y=384) y que el ANCHO sobraba. El 463 fundió 4 filas en 2; el 498b da el
   paso siguiente: lo que antes se SUMABA en vertical (campos + preferencias)
   ahora corre EN PARALELO ⇒ el bloque mide el alto de la columna MÁS ALTA,
   no la suma de las dos. Mockup congelado: MBS\kahlo\mockup-498b-kiosco-2columnas.html
   ⚠️ ÁMBITO: TODO cuelga de `.kiosk-new-step` — las otras 6 pantallas del
   kiosco y `MobileCheckin.razor` (que usa su propio markup plano con
   `.kiosk-name-row`/`.kiosk-mi`, SIN .kiosk-form-box ni .kiosk-form-row-*)
   no ven ni una de estas reglas.
   ══════════════════════════════════════════════════════════════════════════ */

/* (1) EL ANCHO QUE EL LAYOUT NECESITA — SOLO EN ESTE PASO.
   Con `.kiosk-wrap` en 640px las 2 columnas NO CABEN (la columna de campos
   quedaría en ~348px y los botones largos en 226/113). Soltar el wrap
   globalmente es el ítem 491/498a y toca las 7 pantallas del kiosco: AQUÍ NO
   SE TOCA. En su lugar, este paso —y solo este— se sale del techo del wrap
   con un margen negativo calculado, así queda CENTRADO en la pantalla sin que
   ninguna otra pantalla se entere. A 1024×600: wrap 640 → paso 780 (x=122..902,
   sin cruzarse con el botón de idioma que vive en x=891..1010 porque el
   formulario interior se queda en 720px, x=152..872). */
.kiosk-new-step {
    width: min(94vw, 780px);
    margin-left: calc((100% - min(94vw, 780px)) / 2);
}

/* (2) EL CONTENEDOR QUE DECIDE — container query, no media query.
   Lo que manda no es el ancho de la PANTALLA sino el del FORMULARIO: el mismo
   viewport puede dar anchos distintos según banner/padding, y una tablet de
   1920 no necesita otro breakpoint. `.kiosk-form-box` (que ya existía con
   max-width:720px, ítem 463) pasa a ser el contenedor consultado. */
.kiosk-new-step .kiosk-form-box { container-type: inline-size; }
/* La columna de campos es OTRO contenedor, para su propia red de seguridad
   (regla (6)); el grid de 2 columnas NO necesita ser contenedor — pregunta por
   el de arriba. */
.kiosk-new-step .kiosk-col-fields { container-type: inline-size; }

/* (3) BASE = APILADO (cualquier formulario < 700px). Orden natural del DOM:
   campos → preferencias → BOTONES AL FINAL. Con flex + orden de DOM es
   IMPOSIBLE por construcción que la botonera quede en medio — el requisito
   explícito para el apilado. */
.kiosk-new-step .kiosk-form-2col,
.kiosk-new-step .kiosk-col-fields { display: flex; flex-direction: column; gap: 10px; }
/* dentro de las columnas el aire lo pone el `gap`, no los márgenes del 463
   (si no, 12/14px + 10px de gap = huecos dobles). Especificidad (0,3,0) para
   ganarle a `.kiosk-new-step .kiosk-form-row-a` (0,2,0) sin !important. */
.kiosk-new-step .kiosk-col-fields > .kiosk-form-row-a,
.kiosk-new-step .kiosk-col-fields > .kiosk-form-row-b { margin-bottom: 0; }
/* las casillas de aviso: centradas apilado, a la izquierda en 2 columnas
   (regla 4). `flex-wrap` porque en español "Mensaje de texto" + "Correo
   electrónico" NO caben en una línea dentro de una columna de ~301px. */
.kiosk-new-step .kiosk-notify-choices { flex-wrap: wrap; justify-content: center; }

/* (4) DOS COLUMNAS — desde 700px de FORMULARIO.
   1.3fr / 1fr con 28px de separación: a 720px de formulario da 391px de
   campos y 301px de preferencias (los números del mockup). La columna de
   preferencias ocupa LAS DOS FILAS del grid ⇒ el aviso de error del 176c
   crece DENTRO del hueco que ya sobraba en esa columna, sin empujar el alto
   total. Eso es lo que hace que el bloque mida casi lo mismo en los 4 estados. */
@container (min-width: 700px) {
    .kiosk-new-step .kiosk-form-2col {
        display: grid;
        grid-template-columns: 1.3fr 1fr;
        grid-template-areas: "fields prefs" "actions prefs";
        /* 🔴 `auto 1fr` Y NO `auto auto` — ESTO ES LO QUE MANTIENE EL BOTÓN
           QUIETO (lo que queda de LV-09 "la acción principal no se mueve").
           La columna de preferencias ocupa LAS DOS filas; cuando ella es la
           más alta (pasa SIEMPRE en español: 178.8px contra 152px de la
           izquierda) el sobrante se repartía entre las dos filas y el botón
           BAJABA 13px solo por cambiar de idioma — medido en el DOM, no
           supuesto. Con la segunda fila en `1fr` el sobrante cae TODO en ella
           y, con `align-items:start`, `Registrarme` se queda pegado justo
           debajo del Correo electrónico en los dos idiomas. */
        grid-template-rows: auto 1fr;
        column-gap: 28px; row-gap: 10px;
        align-items: start;
    }
    .kiosk-new-step .kiosk-col-fields { grid-area: fields; min-width: 0; }
    .kiosk-new-step .kiosk-col-prefs { grid-area: prefs; min-width: 0; text-align: left; }
    .kiosk-new-step .kiosk-new-actions { grid-area: actions; }
    .kiosk-new-step .kiosk-notify-choices { justify-content: flex-start; }
    /* 🚫 CERO OVERLAPPING (frente 14): con el flag de foto ON el bloque de
       avisos lleva el halo `.kiosk-wash` de 12+13px de spread, que pinta FUERA
       de la caja sin ocupar espacio. Al subir ese bloque al tope de su columna,
       su halo superior SE MONTA sobre el halo del chip del título si sube a
       ras. MEDIDO en el DOM real con los rects INFLADOS por el spread, y con
       los anillos REALES de cada pieza —el bloque de avisos NO usa los 12/13px
       de `.kiosk-wash` sino los 4/5px que le recortó el ítem 249c, y el chip
       del título usa 8/9px—: sin respiro quedaría en −2.4px; con estos 8px
       queda en +5.6px de aire. No cuesta alto total mientras la columna de
       preferencias no sea la más alta. */
    .kiosk-new-step .kiosk-col-prefs { padding-top: 8px; }
    /* 🔴 EL PEOR CASO (banner de cierre + error de validación a la vez) ES EL
       QUE MANDA: ahí el banner se come 75px arriba Y el bloque pierde el
       reclamo de −26px del 313(g) (deja de ser `:first-child`), así que el cupo
       baja a 234.8px. El último aviso de la columna derecha arrastraba su
       `margin-bottom` de .35rem como alto MUERTO al fondo del formulario — no
       separa de nada, es el último elemento. Quitarlo devuelve 5.6px justo
       donde más falta hacen. */
    .kiosk-new-step .kiosk-col-prefs > .alert:last-child { margin-bottom: 0; }
    /* ⚠️ EL TELÉFONO BAJA DE 240 A 150px — la fila ya no tiene los 640px del
       formulario entero, sino los ~391px de su columna. Con el criterio del 463 ("el teléfono son menos de 15 dígitos") 150px sobra; el
       que PAGA es el correo, que en 2 columnas mide ~231px contra los 388px de
       hoy. Es el efecto colateral inevitable de partir el formulario en dos y
       está medido y reportado, no descubierto en producción.
       🐛 POR QUÉ VA COMO VARIABLE Y NO COMO REGLA DIRECTA SOBRE LA FILA (medido
       en el DOM real, no supuesto): `.kiosk-form-row-b` vive DENTRO de
       `.kiosk-col-fields`, que ES un contenedor — así que una `@container` que
       apunte a la FILA pregunta por los 391px de la columna, NO por los 720px
       del formulario, y nunca se cumple (primer intento: el teléfono se quedó
       en 240px y el correo en 139px). Se le pone la variable a la COLUMNA
       (cuyo contenedor SÍ es `.kiosk-form-box`) y la fila la hereda. */
    /* 🟢 498c (mockup aprobado: "yo creo que estamos bien
       con el mockup nuevo") — EL ANCHO BASE BAJA DE 150px A 140px.
       Necesidad real de "(787) 555-0123" = 134.4px (Kahlo, `canvas.measureText`
       con la tipografía real del kiosco) ⇒ 140px deja 5.6px de aire. Los 10px
       que suelta el teléfono se los queda el correo, que pasa de 231 a 241px.
       🔴 ESTA LÍNEA NO DEPENDE DE NINGUNA PROPIEDAD NUEVA: es el CIMIENTO que
       corre en TODOS los navegadores, incluida la Raspberry, si resulta
       que no soporta `field-sizing`. Ya es mejor que hoy SIN arriesgar nada. */
    .kiosk-new-step .kiosk-col-fields { --bsp-kiosk-row-b: 140px 1fr; }

    /* ══ 🆕 MEJORA PROGRESIVA — CAMPOS ELÁSTICOS, DENTRO DE SU GUARDIA ══════
       🔴 POR QUÉ LA GUARDIA `@supports` ES LO MÁS IMPORTANTE DE ESTE LOTE —
       Kahlo lo MIDIÓ, no lo supuso: armar el grid `auto 1fr` pensando en
       `field-sizing` pero SIN envolverlo deja al navegador que no la soporta
       PEOR QUE HOY, no igual. Un <input> sin `field-sizing` dentro de una pista
       `auto` NO se mide por su contenido: toma el tamaño por defecto del
       navegador (~20 caracteres) y NO reacciona a lo que se escribe.

         escenario            teléfono    correo
         con field-sizing      139.2px    241.9px   ✅
         SIN guardia, sin      219.4px    161.7px   🔴 PEOR que hoy
         hoy (150px fijo)      150.0px    231.1px

       "Si no la soporta la ignora y no duele" es FALSO. Y habría reventado
       justo en la Raspberry, que es la sospechosa de no soportarla.
       ⇒ La regla elástica va AQUÍ DENTRO. El ancho base de arriba es el que
       corre siempre. NO SACAR ESTAS REGLAS DE SU `@supports`.

       🐛🐛 Y AQUÍ VIVE LA TRAMPA QUE ME COMÍ ENTERA — LÉELA ANTES DE "LIMPIAR"
       ESTO. Mi primer intento metió las reglas de `field-sizing` DENTRO de este
       `@container (min-width:700px)`, creyendo que así blindaba el apilado.
       RESULTADO MEDIDO EN EL DOM REAL: teléfono 219.4px / correo 159.8px — o
       sea, EXACTAMENTE el desastre que la guardia venía a evitar.
       ¿Por qué? Porque `.kiosk-col-fields` ES ÉL MISMO un contenedor
       (`container-type: inline-size`, regla (2) más arriba). Una `@container`
       se resuelve contra el contenedor MÁS CERCANO: para la COLUMNA ese es
       `.kiosk-form-box` (720px ✅), pero para todo lo que vive DENTRO de ella
       —la fila y los <input>— el más cercano es la propia columna, de 391px, y
       `min-width:700px` NUNCA se cumple. Resultado: se aplicó solo la regla que
       apunta a la columna (la variable del grid `auto minmax(...)`) y NO las que
       apuntan a los campos (`field-sizing`) ⇒ pista `auto` SIN campo elástico =
       el input toma su ancho por defecto (~20 caracteres) y aplasta al correo.
       📌 Es LA MISMA trampa que el 498b ya documentó 15 líneas más arriba para
       `--bsp-kiosk-row-b`. La pisé otra vez desde el otro lado.
       ⇒ REGLA PARA ESTE ARCHIVO: dentro de un `@container` de `.kiosk-form-box`
       SOLO se puede apuntar a `.kiosk-col-fields` (o a sus hermanos). Todo lo
       que apunte a sus DESCENDIENTES viaja como VARIABLE HEREDADA — nunca como
       regla directa. Las 3 variables de abajo son ese canal. */
    @supports (field-sizing: content) {
        .kiosk-new-step .kiosk-col-fields {
            /* 🔴 EL TECHO NO ES UN NÚMERO A MANO — ES LA GEOMETRÍA DE LA FILA:
               el teléfono es `auto` (se ajusta a lo escrito) y el correo es
               `1fr`, o sea EXACTAMENTE lo que sobra, nunca más. El
               `minmax(44px,·)` impide que un correo largo infle su propia pista
               y desborde la columna (Kahlo, correo de 50 caracteres: fila
               391.1px = columna 391.1px, 0px de desborde). */
            --bsp-kiosk-row-b: auto minmax(44px, 1fr);
            /* Fila Nombre·Inicial·Apellido: la Inicial deja de ocupar 80px fijos
               y se ajusta a su único carácter; lo que suelta se lo reparten
               Nombre y Apellido (el Apellido más ancho a propósito, ítem 463: en
               PR mucha gente escribe los dos apellidos). */
            --bsp-kiosk-row-a: 1fr auto 1.1fr;
            /* EL INTERRUPTOR de los campos elásticos + el tope del teléfono.
               Viajan HEREDADOS hasta los <input> (ver la trampa de arriba) y,
               fuera de las 2 columnas, quedan sin definir ⇒ los campos se
               comportan EXACTAMENTE como hoy. Por eso el apilado angosto y el
               formulario de 1 columna (teléfono 240px, ítem 463) no se enteran
               de que esto existe. */
            --bsp-kiosk-elastic: content;
            --bsp-kiosk-tel-max: 200px;
        }
    }
}
/* ── Los campos elásticos, ya FUERA del `@container` (obligatorio: ver la
      trampa documentada arriba). No se activan solos: obedecen a las variables
      que la columna solo define en modo 2 columnas y con la guardia puesta.
   🔴 EL SUELO TÁCTIL: 44px. Un campo con UN SOLO carácter cae exacto a 44.0px y
      ni un pixel menos (sin el piso, Kahlo midió 39.5px). Es EL MISMO 44 que ya
      rige el ALTO de todos los campos (LV-09) — no es un número nuevo: un campo
      que un dedo no puede tocar es peor que uno estrecho. Es inofensivo en todos
      los modos (ningún campo del kiosco baja de 44px), así que va incondicional.
   `:not([type=email])` = SOLO el teléfono. El correo NO lleva `field-sizing` a
      propósito: vive en `1fr`, así que su contenido no mueve su ancho (Kahlo lo
      verificó en vivo), y dejarlo fuera hace IMPOSIBLE que un correo largo
      empuje la pista si alguien cambia mañana el `minmax` por un `1fr` pelado.
   📌 En el DOM real el teléfono es un <input> SIN `type` y el correo lleva
      `type="email"` ⇒ el selector acierta sin tocar el markup.
   ⚠️ NO ponerle `type="tel"` al teléfono para "mejorar" el selector: cambia el
      teclado del móvil y el ítem 460 está fuera de lote. */
@supports (field-sizing: content) {
    .kiosk-new-step .kiosk-col-fields > .kiosk-form-row-a .kiosk-mi {
        field-sizing: var(--bsp-kiosk-elastic, fixed); min-width: 44px;
    }
    .kiosk-new-step .kiosk-col-fields > .kiosk-form-row-b input:not([type="email"]) {
        field-sizing: var(--bsp-kiosk-elastic, fixed); min-width: 44px;
        /* el tope solo existe en 2 columnas: apilado, el teléfono llena su fila
           como hoy (un `max-width:200px` suelto lo habría encogido ahí). */
        max-width: var(--bsp-kiosk-tel-max, none);
    }
}
/* el ancho de la fila Teléfono·Correo, en un solo sitio y con sus 3 estados:
   240px+resto (formulario ancho de 1 columna) · 150px+resto (2 columnas) ·
   apilado (pantalla angosta, el mismo umbral del 463). */
.kiosk-new-step .kiosk-col-fields > .kiosk-form-row-b {
    grid-template-columns: var(--bsp-kiosk-row-b, 240px 1fr);
}
@media (max-width: 559.98px) {
    .kiosk-new-step .kiosk-col-fields { --bsp-kiosk-row-b: 1fr; }
}

/* (5) 🟢 LV-10 — BOTONES LARGOS, 2:1 EXACTO.
   🔴 CON `flex-grow` NO SALE 2:1: Kahlo lo midió en 1.7:1 — un <button> con
   texto tiene un piso de tamaño ligado a su CONTENIDO que resiste el reparto
   elástico INCLUSO con `min-width:0` (lo confirmó comparando contra un <div>
   vacío con el mismo CSS, que sí daba 2:1). NO REINTENTAR con flex-grow
   pensando que fue un error de configuración: ya se probó.
   La cura es un ANCHO EXPLÍCITO con calc(), que no depende del contenido y da
   la misma proporción en EN y en ES.
   El relleno lateral baja de 1.4rem a .5rem: con el ancho ya fijado, el
   relleno solo sirve para que una etiqueta larga no toque el borde — y en
   `Volver` (127px) 1.4rem×2 se comía 45px de los 127. */
.kiosk-new-step .kiosk-new-actions {
    display: flex; gap: 10px; width: 100%;
    flex: 0 0 auto; padding-top: 0;
}
.kiosk-new-step .kiosk-new-actions > .btn {
    flex: 0 0 auto; min-width: 0; text-align: center;
    padding-left: .5rem; padding-right: .5rem;
}
.kiosk-new-step .kiosk-new-actions > .btn-primary { width: calc((100% - 10px) * 2 / 3); }
.kiosk-new-step .kiosk-new-actions > .btn-outline-secondary { width: calc((100% - 10px) * 1 / 3); }

/* (6) RED DE SEGURIDAD DE LA COLUMNA DE CAMPOS — si alguna vez queda angosta
   (< 380px), sus dos filas se apilan igual que en el breakpoint de 559.98px de
   arriba. Con los dispositivos conocidos NUNCA se activa en modo 2 columnas
   (la columna mide 391px); sirve para el apilado en pantallas estrechas, donde
   coincide exactamente con lo que ya hacía la media query. */
@container (max-width: 379.98px) {
    .kiosk-new-step .kiosk-form-row-a { grid-template-columns: 1fr 80px; gap: 10px; }
    .kiosk-new-step .kiosk-form-row-a .kiosk-last { grid-column: 1 / -1; }
    .kiosk-new-step .kiosk-form-row-b { grid-template-columns: 1fr; gap: 10px; }
}

.kiosk-pad { display: grid; grid-template-columns: repeat(3, 88px); gap: 10px; justify-content: center; }
.kiosk-key { height: 64px; font-size: 1.4rem; font-weight: 600; touch-action: manipulation; }
/* R-411: SOLO la pantalla del código de 6 dígitos — teclas una pizca más bajas
   (56px sigue sobre el mínimo táctil de 44px) para que la fila "¿No te llegó el
   código?" quepa SIN scroll en la tablet de 1024×600 (ítem 238). Medido: con las
   teclas de 64px la salida quedaba bajo el doblez — invisible justo en el
   momento de la frustración, el bug que el ítem mata. El teclado del TELÉFONO
   no se toca. */
.kiosk-pad-code .kiosk-key { height: 56px; font-size: 1.25rem; }

/* ══════════════════════════════════════════════════════════════════════════
   ÍTEM 282 — KIOSKO: pantallas "¿Con quién?" / "¿Qué
   servicios?", calcadas del mockup APROBADO
   MBS\kahlo\mockup-kiosko-barbero-servicios.html. Carrusel horizontal
   (flechas + swipe nativo overflow-x/scroll-snap + drag mouse vía
   bspKiosk.initCarouselDrag) — CUALQUIER cantidad de barberos/servicios, UNA
   sola fila, CERO scroll vertical del kiosko (regla 238-v2). Cards = FOTO
   COMPLETA con banda-overlay de nombre (nunca texto directo sobre foto);
   recorte de cara sesgado hacia arriba (regla 280, jamás decapitar).
   ══════════════════════════════════════════════════════════════════════════ */
.kbs-crumb { display: flex; align-items: center; gap: .5rem; flex-shrink: 0; justify-content: center; margin-top: .25rem; }
/* ══ ÍTEM 315 · NIVEL 1 (Kahlo, "bomba de tiempo") — ESTOS CONTROLES VIVEN
   TAMBIÉN FUERA DEL KIOSKO (MobileCheckin 29 usos, WaitingList, y el ítem 314
   los va a clonar a Pos/BookAppointment/GuestBooking). Antes pintaban
   `color: var(--primary-900)` — un token de rampa "900" que es OSCURO en LOS 8
   TEMAS — sobre `--surface-color`, que es OSCURO en 5 → el texto ERA el fondo
   (deep 1.00:1, hc-dark 1.06:1, onyx 1.18:1, jade 1.02:1, rose 1.93:1).
   AHORA: tinta = --bs-body-color (claro en oscuro, oscuro en claro) y relleno =
   --surface-color (nunca un tinte hard-coded). El borde sube de #D8D4C8
   (1.36:1) al navy de marca suavizado (3.51:1 sobre #F7F5F0). Los que NO tenían
   borde (step-pill, who-badge) lo ganan con el padding COMPENSADO en 1px, para
   que la caja mida EXACTAMENTE lo mismo que antes (regla E-40 v2: cero
   desplazamiento). El Nivel 2 (porcelana + halo, sobre foto) va más abajo. ══ */
.kbs-crumb-btn { background: var(--surface-color, #fff); border: 1px solid var(--control-edge, rgba(20,34,78,.55)); border-radius: .4rem; color: var(--bs-body-color); }
.kbs-step-pill { background: var(--surface-color, #fff); border: 1px solid var(--control-edge, rgba(20,34,78,.55)); border-radius: 999px; padding: calc(.25rem - 1px) calc(.8rem - 1px); font-size: .78rem; font-weight: 600; color: var(--bs-body-color); }
.kbs-title-block { text-align: center; margin-top: .35rem; flex-shrink: 0; }
.kbs-carousel-wrap { display: flex; align-items: center; gap: 8px; margin-top: .5rem; flex-shrink: 0; }
.kbs-carousel-grow { flex: 1 1 auto; min-height: 0; }
.kbs-arrow {
    flex-shrink: 0; width: 40px; height: 40px; border-radius: 50%; padding: 0;
    background: var(--surface-color, #fff); border: 1px solid var(--control-edge, rgba(20,34,78,.55));
    color: var(--bs-body-color); font-size: 1.2rem; font-weight: 800;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 2px 6px rgba(20,34,78,.12); touch-action: manipulation;
}
/* ÍTEM 315 · hallazgo 2️⃣ de Kahlo: `.kbs-arrow:disabled { opacity:.25 }` era CSS
   MUERTO — ni Kiosk.razor ni bsp-kiosk.js ponen jamás `disabled` en las flechas
   del carrusel. Se BORRA (no se deja comentado). Que las flechas nunca avisen
   "llegaste al final" es un hueco de UX real, pero es otro ítem. */
.kbs-scroller {
    flex: 1 1 auto; min-width: 0; height: 100%; display: flex; gap: 12px; align-items: stretch;
    overflow-x: auto; scroll-snap-type: x mandatory; padding: 4px 2px 8px;
    scrollbar-width: none; -ms-overflow-style: none; cursor: grab; touch-action: pan-x;
}
.kbs-scroller::-webkit-scrollbar { display: none; }
.kbs-scroller.kbs-dragging { cursor: grabbing; scroll-snap-type: none; }

/* Card = foto completa 148x168 (mismo tamaño para barbero Y servicio — un
   solo sistema visual entre las 2 pantallas del kiosko) */
.kbs-card {
    position: relative; scroll-snap-align: start; flex-shrink: 0; cursor: pointer;
    width: 148px; height: 168px; border-radius: 14px; overflow: hidden; padding: 0;
    border: 2px solid transparent; box-shadow: 0 2px 8px rgba(20,34,78,.20);
    background: var(--primary-900, #14224E); transition: transform .12s ease, box-shadow .12s ease;
    touch-action: manipulation;
}
.kbs-card:hover { transform: translateY(-2px); box-shadow: 0 8px 18px rgba(0,0,0,.32); }
.kbs-card.selected { border-color: var(--accent-color, #C8102E); box-shadow: 0 0 0 3px rgba(200,16,46,.35), 0 8px 18px rgba(0,0,0,.32); }

/* ══ 🚫 R-429 — LA CARD DEL BARBERO QUE NO ESTÁ TOMANDO WALK-INS ═════════════
   Sale AL FINAL del carrusel, ATENUADA y NO TOCABLE (excepción de transparencia
   decidida por el dueño: en sillas rentadas, si el barbero desaparece el cliente
   cree que se fue de la barbería y eso le hace daño al que paga la renta).
   · `pointer-events:none` mata el dedo Y el mouse; el markup además es un <div>
     sin handler y sin tabindex, así que el teclado tampoco lo alcanza. Los tres
     candados a propósito: la card es la puerta MÁS visible del kiosco.
   · `filter: grayscale` + opacidad = se ve QUIÉN es (esa es toda la gracia) pero
     nunca compite con los disponibles.
   · Se anula el hover heredado: una card que se levanta al pasar por encima
     PROMETE que se puede tocar. */
/* ⚪ N-11, LA SEGUNDA MITAD (Tito, cierre de la vuelta 9). Lupe midio el CHIP de
   la lista de espera (3.15:1). MEDIDO AQUI, que es la MISMA ley con otro nombre
   dimido: el nombre del barbero apagado en la card del kiosco daba **4.43:1**
   sobre su banda — bajo el 4.5:1 de AA, y esta es la superficie donde el CLIENTE
   busca la cara de su barbero. La atenuacion sube de .55 a .62: sigue clarisimo
   que la card esta apagada (grayscale(1) intacto, sin hover, sin puntero, con su
   "No disponible ahora") y el nombre pasa AA. Un acierto no es el unico: si el
   atenuado tiene que dejar leer el nombre (razon de negocio de R-429), tiene que
   dejarlo leer en LAS DOS superficies. */
.kbs-card-off {
    cursor: default; pointer-events: none;
    filter: grayscale(1); opacity: .62;
    box-shadow: 0 1px 4px rgba(20,34,78,.14);
}
.kbs-card-off:hover { transform: none; box-shadow: 0 1px 4px rgba(20,34,78,.14); }
.kbs-card-off .kbs-name-band .tg { opacity: 1; font-weight: 700; }
/* Hallazgo Lupe (2026-07-23): "Cualquier barbero" FIJO fuera del carrusel —
   separador sutil para que se lea como "anclado" y no como un card más que
   quedó suelto del scroller.
   ══ ÍTEM 339 (SEGUNDA VEZ QUE SE REPORTA) ══════════════
   Verbatim: "el primer card, cualquier barbero, esta fijo, los otros cards se
   deslizan por detras de el". La estructura NO está mal (la tarjeta vive FUERA
   del scroller a propósito, para que nunca se pueda ir de vista); lo que
   faltaba era FRONTERA VISUAL: el scroller arrancaba pegado, así que una
   tarjeta a medio desplazar se cortaba EXACTAMENTE en el borde de la fija y el
   ojo lo lee como "pasa por debajo".
   ⚠️ LA LECCIÓN DE LA PRIMERA VEZ: el arreglo anterior fueron 2px de margen +
   una sombra de 3px y NO BASTÓ — se volvió a reportar 2 días después.
   Cuando el problema es de PERCEPCIÓN, un arreglo sutil no lo resuelve. Aquí la
   frontera es EXPLÍCITA: separación real + una REGLA VERTICAL de 2px con el
   mismo `--control-edge` que ya usan flechas y migas (cero paleta nueva, cero
   riesgo en los 8 temas). Con la regla puesta, la tarjeta que se desplaza se
   corta EN LA LÍNEA — o sea, en el borde de una pista que se ve — en vez de
   contra el canto de la tarjeta anclada.
   Por qué el borde va en el SCROLLER y no en la tarjeta: `.kbs-card` tiene
   `overflow:hidden` + `border-radius:14px` + su propio `border` de selección,
   así que cualquier línea puesta ahí se leería como parte de la tarjeta.
   El selector es de HERMANO ADYACENTE a propósito: el carrusel de chips de la
   Lista de Espera usa `.kbs-carousel-wrap` SIN tarjeta anclada y no debe
   heredar ninguna línea.
   📍 Las reglas del SCROLLER de este ítem NO viven aquí sino más abajo, DESPUÉS
   de `.kbs-shared > .kbs-scroller` y `.kbs-compact > .kbs-scroller`: esos dos
   usan el atajo `padding` y tienen la misma especificidad (0,2,0) que
   `.kbs-card-pinned + .kbs-scroller`, así que puestas aquí el `padding-left`
   se perdería en silencio en las pantallas compartidas (trampa ④ del estándar
   de QA: "a igual especificidad manda el orden"). Buscar «ÍTEM 339 · frontera».
*/
/* ══ 🐛 TERCERA VEZ QUE SE REPORTA (el dueño 2026-08-22, con foto) ═════════════
   *"la tarjeta Cualquier barbero está congelada y las Sillas se ven pasar POR
   DETRÁS de ella."*
   📏 LO QUE DICE LA MEDICIÓN, QUE NO ES LO QUE PARECE (arnés `carrusel.py`,
   1024×600 y 390×844, en reposo y a media tarjeta):
     · tarjetas del scroller que INVADEN la caja de la anclada: **0** (ni un px);
     · `background-color` de la anclada: **rgb(20,34,78)**, `opacity: 1` — OPACA;
     · hueco real entre la anclada y la pista: **20px** (12 en teléfono).
   ⇒ **NADA pasa por detrás: es imposible con este DOM** (la tarjeta vive FUERA
   del scroller). Y por eso los dos arreglos anteriores —2px de margen + sombra
   de 3px, y luego el divisor de 2px del ítem 339— no cerraron el reporte: los
   dos atacaban el HUECO, y el problema no está en el hueco.
   🔑 LO QUE DE VERDAD PASA, VISTO EN LA CAPTURA A 390px: la tarjeta que entra
   se CORTA en el canto de la pista, **su banda de nombre queda partida a media
   palabra justo al lado de la anclada**, y la anclada es **la única tarjeta de
   la fila con relleno de RAYAS DIAGONALES** — las mismas rayas diagonales del
   papel tapiz del kiosco, en el mismo ángulo. Una tarjeta rayada sobre un fondo
   rayado **se lee como un hueco**, y por un hueco las cosas pasan por detrás.
   ✅ POR ESO EL ARREGLO ES DE PERCEPCIÓN Y VA EN DOS PIEZAS, ninguna cuesta un
   píxel de layout (`box-shadow` y `background` no entran en el flujo, así que la
   geometría 14/14 del ítem 339 queda intacta):
     ① relleno SÓLIDO (abajo, `.kbs-card-any-stripe`) — deja de haber hueco;
     ② esta sombra, que ahora se PROYECTA SOBRE LA PISTA en vez de insinuarse:
        la tarjeta que se desliza pasa por DEBAJO de una tarjeta claramente
        LEVANTADA, que es exactamente lo que es.
   📌 Sigue siendo `box-shadow` y NO `border` (trampa de R-527): un borde en
   `.kbs-card` competiría con el anillo de selección de 2px y con su
   `border-radius`. */
/* ══ 🎠 R-431 (2026-08-25) — QUIÉN SIGUE USANDO ESTA CLASE, Y POR QUÉ NO SE
   BORRÓ ═══════════════════════════════════════════════════════════════════════
   El KIOSCO (`Kiosk.razor`) y el CHECK-IN MÓVIL (`MobileCheckin.razor`) YA NO LA
   USAN: por R-431 su tarjeta "Cualquier barbero" volvió DENTRO de `.kbs-scroller`
   como primer hijo y scrollea como una más. Toda la arqueología de arriba (ítem
   282 → Lupe 07-23 → ítem 339 → la tercera vez del 08-22) describe un anclaje
   que en esas dos pantallas ya no existe, y con él se van sus tres problemas de
   percepción: no hay nada anclado detrás de lo cual pasen los otros.
   ⛔ PERO LAS REGLAS SE QUEDAN, porque la clase TIENE OTRO CONSUMIDOR VIVO:
   `Components/Shared/BarberCards.razor` (el componente compartido del ítem 314),
   y por él la usan CUATRO pantallas más — wizard de citas (`BookAppointment`),
   POS, Lista de Espera y `StaffBookPanel`. Borrarlas hoy les cambiaría el layout
   en silencio y sin medir, que es justo lo que R-431 vino a evitar.
   📌 PENDIENTE DE DECISIÓN DEL DUEÑO: el wizard de citas es CARA AL CLIENTE y
   corre en celular ⇒ le aplica el mismo verbatim ("en un celular el card ocupa
   mucho espacio"). Las otras tres son de staff. R-431 acotó su alcance a kiosco
   + check-in móvil, así que aquí no se toca: se anota y se pregunta. */
.kbs-card-pinned {
    margin-right: 6px;
    box-shadow: 0 2px 8px rgba(20,34,78,.20),
                10px 0 14px -6px rgba(6,10,28,.60);
}
/* R-472 / ÍTEM 1164 (vuelta 2) — LA TARJETA DEL KIOSCO, DEL CHECK-IN MÓVIL Y
   DEL SELECTOR DE SERVICIO AL RESERVAR. Levantada por Eugenio (9) en el barrido
   de coherencia; NO estaba en mi lista de la vuelta 1 ni en la de Kahlo, y el
   motivo es que **no se puede ver desde el CSS**: el `src` es
   `/services/{id}/photo.jpg`, y ese endpoint (`Program.cs:1189-1195`) devuelve
   el HERO DEL MODELO VIRTUAL cuando el servicio no tiene foto propia (cascada
   R-423) ⇒ una clase que nunca nombra el catálogo recorta fotos del catálogo.
   Se encuentra grepeando al ESCRITOR (el endpoint), no las clases (LEY 14).
   Medido en vivo (reserva de invitado, `/b/{barbero}/book`, hueco 144x164):
   sin ancla el recorte empezaba en el **18,206 %** ⇒ cortaba la coronilla de
   LAS 153 fotos de retrato del catálogo. R-472 nombra el kiosco por su nombre.
   ⚠️ LA MISMA CLASE SIRVE FOTOS DE BARBERO (`/employees/{id}/photo.jpg`, en
   Kiosk.razor:939/968, MobileCheckin.razor:429/448 y BarberCards.razor:72), y
   esas llevan su `object-position:50% 15%` EN LÍNEA por el ítem 280 — el estilo
   en línea gana siempre, así que este ancla NO las toca (verificado: siguen en
   `50% 15%`). Y el default de aquí es el SEGURO en las dos direcciones: si
   mañana alguien añade una tarjeta de SERVICIO se le aplica lo que R-472 pide,
   y si añade una de BARBERO cae en el tope de la cabeza, no en el centro.
   📌 PENDIENTE, y lo digo en vez de hacerlo aquí: ese `50% 15%` está copiado
   CINCO veces en línea. Consolidarlo en una clase es del ítem 280, no de éste. */
.kbs-card-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 0; }
.kbs-card-mono {
    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    font-size: 2.4rem; font-weight: 800; color: #fff;
    background: linear-gradient(160deg, #1E3A8A, var(--primary-900, #14224E));
}
/* Card "Cualquier barbero" — SIEMPRE el primer card, patrón navy + tijeras */
.kbs-card-any { position: absolute; inset: 0; background: linear-gradient(160deg, #1c2c5e, #0c1636); }
/* ① RELLENO SÓLIDO. Antes: `background: var(--barber-stripe); opacity:.16` — o
   sea las MISMAS rayas diagonales del papel del kiosco, encima del navy. El
   resultado es técnicamente opaco (medido: `opacity` del card = 1) pero se LEE
   translúcido, porque el ojo empareja las diagonales de la tarjeta con las del
   fondo. Ahora es un brillo vertical suave: da volumen, **no repite el motivo
   del fondo** y no depende del tema (va sobre el navy propio de la tarjeta, no
   sobre la superficie de la página).
   🎨 La identidad de barbería NO se pierde: la ✂️ del centro es la que dice
   "cualquier barbero", y la raya de la casa sigue viva en su sitio (cabecera,
   pantalla de reconexión, poste). */
.kbs-card-any-stripe {
    position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(255,255,255,.12) 0%, rgba(255,255,255,0) 58%);
}
.kbs-card-any-icon { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 2.4rem; }
.kbs-svc-fallback {
    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    font-size: 2.2rem; color: #fff; background: linear-gradient(160deg, #1E3A8A, var(--primary-900, #14224E));
}
.kbs-name-band {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
    padding: 7px 8px 8px; text-align: center; color: #fff;
    background: linear-gradient(to top, rgba(6,10,28,.92) 0%, rgba(6,10,28,.55) 55%, rgba(6,10,28,0) 100%);
}
.kbs-name-band .nm { font-size: .8rem; font-weight: 700; line-height: 1.15; }
.kbs-name-band .tg { font-size: .68rem; opacity: .9; margin-top: 1px; }
.kbs-check {
    position: absolute; top: 6px; right: 6px; z-index: 3; width: 24px; height: 24px; border-radius: 50%;
    background: var(--accent-color, #C8102E); color: #fff; display: none; align-items: center; justify-content: center;
    font-size: .8rem; font-weight: 800; box-shadow: 0 2px 6px rgba(0,0,0,.4);
}
.kbs-card.selected .kbs-check { display: flex; }
.kbs-qty-badge {
    position: absolute; top: 6px; right: 6px; z-index: 3; min-width: 24px; height: 24px; border-radius: 12px;
    background: var(--accent-color, #C8102E); color: #fff; display: none; align-items: center; justify-content: center;
    font-size: .72rem; font-weight: 800; box-shadow: 0 2px 6px rgba(0,0,0,.4); padding: 0 .4rem;
}
.kbs-card.selected .kbs-qty-badge { display: flex; }

/* Stepper −/+ DENTRO de la banda-overlay (solo con qty≥1) — control preciso
   sin saturar la vista con 14 servicios en pantalla) */
.kbs-stepper { display: flex; align-items: center; justify-content: center; gap: .5rem; margin: .15rem 0 .05rem; }
.kbs-stepper button {
    width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; padding: 0;
    border: 1px solid rgba(255,255,255,.55); background: rgba(255,255,255,.16); color: #fff;
    font-weight: 800; font-size: .9rem; display: flex; align-items: center; justify-content: center;
    touch-action: manipulation;
}
.kbs-stepper button:disabled { opacity: .3; }
/* ⓘ R-347 · ÍTEM 980(b) 2.ª vuelta — el ⓘ del tope vive en la ESQUINA SUPERIOR
   IZQUIERDA de la card (espejo del badge ×N, z3 como él: encima de la foto y
   del disparador de la galería). Por qué no en la fila del stepper: medido a
   390x844 con dedo, la banda compacta tiene 100 px útiles y «− ×N + ⓘ» pide
   121 ⇒ el borde de la card recortaba el blanco de 44 a 41 y empujaba el «−»
   fuera. Aquí el envoltorio `.bsp-help` (44×44 con `pointer: coarse`, ítem 951)
   cabe entero en la foto. El DIBUJO no cambia de tamaño (1,15 rem); solo la
   tinta: blanco con un fondo oscuro translúcido para leerse sobre cualquier
   foto (la de un servicio puede ser clara). */
.kbs-cap-help { position: absolute; top: 4px; left: 4px; z-index: 3; display: flex; }
.kbs-cap-help .bsp-help-btn {
    border-color: rgba(255,255,255,.9); color: #fff; background: rgba(6,10,28,.55);
    box-shadow: 0 1px 4px rgba(0,0,0,.35);
}
.kbs-cap-help .bsp-help-btn:hover,
.kbs-cap-help .bsp-help-btn[aria-expanded="true"] { background: #fff; color: #14224E; }
.kbs-stepper button:hover:not(:disabled) { background: rgba(255,255,255,.28); }
/* ══ 🔴 ÍTEM 980 · R-400 — "×20" SE PARTÍA EN DOS LÍNEAS ══════════════════════
   R-400 manda, en su lista de verificación, "que el stepper pinte bien DOS
   DÍGITOS". Con el tope en ×20 (`ServiceQty.FixedMax`), al 175% de la escala de
   accesibilidad el número se leía "×2" con un "0" DEBAJO. Regla escrita y la
   pantalla decía lo contrario ⇒ fallo, no roce. Lo levantó VERA midiéndolo
   RENDERIZADO, que es lo que no se había hecho.

   🔬 LA CAUSA REAL, y NO es la que se supuso de entrada — medida en un banco
      con este mismo app.css, la fuente Inter real y `<article class="content">`:
      el culpable es `overflow-wrap: break-word` que `.content` (app.css:64)
      DEJA CAER POR HERENCIA hasta este `.n`. `break-word` autoriza a partir
      DENTRO de una palabra justo cuando la palabra no cabe — y "×20" al 175%
      no cabe. CONTROL, en los dos sentidos: fuera de `.content`, con la tarjeta
      estrujada a 40px y el `.n` a su `min-width`, "×20" NO se parte (desborda,
      1 línea); dentro de `.content` se parte en 2 SOLO al 175% y SOLO con ×20.
      ⇒ "×" (U+00D7) no ofrece salto de línea por sí solo. Sin esa herencia no
        hay partición: la falta de `nowrap` sola no bastaba para romperlo.

   ✅ `white-space: nowrap` va en la regla BASE, no solo en la compacta, y es a
      propósito (LEY 14 — una compuerta no es compuerta si no es la única
      puerta). Las otras superficies del stepper —kiosko, check-in móvil,
      reserva pública— hoy NO se parten, pero por ARITMÉTICA (hueco 52px contra
      39,6 que pide el texto), no por construcción. `MobileCheckin` ni siquiera
      declara `@layout` ⇒ también cuelga de `.content` y también hereda
      `break-word`. Eso es un plazo, no una garantía. Aquí se cierra la puerta.
   📏 REGRESIÓN CERO, medida: `nowrap` NO mueve un pixel de lo que hoy cabe —
      solo prohíbe una segunda línea que hoy solo aparece en el caso roto. */
.kbs-stepper .n { min-width: 24px; text-align: center; font-weight: 800; font-size: .82rem; color: #fff; white-space: nowrap; }

/* Chip "con quién" (pantalla 2) + chips de categoría */
.kbs-who-badge {
    flex-shrink: 0; display: flex; align-items: center; justify-content: center; gap: .4rem;
    background: var(--surface-color, #fff); border: 1px solid var(--control-edge, rgba(20,34,78,.55));
    border-radius: 999px; padding: calc(.3rem - 1px) calc(.9rem - 1px); margin: .35rem auto 0;
    font-size: .8rem; font-weight: 600; width: fit-content; color: var(--bs-body-color);
}
.kbs-who-badge img, .kbs-who-badge .mini-mono { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
.kbs-who-badge .mini-mono { display: flex; align-items: center; justify-content: center; font-size: .62rem; font-weight: 800; color: #fff; background: var(--primary-900, #14224E); }
.kbs-cat-chips { flex-shrink: 0; display: flex; gap: .4rem; justify-content: center; flex-wrap: wrap; margin-top: .5rem; }
.kbs-chip {
    background: var(--surface-color, #fff); border: 1px solid var(--control-edge, rgba(20,34,78,.55)); border-radius: 999px;
    padding: .3rem .85rem; font-size: .78rem; font-weight: 600; color: var(--bs-body-color);
    touch-action: manipulation;
}
.kbs-chip.active { background: var(--gradient-btn, linear-gradient(135deg,#C8102E,#9E0C24)); color: #fff; border-color: #9E0C24; }

/* Pie fijo: total estimado + "Ponerme en la fila" (CREA el turno — único punto) */
.kbs-footer { flex-shrink: 0; margin-top: .6rem; padding-top: .5rem; border-top: 1px solid rgba(20,34,78,.12); display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.kbs-total { font-size: .88rem; }
.kbs-total b { font-size: 1.1rem; color: var(--accent-color, #C8102E); }
.kbs-cta {
    min-height: 56px; font-size: 1.05rem; font-weight: 600; color: #fff;
    background: var(--gradient-btn, linear-gradient(135deg,#C8102E,#9E0C24)); border: 1px solid #9E0C24; border-radius: .6rem;
    padding: .5rem 1.4rem; box-shadow: 0 0 12px rgba(200,16,46,.25); touch-action: manipulation;
}
.kbs-cta:disabled { opacity: .4; box-shadow: none; }
.kbs-cta:not(:disabled):hover { background: linear-gradient(135deg,#9E0C24 0%,#7C081B 100%); }

/* ══════════════════════════════════════════════════════════════════════════
   CALENDARIO VERDE/ROJO — reservar cita Y reserva de invitado
   ITEM 314 (hallazgo de Tito 2026-07-25): estas reglas vivian en un <style>
   DENTRO de BookAppointment.razor, pero GuestBooking.razor usa exactamente las
   mismas clases. Resultado: el calendario de la pagina PUBLICA (la del QR del
   barbero) salia sin grilla — los 7 dias de la semana uno debajo del otro y los
   dias sin color verde/rojo. Aqui son GLOBALES: una sola definicion para las
   dos pantallas (misma leccion del 277).
   ────────────────────────────────────────────────────────────────────────── */
.bsp-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.bsp-cal-day { border: none; border-radius: 8px; padding: 6px 0; font-size: .9rem; font-weight: 600; }
.cal-ok { background: #d1e7dd; color: #0f5132; cursor: pointer; }
.cal-ok:hover { filter: brightness(.92); }
.cal-no { background: #f8d7da; color: #842029; opacity: .75; }
.cal-past { background: transparent; color: #adb5bd; }
.cal-sel { outline: 3px solid var(--primary-color, #14224E); outline-offset: 1px; }
.bsp-cal-dot { display: inline-block; width: 12px; height: 12px; border-radius: 4px; margin-right: 5px; vertical-align: -1px; }

/* ══════════════════════════════════════════════════════════════════════════
   ITEM 314 — LOS MISMOS CARDS, FUERA DEL KIOSKO
   Componentes compartidos Components/Shared/BarberCards.razor y
   ServiceCards.razor. El kiosko corre a 1024x600 con el carrusel dentro de una
   columna flex de altura fija (.kbs-carousel-grow); en una pagina normal el
   carrusel vive en el flujo del documento, asi que el scroller no puede pedir
   height:100% de un padre de altura indefinida (se resuelve a auto, pero el
   padding vertical del kiosko sobra). Aqui se le da altura por CONTENIDO.
   ────────────────────────────────────────────────────────────────────────── */
/* padding lateral 3px (el kiosko trae 2px): el anillo de seleccion mide 3px de
   spread y `overflow-x:auto` CLIPEA tambien en vertical/horizontal — con 2px el
   anillo del card del borde salia cortado 1px. */
.kbs-shared > .kbs-scroller { height: auto; align-items: flex-start; padding: 4px 3px 8px; }

/* VARIANTE COMPACTA — pantallas de STAFF (POS). La regla de simplicidad
   [[feedback_ui_simplicity_non_tech_customers]] es para el CLIENTE; al cajero
   lo que le importa es cobrar rapido y no hacer scroll de mas. Mismo sistema
   visual (foto entera + banda), 116x132 en vez de 148x168. */
.kbs-compact > .kbs-scroller { gap: 8px; padding: 4px 3px 7px; }
.kbs-compact .kbs-card { width: 116px; height: 132px; border-radius: 12px; }
.kbs-compact .kbs-arrow { width: 32px; height: 32px; font-size: 1rem; }
.kbs-compact .kbs-name-band { padding: 5px 6px 6px; }
.kbs-compact .kbs-name-band .nm { font-size: .72rem; line-height: 1.1; }
.kbs-compact .kbs-name-band .tg { font-size: .62rem; }
.kbs-compact .kbs-check { width: 20px; height: 20px; min-width: 20px; top: 4px; right: 4px; font-size: .66rem; }
/* ══ 🔢 ÍTEM 980 · R-400 — EL BADGE DE CANTIDAD SE SEPARA DE `.kbs-check` ═════
   R-400 manda verificar al construir "que el stepper pinte bien DOS DÍGITOS", y
   con el tope en ×20 (`ServiceQty.FixedMax`) los dos dígitos ya llegan.
   🔬 LO QUE ESTABA MAL, y NO es lo que parecía: `.kbs-check` es un CÍRCULO FIJO
      (un ✓, siempre un carácter) y `.kbs-qty-badge` es una PÍLDORA QUE CRECE
      (`min-width` + `padding` en rem). Iban en la MISMA declaración, así que la
      píldora heredaba `width: 20px` — un ancho FIJO que gana sobre `min-width`
      y le quita la única defensa que tenía.
   📏 MEDIDO (Chrome, 116px de tarjeta compacta), y esto corrige el reporte de
      entrada: a escala de fuente NORMAL "×20" mide 16,6px dentro de un círculo
      de 20px ⇒ NO se sale, queda a 1,7px del borde. Apretado, pero dentro.
      🔴 Se sale con la ESCALA DE FUENTE DE ACCESIBILIDAD, que es función real
      de la app (`html.a11y-font-115/130/150/175`, app.css:702): el texto va en
      `rem` y CRECE, el `width: 20px` NO. Medido: 115% cabe raspando · **130%
      sobresale 0,7px · 150% sobresale 2,8px · 175% sobresale ~4,9px**.
      ⇒ El defecto no era "dos dígitos", era "dos dígitos + un usuario que
        necesita letra grande". Peor caso, y el que menos se prueba.
   ✅ Sin `width`: `min-width` vuelve a ser el suelo y la píldora crece sola,
      igual que la no-compacta del kiosko (25,1px con ×1 → 30,9px con ×20).
   🔒 CONTROL DE REGRESIÓN — el `padding` baja a .25rem A PROPÓSITO: con UN
      dígito el texto mide 11,3px y 11,3+8 = 19,3 < 20 ⇒ manda el `min-width` y
      el badge sigue midiendo EXACTAMENTE 20px. Todo lo que hay hoy en pantalla
      (×1..×9) pinta PIXEL-IDÉNTICO a antes de este lote; solo crece cuando de
      verdad hace falta. (Y el `border-radius: 10px` no cambia nada: con 20px de
      alto el navegador YA escalaba los 12px heredados a 10 — se escribe para
      que se lea, no para mover un pixel.)
   ⛔ DESCARTADO tras mirarlo (para que el siguiente no gaste el rato):
      `@media (max-width:480px) .kbs-wrap-phone .kbs-check` (app.css:5738) NO
      toca este badge — esa variante es del carrusel de BARBEROS del móvil, y
      las tarjetas de barbero no llevan cantidad.
   ✅ EL ROCE QUE AQUÍ SE DEJÓ ANOTADO YA ESTÁ ARREGLADO (misma corrida, más
      abajo: `white-space: nowrap` en `.kbs-stepper .n` + el gap del compacto en
      píxeles). Esta nota decía "175% + ×20 se aprieta 3,4px" y lo llamaba roce;
      VERA lo midió RENDERIZADO y no era un roce: el número se PARTÍA EN DOS
      LÍNEAS ("×2" con un "0" debajo), que es justo lo que R-400 prohíbe por
      escrito. 🔑 La lección, escrita para el siguiente: apretarse 3,4px y
      partirse en dos son el MISMO número y NO son el mismo defecto — medir el
      hueco no es medir el resultado. */
.kbs-compact .kbs-qty-badge { min-width: 20px; height: 20px; border-radius: 10px; top: 4px; right: 4px; font-size: .66rem; padding: 0 .25rem; }
/* 🔢 EL GAP DEL COMPACTO SALE DE `rem` Y PASA A PÍXELES — ÍTEM 980 · R-400.
   `nowrap` (arriba) garantiza que nunca haya DOS LÍNEAS; esto garantiza que
   QUEPA, que es otra pregunta y necesita su propia respuesta.
   🔬 El desajuste, medido: la banda útil de la tarjeta compacta son 100px
      FIJOS y los botones 21px FIJOS, pero el gap iba en `rem` ⇒ crecía con la
      escala de accesibilidad y le ROBABA sitio al número justo cuando el
      número también crecía. Hueco: 46,8px al 100% → 38,4px al 175%, mientras
      "×20" pasaba a pedir 39,6. El aire subía y el sitio bajaba.
   ✅ 5.6px es EXACTAMENTE lo que medía `.35rem` a la escala base ⇒ al 100%
      esto pinta PIXEL-IDÉNTICO a antes (regresión cero donde está el 99% de
      los usuarios). Con el gap fijo el hueco son 46,8px en TODAS las escalas.
   🧠 Y el criterio no es un parche: si los botones NO crecen con la fuente, su
      separación tampoco debe crecer. El que sí tiene que crecer es el número
      (`font-size` sigue en `rem`) — para eso existe la escala de accesibilidad.
   ⛔ La regla BASE (no compacta, `gap: .5rem`) NO se toca: ahí el hueco es de
      52px contra 39,6 que pide el texto, no hay fallo que arreglar, y son
      pantallas medidas al pixel en los ítems 313/315. */
.kbs-compact .kbs-stepper { gap: 5.6px; margin: .1rem 0 .05rem; }
.kbs-compact .kbs-stepper button { width: 21px; height: 21px; font-size: .78rem; }
.kbs-compact .kbs-stepper .n { min-width: 18px; font-size: .74rem; }

/* ══ ÍTEM 339 · frontera — la PISTA del carrusel empieza en una línea visible ══
   Va AQUÍ, al final de las variantes, porque `.kbs-shared > .kbs-scroller` y
   `.kbs-compact > .kbs-scroller` (arriba) escriben `padding` con el atajo y
   comparten especificidad (0,2,0) con `.kbs-card-pinned + .kbs-scroller`: más
   arriba, el `padding-left` se perdería sin avisar (trampa ④). El razonamiento
   del ítem está junto a `.kbs-card-pinned`.
   Solo aplica cuando HAY tarjeta anclada (hermano adyacente): la fila de chips
   de la Lista de Espera usa el mismo wrap SIN tarjeta fija y queda intacta.
   🎠 R-431 — EL KIOSCO Y EL CHECK-IN MÓVIL YA NO ENTRAN AQUÍ: al soltarse la
   tarjeta, el selector de hermano adyacente deja de casar solo, sin tocar nada.
   El separador desaparece de esas dos pantallas por construcción, que es lo que
   pide R-431. Sobrevive para `BarberCards` (POS / Lista de Espera / wizard de
   citas / StaffBookPanel) — ver la nota larga junto a `.kbs-card-pinned`. */
.kbs-card-pinned + .kbs-scroller {
    margin-left: 6px;
    border-left: 2px solid var(--control-edge, rgba(20, 34, 78, .55));
    padding-left: 10px;
}
/* En compacto (POS) todo mide menos: la línea se mantiene, el aire se ajusta. */
.kbs-compact .kbs-card-pinned { margin-right: 4px; }
.kbs-compact .kbs-card-pinned + .kbs-scroller { margin-left: 4px; padding-left: 7px; }
/* 📏 EN TELÉFONO EL AIRE SE PAGA CARO — medido, no supuesto. A 390px de ancho la
   PISTA que se desplaza mide solo 42px de `clientWidth` (dos flechas de 40px +
   la tarjeta anclada de 148px se comen la fila), o sea que de una tarjeta de
   148px se ve una rebanada. Los 20px que cuesta la frontera completa se notan
   justo donde menos sobra, así que por debajo de 480px se conserva **la línea**
   (que es lo que faltaba) y se recorta el colchón: 18px de separación con
   divisor visible, contra los 12px sin divisor de antes. Coste neto: 6px.
   ⚠️ Los 42px de pista NO los introduce este ítem — son de fábrica y merecen su
   propio arreglo (ver el hallazgo anotado en el ítem 339 del plan). */
@media (max-width: 480px) {
    .kbs-card-pinned { margin-right: 2px; }
    .kbs-card-pinned + .kbs-scroller { margin-left: 2px; padding-left: 6px; }
}

/* ══ ÍTEM 339 · HERMANO — la fila de chips de la Lista de Espera ═════════════
   Se detectó: "vi que pusieron un scroll bar que antes no estaba".
   `WaitingList.razor:90` traía TODO su layout en `style=` inline y, por eso,
   sin las dos reglas que ocultan la barra de scroll que el carrusel del kiosko
   sí tiene (`.kbs-scroller`, arriba). Dos carruseles de la misma app
   comportándose distinto — el meta-patrón del ítem 323.
   Se sube el inline a esta clase, con las MISMAS declaraciones (cero cambio de
   layout) más el par que oculta la barra. Un estilo inline gana a cualquier
   clase, así que el `style=` del razor se quitó: si vuelve, esta regla no
   sirve de nada. */
.wl-chips-row {
    display: flex; gap: .5rem; flex: 1 1 auto; min-width: 0;
    overflow-x: auto; scroll-snap-type: x mandatory; padding: 3px 2px 7px;
    scrollbar-width: none; -ms-overflow-style: none;
}
.wl-chips-row::-webkit-scrollbar { display: none; }

/* Nombre largo en un card estrecho: 2 lineas y elipsis — nunca desborda la
   banda ni empuja el precio fuera del card (regla de cero solapes del 249).
   SCOPEADO a .kbs-shared (lo emiten solo los componentes compartidos): las 2
   pantallas del kiosko acaban de medirse al pixel en los items 313/315 y no se
   tocan en esta corrida. */
.kbs-shared .kbs-name-band .nm {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden; overflow-wrap: anywhere;
}

/* ══════════════════════════════════════════════════════════════════════════
   ÍTEM 315 · NIVEL 2 — CONTROL SOBRE FONDO NO CONTROLADO ("la regla del PAR")
   Estándar de Kahlo: MBS\kahlo\KAHLO-ESTANDAR-CONTRASTE-CONTROLES.md (2026-07-25)

   EL DIAGNÓSTICO NO ERA LA OPACIDAD. `.kbs-arrow` y `.kbs-crumb-btn` ya
   renderizaban blanco 100% SÓLIDO (texto 15.34:1, el mejor de la pantalla) y aun
   así "no se ven": lo que fallaba era su LÍMITE contra la foto (1.22:1) con un
   borde #D8D4C8 que mide 1.00:1. Subirles el alpha no habría hecho NADA.

   LA REGLA: un control sobre fondo no controlado presenta SIEMPRE dos
   superficies adyacentes en extremos opuestos de la luminancia — una CLARA
   (porcelana L≈0.88) y una OSCURA (navy #14224E L≈0.027). El navy cubre todo
   fondo con L≥0.182 y la porcelana todo con L≤0.257: los rangos SE SOLAPAN, no
   queda hueco. Fotometría de Kahlo sobre 3,072,000 px de 5 fotos reales de
   tienda (pipeline real: cover 1024×600, object-position 50% 8%, velo .40):
       hoy (relleno 6% + #D8D4C8) ...  3.30% de píxeles pasan 3:1
       solo porcelana .90 ...........  17.54%
       solo borde blanco ............  42.21%
       solo borde navy ..............  99.52%
       PORCELANA + NAVY + HALO ...... 100.00%  ✅

   LA APP SE AUTO-VALIDA: los 2 elementos del kiosko de los que NUNCA se
   quejó son exactamente los 2 que ya llevaban borde navy — .kiosk-key (3.54:1)
   y .qr-holder (3.92:1). Por eso ninguno de los dos se toca aquí.

   CAUSA RAÍZ CRONOLÓGICA: el ítem 238-v2 (receta E-40, más arriba) ya resolvió
   esto el 7/20, pero lista los selectores POR NOMBRE; el ítem 282 creó la
   familia .kbs-* dos días después y no coincidía con ninguno — nació fuera de
   la receta. Esto la completa: mismo prefijo de hermano `.kiosk-photo-bg ~`,
   así que FLAG OFF = kiosko sin porcelana (y MobileCheckin/POS/booking jamás
   reciben porcelana sobre blanco).

   ⚠️ EL HALO ES `box-shadow` CON SPREAD, JAMÁS padding ni borde grueso (técnica
   E-40 v2): pinta FUERA del elemento sin ocupar espacio, así los controles
   quedan en el MISMO píxel y el teclado del RPi no vuelve a tapar el formulario
   (ítem 313 e-bis, botón Check In en y=369 — tiene que quedarse sobre y=384).
   ⚠️ HALO DE 2px, NO 3px: .kbs-cat-chips tiene gap:.4rem = 6.4px; con 3px por
   lado los halos se tocarían (6 de 6.4). Con 2px quedan 2.4px de aire.
   ⚠️ SIN backdrop-filter A PROPÓSITO: el ítem 223 lo apagó en el kiosko (RPi 4
   sin GPU) y, aunque fuera gratis, el blur PROMEDIA el fondo, no lo aleja — una
   pared blanca borrosa sigue siendo blanca.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
    --ovc-fill: rgba(247, 245, 240, .94);   /* relleno del control — texto navy 13.02:1 */
    --ovc-ink:  #14224E;                    /* tinta Y borde — la capa OSCURA del par   */
    --ovc-halo: rgba(247, 245, 240, .90);   /* halo exterior — MISMO valor de .kiosk-wash */
    --ovc-lift: 0 2px 8px rgba(20, 34, 78, .22);
}
/* over-photo control: el par completo, listo para reusar (ítem 240) */
.ovc { border: 1px solid var(--ovc-ink);
       box-shadow: 0 0 0 2px var(--ovc-halo), var(--ovc-lift); }
.ovc-light { background: var(--ovc-fill); color: var(--ovc-ink); }

/* Filas 1,2,3,4,6 de la tabla de Kahlo — porcelana .94 + borde navy + halo.
   SIN `:not(:hover)` a propósito (a diferencia de la receta E-40): aquellos eran
   `btn-outline-*` de Bootstrap, que SÍ tienen estilo de hover propio al que
   volver; estos NO tienen ninguno, así que el guard solo lograría que el control
   PERDIERA su borde y su halo justo mientras el dedo lo toca (en Chromium táctil
   el :hover se queda pegado tras el tap). */
.kiosk-photo-bg ~ .kiosk-wrap .kbs-crumb-btn,
.kiosk-photo-bg ~ .kiosk-wrap .kbs-step-pill,
.kiosk-photo-bg ~ .kiosk-wrap .kbs-who-badge,
.kiosk-photo-bg ~ .kiosk-wrap .kbs-chip:not(.active),
.kiosk-photo-bg ~ .kiosk-wrap .kbs-arrow {
    background: var(--ovc-fill);
    color: var(--ovc-ink);
    border: 1px solid var(--ovc-ink);
    box-shadow: 0 0 0 2px var(--ovc-halo), var(--ovc-lift);
}
/* Filas 5 y 7 — los ROJOS conservan su gradiente (es la marca) y ganan el par:
   rojo+blanco es un par MAL elegido (deja descubierto L 0.30–0.478, por eso la
   chip activa medía 1.40:1); con el borde navy + halo porcelana cierra a 3.54:1 */
.kiosk-photo-bg ~ .kiosk-wrap .kbs-chip.active,
.kiosk-photo-bg ~ .kiosk-wrap .kbs-cta {
    border-color: var(--ovc-ink);
    box-shadow: 0 0 0 2px var(--ovc-halo), var(--ovc-lift);
}
/* Fila 8 — "Total estimado" era texto (rojo) DIRECTO sobre la foto. La pastilla
   la emite Kiosk.razor solo con KioskWashOn (misma puerta que .kiosk-photo-bg);
   el <b> conserva su rojo de marca por especificidad (.kbs-total b) = 5.88:1
   sobre porcelana. Padding mínimo: .kbs-footer lo manda el CTA (min-height 56px),
   así que la pastilla NO cambia la altura del pie. */
.kbs-total-pill { display: inline-flex; align-items: center; gap: .25rem; flex-wrap: wrap;
    border-radius: 999px; padding: .1rem .55rem; }
/* Fila 9 · MEDIDO EN VIVO, mismo patrón de los ítems 249b/249c ("hacia adentro"):
   .kiosk-chip-wash trae anillos de 8px+9px, pensados para el paso del teléfono,
   donde SOBRA aire. En las pantallas kbs-* NO cabe: entre el borde inferior del
   halo de ← Volver (2px) y el tope del título quedan **1.59px**, así que un
   anillo de 9px se montaba 7.41px sobre la fila de migas (medido con los rects
   INFLADOS por el spread — box-shadow no entra en getBoundingClientRect).
   Se quita el COLCHÓN, no la superficie: el <span> ya mide 31px de alto, o sea
   su porcelana .90 cubre los glifos por completo y el contraste del texto no
   cambia ni un punto (Kahlo, Paso 3: "el ancho del halo no cambia el contraste,
   solo su color y alpha"). Queda la sombra de elevación para que despegue de la
   foto. Resultado: 1.59px de aire arriba y 7.16px abajo — CERO solape. */
.kbs-title-wash { box-shadow: 0 4px 14px rgba(20, 34, 78, .18); }
/* Los interactivos y sus halos, SIEMPRE por encima del fondo (un halo puede
   solapar la foto, jamás tapar un control vecino). Las chips no son `.btn`. */
.kiosk-photo-bg ~ .kiosk-wrap .kbs-chip { position: relative; z-index: 1; }

/* ══ ÍTEM 281 — LISTA DE ESPERA v2: chips de barbero en
   carrusel de una sola fila (14 barberos JAMÁS envuelven — el protagonismo es
   el grid de turnos), multi-select acumulable. Mismo lenguaje de carrusel del
   kiosko (arrows + swipe + drag). ══ */
.wl-chip-b {
    flex-shrink: 0; scroll-snap-align: start; display: flex; align-items: center; gap: .5rem;
    background: var(--surface-color, #fff); border: 2px solid var(--border-color, #D8D4C8); border-radius: 999px;
    padding: .35rem .9rem .35rem .35rem; font-size: .85rem; font-weight: 600; color: var(--primary-900, #14224E);
    white-space: nowrap; touch-action: manipulation;
}
.wl-chip-b.active { background: var(--primary-900, #14224E); border-color: var(--primary-900, #14224E); color: #fff; }
.wl-chip-b.all-chip { padding: .35rem 1rem; }
.wl-chip-b .avatar { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; flex-shrink: 0; object-position: 50% 15%; }
.wl-chip-b .avatar-mono { width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: .6rem; font-weight: 800; color: #fff; background: var(--primary-900, #14224E); }
.wl-chip-b .avatar-any { width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: .85rem; background: rgba(20,34,78,.10); }
.wl-chip-b.active .avatar-any { background: rgba(255,255,255,.18); }
.wl-chip-b .eta { font-weight: 800; }
.wl-chip-b .eta.g { color: #178a3e; } .wl-chip-b .eta.y { color: #a9720a; } .wl-chip-b .eta.r { color: #c8102e; }

/* ══ ⏸️ R-428(b)(c) — EL CHIP DICE EL ESTADO Y DEJA AYUDAR ════════════════════
   El chip se parte en DOS controles dentro de un envoltorio: el cuerpo sigue
   siendo el FILTRO de siempre (ítem 281) y al lado nace el botón de
   apagar/encender la disponibilidad de ESE barbero (*"deben poder ayudarse"*).
   Un <button> dentro de otro <button> no es HTML válido — de ahí el <span>.
   El envoltorio hereda el `flex-shrink:0` y el snap que antes llevaba el chip:
   con 17 sillas el carrusel no puede empezar a encoger piezas. */
/* ⚪ N-8 (Lupe, vuelta 9) — EL BLANCO TACTIL DEL PAUSA SE COMIA 3 px DEL CHIP.
   Medido: el ::after de 44 px sobre un boton de 30 px sobresale 7 px por lado;
   con el gap de .25rem (4 px) invadia 3 px del filtro ⇒ un toque en el canto
   derecho del chip APAGABA al barbero en vez de filtrar. Los 3 px son el 2.5%
   del chip, pero el error caro va en la direccion mala (apagar pesa; filtrar
   no). El gap sube a .5rem (8 px): el blanco tactil sigue siendo 44x44 COMPLETO
   (no se recorta el dedo) y queda 1 px de aire por cada lado — el gap de la
   fila entre envoltorios ya era .5rem, asi que por la derecha tampoco invade al
   chip siguiente. Coste: 4 px mas por chip en un carrusel que ya scrollea. */
.wl-chip-wrap {
    display: inline-flex; align-items: center; gap: .5rem;
    flex-shrink: 0; scroll-snap-align: start;
}
/* ⚪ N-11 (Lupe, vuelta 9) — EL NOMBRE ATENUADO TENIA QUE SEGUIR LEYENDOSE.
   Medido por Lupe: `opacity .5` + `grayscale(1)` compone el navy rgb(20,34,78)
   a ~rgb(145,145,145) sobre blanco = 3.15:1, bajo el 4.5:1 de AA. El atenuado
   es DELIBERADO (R-429) y su razon de negocio es justo esa: que se siga viendo
   QUIEN es. Asi que se atenua donde no cuesta legibilidad — la FOTO — y el
   texto sube a .78 (~7.7:1 con el mismo grayscale). El chip se sigue leyendo
   "apagado" a un metro: sin color, foto lavada y su ⏸️ al lado. */
.wl-chip-wrap.off .wl-chip-b { opacity: .78; filter: grayscale(1); }
.wl-chip-wrap.off .wl-chip-b .avatar,
.wl-chip-wrap.off .wl-chip-b .avatar-mono,
.wl-chip-wrap.off .wl-chip-b .avatar-any { opacity: .55; }
/* El gris del rotulo tambien se compone: #6b7280 caia a 2.5:1 con el atenuado.
   #374151 (mismo tono, dos pasos mas oscuro) da ~5.4:1 y sigue leyendose
   secundario frente al nombre en negrita. */
.wl-chip-off-tag { color: #374151; font-weight: 800; }
.wl-chip-pause {
    position: relative; flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    border: 2px solid var(--border-color, #D8D4C8); background: var(--surface-color, #fff);
    color: var(--primary-900, #14224E); font-size: .72rem; padding: 0;
    touch-action: manipulation; cursor: pointer;
}
/* 👆 DEDO DE 44px SIN CRECER EL CHIP: el botón se DIBUJA de 30px (un círculo de
   44 haría el chip más alto que el avatar y con 17 sillas el carrusel dejaría de
   caber — que es justo lo que el ítem 637 vino a arreglar), pero su BLANCO
   TÁCTIL es de 44×44 gracias a este pseudo-elemento. Es la misma superficie que
   `bsp-touch44` garantiza, resuelta sin tocar la geometría. */
.wl-chip-pause::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
}
.wl-chip-pause:hover { border-color: var(--primary-900, #14224E); }
/* Encendido = verde (la acción es "vuelve a recibir gente"), apagado = navy. */
.wl-chip-pause.is-off { border-color: #178a3e; color: #178a3e; background: rgba(23,138,62,.10); }

/* ══ ⏸️ EL AVISO GRANDE — "NO ESTÁS RECIBIENDO WALK-INS" ═════════════════════
   🔴 A PROPÓSITO NO ES UN SWITCH GRIS DISCRETO (R-428, verbatim: *"aviso VISIBLE
   mientras está apagado… nunca un switch gris que se olvida"*). Un barbero que
   pasa el día apagado por olvido pierde el día entero de walk-ins y la culpa se
   la lleva la app. Por eso: barra ámbar, ícono grande y el texto en negrita.
   El switch en sí vive SIEMPRE (encendido o apagado); lo que aparece y
   desaparece es la barra. */
/* 📍 La fila del mockup: el switch A LA IZQUIERDA del botón de registrar. En el
   teléfono envuelve y el switch queda ENCIMA — el CTA nunca sale del pulgar. */
.wl-avail-row { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
/* 🚨 EL DUEÑO EN SU CELULAR, EN PRODUCCIÓN (2026-08-25) — EL BOTÓN SE SALÍA DE
   LA PANTALLA: *"...en la lista de esper"* cortado por el borde derecho.
   🔎 LA CAUSA, MEDIDA: `flex-shrink: 0` clavaba el botón a su ancho de UNA
   LÍNEA (max-content). A 390px la fila mide 308 y el botón medía **352** ⇒
   `right = 393 > 390`. La etiqueta nunca envolvía porque la caja nunca tenía
   por qué estrecharse.
   🔧 EL ARREGLO ES EL QUE PIDIÓ EL DUEÑO — *"¿no se podía poner como el de
   arriba que no se sale de la pantalla?"*: la tarjeta `.wl-avail` de justo
   encima envuelve su texto porque es un bloque con `max-width`. Aquí se le da
   al botón lo mismo: puede encoger, no pasa del ancho disponible, y la
   ETIQUETA ENVUELVE en dos líneas. **No se achica la letra** (sigue en
   1.05rem) y el alto mínimo de 56px de `.kbs-cta` manda ⇒ la ley del dedo
   intacta; envolver solo lo hace más alto, nunca más chico.
   📕 LA LECCIÓN QUE VA AL MANUAL (la vuelta 9 midió "sin scroll horizontal" y
   dio esto por bueno): **`scrollWidth == clientWidth` del DOCUMENTO no prueba
   que cada control quepa.** Un hijo puede desbordar su caja sin empujar el
   `body`. La prueba que vale es por CONTROL:
   `el.getBoundingClientRect().right <= window.innerWidth`. */
.wl-avail-row .kbs-cta {
    flex: 0 1 auto;          /* antes `flex-shrink: 0` — esa era la línea del bug */
    max-width: 100%;
    white-space: normal;     /* la etiqueta envuelve en vez de empujar la caja */
    text-align: center;
    line-height: 1.25;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.wl-avail {
    display: flex; flex-direction: column; gap: .5rem;
    padding: .6rem .85rem; border-radius: 12px;
    border: 2px solid var(--border-color, #D8D4C8); background: var(--surface-color, #fff);
    max-width: 640px;
}
.wl-avail.is-off { border-color: #b45309; background: #fff8ec; }
.wl-avail-banner {
    display: flex; align-items: center; gap: .6rem;
    padding: .5rem .7rem; border-radius: 10px;
    background: #b45309; color: #fff;
}
.wl-avail-icon { font-size: 1.5rem; line-height: 1; }
.wl-avail-hd { display: block; font-size: 1.05rem; font-weight: 800; line-height: 1.2; }
.wl-avail-sub { display: block; font-size: .78rem; opacity: .95; margin-top: .1rem; }
@media (max-width: 480px) {
    .wl-avail-hd { font-size: .98rem; }
}

/* ══ ÍTEM 637 — EL CHIP APILA: nombre arriba, tiempo debajo, sin la raya `—` ══
   (el dueño 2026-08-07) Verbatim: "esto está cool pero como que toma mucho espacio
   ... no veo a todos los barberos ... poniendo el tiempo de espera DEBAJO del
   nombre ... no necesitaríamos la raya esa `—`".

   📏 POR QUÉ NO CRECE DE ALTO (que era el riesgo real en el teléfono): el chip ya
   medía 41 px por el AVATAR (26 px + 11.2 de padding + 4 de borde). El texto
   apilado (nombre 1.15 + tiempo .72rem) mide ~27 px, o sea CABE dentro de ese
   mismo alto. Medido: el chip sigue en 41 px en las tres pantallas (1920, 390×844
   y 844×390) ⇒ la fila del carrusel NO crece y NO empuja la cola de clientes ni
   un pixel. Por eso este cambio NO necesita media query: gana ancho en escritorio
   sin costar alto en el móvil.

   👆 TÁCTIL: el chip se toca en el celular y el alto de 41 px lo pone el avatar,
   no este bloque — no se toca aquí (ver la nota del informe del lote 636/637).

   🚦 EL SEMÁFORO NO SE PIERDE: el tiempo conserva su <span class="eta g|y|r">, así
   que las reglas de color de arriba siguen aplicando tal cual — solo cambia de
   renglón. En el chip ACTIVO el color pasa a blanco por la regla de abajo, igual
   que antes.
   ⚖️ "Todos" y el 🎲 (que no tienen tiempo) no quedan descuadrados: `.wl-chips-row`
   es flex con `align-items: stretch` por defecto, así que TODOS los chips igualan
   el alto del más alto — verificado midiendo (los 21 chips en 41 px). */
.wl-chip-b .wl-chip-txt {
    display: flex; flex-direction: column; align-items: flex-start;
    justify-content: center; min-width: 0; line-height: 1.15;
}
.wl-chip-b .wl-chip-txt .nm { overflow: hidden; text-overflow: ellipsis; }
.wl-chip-b .wl-chip-txt .eta { font-size: .72rem; line-height: 1.15; }
.wl-chip-b.active .eta.g, .wl-chip-b.active .eta.y, .wl-chip-b.active .eta.r { color: #fff; }
.wl-kpi-narrow { flex: 0 0 auto !important; min-width: 104px; }

/* Modal Atender/Notificar (ítem 283b) */
.wl-notify-actions button { flex: 1 1 160px; }

/* Item 43: display de foto en los edit +25%+ (56 -> 72px) */
/* ÍTEM 280: preview de foto en Employee/Store/Company edit — sesgo hacia arriba
   (Employee es SIEMPRE persona; Store/Company no sufren con el mismo sesgo leve) */
.edit-title-photo { width: 72px; height: 72px; object-fit: cover; object-position: 50% 18%; border-radius: 12px; border: 2px solid var(--border-color, rgba(0,0,0,.1)); }

/* == Cropper de fotos con rotacion (portado de BCP, 2026-07-04) == */
#bsp-cropper-overlay {
    position: fixed; inset: 0; z-index: 1080;
    background: rgba(0, 0, 0, 0.85);
    display: none; align-items: center; justify-content: center;
}
.bsp-cropper-box { display: flex; flex-direction: column; width: 100%; max-width: 640px; height: 100%; max-height: 100vh; padding: 1rem; }
.bsp-cropper-hint { color: #fff; text-align: center; margin-bottom: .5rem; font-weight: 600; }
.bsp-cropper-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.bsp-cropper-stage img { max-width: 100%; max-height: 100%; display: block; }
.bsp-cropper-actions { display: flex; gap: .5rem; justify-content: center; padding-top: 1rem; }
.bsp-cropper-actions .btn { flex: 1; max-width: 160px; }
.bsp-cropper-actions .bsp-cropper-rotate { flex: 0 0 auto; max-width: 52px; font-size: 1.25rem; }

/* == Lapiz de editar foto == */
.photo-edit-wrap { position: relative; display: inline-block; }
.photo-edit-pencil {
    position: absolute; right: -7px; bottom: -7px;
    width: 26px; height: 26px; border-radius: 50%; padding: 0;
    background: var(--accent-color, #C8102E); color: #fff;
    border: 2px solid #fff; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-size: .72rem; box-shadow: 0 1px 4px rgba(0,0,0,.35);
}
.photo-edit-pencil:hover { filter: brightness(1.15); }
.photo-edit-thumb {
    /* Item 43/62: 56 -> 72px */
    width: 72px; height: 72px; object-fit: cover; border-radius: 12px;
    border: 2px solid var(--border-color, rgba(0,0,0,.1));
    display: flex; align-items: center; justify-content: center;
}
.photo-edit-empty { background: var(--icon-chip-bg, #F1F5F9); color: #94A3B8; font-size: 1.3rem; }

/* == Localidades == */
/* Item 62: 72 -> 96px (escala md) */
/* ÍTEM 206 (2026-07-18): un retrato vertical salía con la CABEZA CORTADA —
   cover centrado en un cuadro recorta arriba y abajo por igual, y en un
   retrato vertical la cabeza (tercio superior) se va. El ancla 50% 25% sesga
   el encuadre hacia arriba (donde vive la cara) sin perder el cover: vertical
   conserva la cabeza, horizontal/cuadrada casi no cambian. Mismo tratamiento
   de cover de los selectores de barbero/servicio del wizard. */
.loc-photo { width: 96px; height: 96px; object-fit: cover; object-position: 50% 25%; border-radius: 14px; flex-shrink: 0; }
.loc-photo-empty { display: flex; align-items: center; justify-content: center; background: var(--icon-chip-bg, #F1F5F9); color: #94A3B8; font-size: 1.6rem; }
.loc-card { border: 1px solid var(--border-color, rgba(0,0,0,.08)); }

/* == Paginacion estandar: Rows per page abajo == */
.bsp-pager { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-top: .5rem; }
.bsp-pager .rows-per-page { display: flex; align-items: center; gap: .5rem; font-size: .85rem; color: var(--bs-secondary-color, #6c757d); }
.bsp-pager .rows-per-page select { width: auto; }

/* Encabezado de columna ORDENABLE para tablas manuales (item 9) */
.bsp-sort-header { background: none; border: 0; padding: 0; font: inherit; font-weight: 600;
    color: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: .35rem; }
.bsp-sort-header:hover { color: var(--accent-color, #2563EB); }
.bsp-sort-header .bsp-sort-idle { opacity: .35; font-size: .8em; }

/* Modal centrado simple para el borrado de usuario (item 2) */
.modal-backdrop-bsp { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 1050; }
.bsp-del-modal { position: fixed; z-index: 1055; top: 50%; left: 50%;
    transform: translate(-50%, -50%); width: min(560px, 94vw);
    max-height: 90vh; overflow: auto; }

/* == Agenda: foto del barbero en el header == */
/* ÍTEM 280: círculo de barbero — sesgo hacia arriba (misma regla, cara nunca cortada) */
.sch-barber-photo { width: 34px; height: 34px; object-fit: cover; object-position: 50% 15%; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; }
.sch-barber-photo-empty { background: var(--icon-chip-bg, #F1F5F9); color: #94A3B8; font-size: 1rem; }

/* == Dropdown con foto (Reservar cita) == */
.bsp-photo-dd { position: relative; }
.bsp-photo-dd-menu {
    /* Item 57: items mas altos -> menu un poco mas alto, scroll interno igual */
    position: absolute; z-index: 30; width: 100%; max-height: 420px; overflow: auto;
    background: var(--bs-body-bg, #fff); border: 1px solid rgba(0,0,0,.15); border-radius: 8px;
}
.bsp-photo-dd-item {
    /* Item 57: items ~50% MAS ALTOS para que la foto se
       aprecie — foto 32->48px + padding vertical .45->.65rem */
    display: flex; align-items: center; gap: .6rem; width: 100%;
    padding: .65rem .75rem; border: 0; background: transparent; text-align: left;
}
.bsp-photo-dd-item:hover { background: rgba(13,110,253,.08); }
.bsp-dd-photo { width: 48px; height: 48px; object-fit: cover; border-radius: 10px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; }
/* ÍTEM 280 (BARRIDO): -round SIEMPRE es foto de PERSONA (barbero) — sesgo
   hacia arriba, mismo tratamiento del kiosko/TV. La variante cuadrada (tienda/
   servicio) no se toca. */
.bsp-dd-photo-round { border-radius: 50%; object-position: 50% 15%; }
.bsp-dd-photo-empty { background: var(--icon-chip-bg, #F1F5F9); color: #94A3B8; }

/* == Cuadre del Dia: al IMPRIMIR salen ambos tabs == */
@media print {
    .dc-section { display: block !important; }
}

/* == Filtros elasticos: se encogen para caber en UNA linea ==
   ÍTEM 270 cosmético 14b (Lupe): el <select> recortaba "Todas las tiendas"/
   "Todas las compañías" en ES a 390px (el ancho no alcanzaba para el texto + la
   flecha nativa). min-width 0 permite encoger sin overflow horizontal; el
   max-width sube a 200px para que la etiqueta ES más larga quepa completa. */
.bsp-filter { flex: 1 1 140px; min-width: 0; max-width: 200px; width: auto; text-overflow: ellipsis; }

/* == Encuesta publica: estrellas 1-5 == */
.sv-stars { display: flex; gap: .25rem; }
.sv-star {
    background: none; border: none; font-size: 1.9rem; line-height: 1;
    color: #CBD5E1; cursor: pointer; padding: 0 .15rem; transition: color .1s, transform .1s;
}
.sv-star.on { color: #F59E0B; }
.sv-star:hover { transform: scale(1.15); }

/* == ITEM 353: /encuesta - las 5 estrellas VACIAS, decorativas ==
   Van vacias (no rellenas) a propósito: "para no predisponer al
   cliente". No son pulsables: la escala se ensena aqui, se valora en la
   pantalla siguiente. */
.enc-stars {
    font-size: 1.7rem; line-height: 1; letter-spacing: .35rem;
    color: #B8860B; margin: .35rem 0 .5rem; text-align: center;
}

/* == TV BOARD: 1/4 cola + 3/4 anuncios == */
.tv-root { position: fixed; inset: 0; background: #0B1220; color: #fff; z-index: 1000; overflow: hidden; }
/* ══ ÍTEM 1134b — LA RED DE SEGURIDAD SERVER-RENDERED (ver TvLayout.razor
   para el porqué completo). VISIBLE POR DEFECTO — el `<div>` vive en el HTML
   crudo desde el primer byte, así que "por defecto visible" es exactamente
   lo que hace falta mientras el circuito de Blazor todavía no confirmó nada.
   `position:fixed;inset:0` PROPIO (no depende de ser el último hijo de
   `.tv-root`): cubre la pantalla entera pase lo que pase con `@Body`.
   `z-index` por encima de cualquier cosa dentro de `.tv-root` (el tope local
   medido hoy es 3, en `.tv-turn-call-inner`) pero DEBAJO del `<dialog>` de
   reconexión nativo (los `<dialog>` con `::backdrop` van en su propia capa
   por encima de cualquier z-index de documento) — si el módulo SÍ carga y
   el modal real se abre, éste manda; si no carga, esta capa es todo lo que
   hay, y basta. */
.tv-boot-wait {
    position: fixed; inset: 0; z-index: 500;
    display: flex; align-items: center; justify-content: center;
    background: #0B1220; color: #fff;
}
.tv-boot-wait-inner { display: flex; flex-direction: column; align-items: center; gap: 1.2rem; }
.tv-boot-wait-pole { width: 4rem; height: auto; opacity: .92; }
.tv-boot-wait-text {
    font-size: clamp(1.1rem, 2vh, 1.8rem); font-weight: 600; text-align: center;
    max-width: 80vw; opacity: .92; margin: 0;
}
/* La CLASE la pone/quita `bsp-circuit.js` (script CLÁSICO, nunca `type=module`
   — ver el comentario de TvLayout.razor) sobre `<html>`, nunca sobre
   `.tv-root`: así un `<html>` sin la clase (JS roto de punta a punta, ni
   siquiera bsp-circuit.js corrió) dice la verdad — "no sé si esto está
   vivo" — y la pantalla de espera se queda puesta para siempre en vez de
   arriesgar un `.tv-root` vacío. */
html.bsp-tv-live .tv-boot-wait { display: none; }
.tv-pick { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; }
/* ✅ ÍTEM 1088 CERRADO (Jack, 2026-09-03) — esta línea empezó como el PARCHE
   de Tito para el bug real: `theme-theshop.css:583` traía
   `h1,h2,h3{color:var(--primary-900)}` GLOBAL y le ganaba a la herencia del
   blanco de `.tv-root` (navy sobre navy, contraste 1.22:1 — WCAG exige
   4.5:1). Pintaba el picker de tienda de TvBoard/TvMenu ("Elige tu tienda")
   y el aviso de token inválido de TvPublic (los tres usan `.tv-pick h3`).
   LA CAUSA YA ESTÁ ARREGLADA EN LA RAÍZ (`theme-theshop.css`, la regla
   ahora es `main h1,h2,h3` / `.print-root h1,h2,h3` — NO llega a `.tv-root`)
   ⇒ esta línea quedó REDUNDANTE, no necesaria: el h3 de `.tv-pick` ya
   hereda blanco de `.tv-root` sin ayuda. Se deja de todos modos como
   refuerzo explícito y barato (evita que un h3 nuevo aquí dependa de la
   herencia si algún día alguien mete otra regla de color de por medio) —
   pero si la borras, el picker NO se rompe: ya no es el único blindaje. */
.tv-pick h3 { color: #fff; }
.tv-board { display: flex; height: 100%; }
/* ══ ÍTEM 610 (el socio + el dueño sobre el TV 4K, 2026-08-05) — CUÁNTA GENTE SE VE
      ES CONSTANTE; LO QUE CRECE ES LA LETRA ════════════════════════════════════
   > *"la lista a la izquerda casi no se ve, que font tenemos en esa lista? si es
   >  posible lo podemos agrandar para que se vea a distancia?"*
   > Y al enseñarle cuánta gente cabía en cada pantalla:
   > *"imaginate, yo creo que deberiamos poder mostrar 13 en el 4k tambien"*
   🩸 EL DIAGNÓSTICO BUENO NO ERA "LA LETRA NO ESCALA", ERA ESTE — y cambia la
   solución entera: MEDIDO, a 1080p cabían **11 nombres** y en 4K **27**. En 4K se
   estaban metiendo 27 filas donde tienen que ir 11. **Por eso salían diminutas.**
   ✅ ENTONCES LO QUE SE FIJA ES LA CUENTA DE NOMBRES, NO EL TAMAÑO: todo lo que
   ocupa alto en esta columna (letras, rellenos, huecos, la foto) se mide en `vh`.
   El alto de la columna es el de la pantalla ⇒ en 4K cada fila mide el doble ⇒ **la
   letra dobla sola y siguen cabiendo los mismos nombres.**
   🔑 POR QUÉ AL ALTO Y NO AL ANCHO (`vw`, que fue el primer intento y se descartó):
   la restricción REAL de esta columna es VERTICAL — cuánta gente cabe. Atar la letra
   al ancho deja la cuenta de filas a la deriva; atarla al alto la clava.
   🔒 EL CONTROL DE REGRESIÓN, Y ES LA RAZÓN DE CADA `clamp`: el mínimo es el tamaño
   EXACTO de hoy, y la recta en `vh` pasa por ese mismo valor a 1080 px de alto ⇒
   **en 1080p no cambia NI UN PÍXEL** (misma letra, mismas 11 filas), y en pantallas
   más BAJAS que 1080 (tableta, laptop) manda el mínimo y tampoco encoge nada.
   Crece solo hacia arriba. 📖 Las cuentas medidas: ítem 610 del plan.
   ⚠️ LA FOTO Y EL RÓTULO TAMBIÉN VAN EN `vh`, y no es por estética: comen ALTO. Si
   se quedaran fijos, en 4K liberarían hueco para filas de más y la cuenta de nombres
   volvería a descuadrarse. Esta columna escala como UN bloque o no escala. */
/* 📺 c= (mini-lote #12): el 25% de siempre ahora es `--tvcw` — la escalera
   tv-c0..tv-c5 (al final del archivo, junto a f= y t=) lo mueve por link.
   Sin ?c= (o basura) queda en su default 25%: pixel-idéntico. El min-width
   260px se respeta SIEMPRE. */
.tv-queue {
    width: var(--tvcw, 25%); min-width: 260px; display: flex; flex-direction: column;
    background: linear-gradient(180deg, #14224E 0%, #0B1220 100%);
    border-right: 4px solid #C8102E; padding: clamp(1.2rem, 1.778vh, 2.4rem);
}
.tv-store-name {
    /* ÍTEM 1154 (Héctor, 2026-09-04 21:30: "el titulo de la barberia
       definitivamente tiene que ir mas grande"): el tamaño vive en
       `--tv-store-name-fs` — el VALOR lo fija Kahlo (4) con su método de
       legibilidad a 3 m. ══ LV-59 (Héctor, 22:37 EDT: "ok sigo la
       recomendacion" = ×2, acta 1154 v3 de Kahlo): `--tv-store-name-size:
       4.148vh` (×2 de los 2.074vh de siempre; 44.8 px a 1080p = 23,7′ de arco
       a 3 m en 55″, 21,0′ a 4 m en 65″, sobre el suelo de 16′; hoy medía 11,8′).
       ⚠️ EL MÍNIMO DEL CLAMP SE QUEDA EN 1.4rem — Kahlo midió que con 2.8rem
       el nombre pasa a 5.83vh a 768p y «Executive»/«Alternatives» DESBORDAN
       la columna (4/57 px); con 1.4rem manda el vh (31.9 px) y todo cabe. El
       máximo se dobla (5.6rem) para que por encima de 1080 siga mandando el
       vh (ítem 610). La otra variable del ítem es `--tv-menu-title-size`
       (TvMenuPanel.razor.css, título de la banda; y `--m-shopfs` ×2 para el
       nombre a la derecha de la banda). R-466: el nombre va TAL CUAL, un solo
       color, y envuelve en su corte natural — sin nowrap ni ellipsis. */
    --tv-store-name-size: 4.148vh;
    font-size: clamp(1.4rem, var(--tv-store-name-size), 5.6rem);
    font-weight: 700; margin-bottom: clamp(.4rem, .593vh, .8rem);
}
/* ÍTEM 1136 — discreto A PROPÓSITO: no es un botón, nadie lo toca; solo
   informa por qué ESTE aparato no suena. Vive en la franja de la izquierda,
   opacidad baja, tamaño de glifo pequeño — nunca compite con la cola. */
.tv-mute-badge {
    display: inline-block; font-size: clamp(1rem, 1.5vh, 1.8rem);
    opacity: .55; margin-bottom: clamp(.4rem, .593vh, .8rem);
}
/* ÍTEM 239 (estándar "pantallas personalizadas"): FOTO de la
   barbería junto al ícono y el título del TV board — compacta, nunca roba la cola */
/* ÍTEM 280 (2026-07-22, "un retrato vertical salió sin cabeza en el TV board"): mismo
   tratamiento focal del kiosko (238) — sesgo hacia ARRIBA, la cara vive en el
   tercio superior de la foto. Regla de la casa: LAS CARAS JAMÁS SE CORTAN. */
/* ══ 📺 LOTE TV — LA FOTO DE LA FRANJA, CUADRADA Y COMPLETA (el dueño 2026-08-22,
   viendo su tienda en prod): *"la foto en el rectángulo no se ve bien… vamos a
   tener que ponerla en un cuadrado para que salga la foto completa, aunque
   ocupe espacio de la lista."* El recorte rectangular con `cover` se quedaba
   con el toldo y cortaba los pósteres y la puerta — perdía justo lo que
   identifica al negocio.
   · CONTENEDOR CUADRADO (`aspect-ratio: 1/1`) + `contain`: la foto ENTERA,
     con franjas finas arriba/abajo si no es cuadrada — invisibles sobre el
     fondo navy de la franja. ⛔ NO `cover`: cover en un cuadrado sigue
     cortando los lados.
   · El `object-position` del ítem 280 (sesgo a la cara) queda sin efecto con
     `contain` (no hay recorte del que proteger una cara) — se retira aquí y
     se queda documentado en el kiosco, donde sigue habiendo cover.
   · ✅ El espacio que le roba a la lista NO es problema: la paginación del 611
     MIDE cuántas filas caben y reparte sola (verificado en vivo por el dueño y
     el socio) — menos alto = más páginas, cero código extra.
   · `max-height: 38vh` = el seguro: en una pantalla más alta que ancha el
     cuadrado a lo ancho se comería la columna entera; con el tope, `contain`
     sigue enseñando la foto completa dentro de lo que quepa. */
/* ══ 🖼️ R-420 lote (ojo de diseño del dueño, 2026-08-24) — ESQUINAS CONCÉNTRICAS
   El hallazgo: la foto vivía en un marco redondeado pero SUS esquinas salían
   RECTAS — porque el <img> era una caja cuadrada forzada (`aspect-ratio:1/1` +
   `contain`) y una foto no exactamente cuadrada quedaba letterboxed: el dibujo
   no tocaba el borde redondo, así que el radio nunca lo recortaba.
   El arreglo es el patrón clásico de esquinas anidadas, con DOS elementos:
   · `.tv-store-photo-frame` = el MARCO: borde de 2px + radio EXTERIOR 12px +
     4px de aire. `width: fit-content` para que ABRACE la foto (adiós franjas
     navy: la foto sigue saliendo ENTERA — la orden del 2026-08-22 intacta —
     solo que ahora el marco mide lo que mide ella).
   · `.tv-store-photo` = la FOTO a su aspecto natural, con radio INTERIOR
     6px = 12 exterior − (2 borde + 4 aire). Concéntricas de verdad.
   `max-height: 38vh` sigue siendo el seguro de la columna (una foto vertical
   se limita en alto y el ancho la sigue en proporción — regla de reemplazados
   de CSS, sin recortar nada). */
/* 📺 t= v3 (el dueño 2026-08-24, mini-lote — SU mockup): t = % del TAMAÑO
   ORIGINAL de la foto, vía `zoom` (--tvz) + tope de columna. La escalera
   tv-t0..tv-t4 vive al final del archivo, junto a la del f=. Sin ?t= (o
   basura): --tvz 1 + --tvt 38vh = el tamaño de HOY, pixel-idéntico.
   ⚠️ MEDIDO, NO SUPUESTO (humo en Chromium, las 3 mediciones en el informe
   del mini-lote): `zoom` multiplica el tamaño INTRÍNSECO (eso es lo que
   queremos: 25% del original) y TAMBIÉN los largos ABSOLUTOS del elemento
   (px y vh de max-height/border-radius: el tope de 62vh se volvía 15.5vh a
   t=1 — foto ancha en 298px en vez de sus 400, medido) — PERO los
   PORCENTAJES resuelven contra el contenedor SIN zoom (reales). ⇒ los topes
   absolutos se DIVIDEN por --tvz para valer su medida real ya zoomeados
   (max-height 62vh de verdad, radio interior 6px concéntrico) y el
   max-width se queda en 100% a secas (capea al ancho real de la columna —
   con el calc dividido quedaba 200% y la foto DESBORDABA, medido).
   A t=0 (--tvz:1) la división es /1: pixel-idéntico a hoy. */
.tv-store-photo-frame { width: fit-content; max-width: 100%; margin: 0 auto clamp(.55rem, .815vh, 1.1rem);
    padding: 4px; border: 2px solid rgba(255, 255, 255, .18); border-radius: 12px; line-height: 0; }
.tv-store-photo { max-width: 100%; max-height: calc(var(--tvt, 38vh) / var(--tvz, 1));
    width: auto; height: auto;
    zoom: var(--tvz, 1);
    border-radius: calc(6px / var(--tvz, 1)); /* interior efectivo = 12 − (2 + 4) */ }
.tv-queue-title { text-transform: uppercase; letter-spacing: .15em; font-size: clamp(.85rem, 1.259vh, 1.7rem); color: #93C5FD; margin-bottom: clamp(1rem, 1.481vh, 2rem); }
/* ══ ÍTEM 619 (el dueño 2026-08-06) — DOS LISTAS QUE ROTAN EN LA MISMA COLUMNA ═══
   Cada lista vive en su SECCIÓN (rótulo + filas) y el navegador enseña UNA a la
   vez, con el mismo mecanismo del 611.
   🔴 LA SEGUNDA SECCIÓN NACE OCULTA, Y NO ES COSMÉTICA: el JS que reparte las
   páginas viaja por el circuito de Blazor (SignalR) y tarda un viaje de ida y
   vuelta. Sin esta regla, el televisor pintaría UN FOTOGRAMA con las dos listas
   a media altura cada vez que se re-renderiza la columna — el "flash" que en un
   55" se ve como un fallo. Con ella, lo primero y lo último que se ve es una
   lista completa.
   🔒 Y ES EL SEGURO SI EL JS NO CORRE: sin interop se queda la primera sección
   sola, o sea EXACTAMENTE el televisor de antes de este ítem. Nunca en blanco.
   ⚠️ Por eso el JS pone `display:flex`/`none` EXPLÍCITOS y jamás '' (un '' lo
   devolvería a esta regla y la página de citas no aparecería nunca). */
.tv-queue-sec { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.tv-queue-sec ~ .tv-queue-sec { display: none; }
/* ══ 🟢 R-313 · LA LETRA DE LA LISTA SUBE AL "PASO 6" (el dueño 2026-08-06) ══════
   Verbatim: *"ok llegamos a la conclusión que se ve bien la línea 6"* → *"vamos a
   aprobar ese font."* Lo decidió mirando **el televisor 4K del socio a 3840×2160
   a pantalla completa**, con una escalera de 7 tamaños. Escogió el 6.
   **FACTOR ×1,5610** (2.891vh ÷ 1.852vh) aplicado a TODA la fila — letra, huecos,
   relleno y radios — para que la fila crezca como UN bloque.

   🪤 LA TRAMPA QUE HAY QUE CONOCER ANTES DE TOCAR ESTO — MEDIDA, NO SUPUESTA:
   a 3840×2160 estas clases estaban **EXACTAMENTE EN SU TECHO**. `1.852vh` × 2160
   = 40,00 px y el techo era `2.5rem` = 40,00 px. `clamp()` pinta el MENOR de los
   dos últimos ⇒ **subir solo el `vh` habría dejado el televisor del barbero IGUAL.**
   🔴 Y lo venenoso: en una laptop de 1080p SÍ se habría visto el cambio (ahí el
   `vh` aún no llega al techo), así que quien lo hiciera lo daría por bueno
   mirando su pantalla y en el televisor del cliente no habría pasado nada.
   ⇒ **SE SUBE EL TECHO TAMBIÉN, Y SE COMPRUEBA A 2160, NO A 1080.**

   ⚖️ POR QUÉ ×1,561 A TODO Y NO LAS PROPORCIONES DE LA REGLA DE MEDIR — decidido,
   no reabrir: el detalle *"con X a las…"* es **0,840** del nombre en la app y
   **0,680** en el instrumento con que se midió. El dueño juzgó **el tamaño del
   NOMBRE desde lejos** (*"es una regla de medir, no una presentación"*); las
   proporciones internas de la fila YA SON DISEÑO APROBADO. Copiar el 0,680
   metería en producción un detalle del instrumento.

   ⛔ LO QUE NO ENTRA AQUÍ: `.tv-store-name`, `.tv-queue-title`, `.tv-footer`,
   `.tv-store-photo` y TODO el cartel de llamado (`.tv-turn-call-*`). La decisión
   del dueño es sobre **LA LISTA**, no sobre la pantalla entera. ══════════════ */
.tv-queue-list, .tv-appt-list { flex: 1; overflow: hidden; display: flex; flex-direction: column; gap: clamp(.859rem, 1.272vh, 1.717rem); }
/* ══ ÍTEM 619 — LA FILA DE CITA ES UNA FRASE, NO UNA TABLA ════════════════════
   Forma pedida: `[cliente abreviado] con [barbero] a las 12:30 pm` — *"una frase
   corrida, NO columnas. Se lee de un vistazo y desde lejos."*
   🔒 El ejemplo va con ROLES y no con nombres propios a propósito: este archivo
   se sirve TAL CUAL al navegador de cualquiera que abra el TV.
   · `flex-wrap` + `column-gap` en vez de espacios en el markup: el hueco entre
     el nombre y el resto es SIEMPRE el mismo aunque Razor recorte el espacio en
     blanco, y la frase se parte sola en dos líneas en una columna estrecha en
     vez de salirse de la caja.
   · ⛔ SIN `text-overflow: ellipsis` A PROPÓSITO: cortar "…a las 12:3" es peor
     que ocupar dos líneas. Y el paginador MIDE la altura real de cada fila
     (ítem 611), así que una fila de dos líneas no esconde a nadie.
   · Todo en `vh` como el resto de la columna (R-305 / ítem 610): en 4K la letra
     dobla sola y siguen cabiendo los mismos nombres. */
/* ══ 17:45 (Héctor): "rectángulo azul sobre background azul no se ve".
   🔄 20:00 (v5, LV-43): SUPERSEDE el borde blanco 1px del 17:45/18:30 — v5
   resuelve el mismo problema con un degradado MÁS CLARO + borde azul con
   brillo (ver `.tv-queue-row`, arriba de este bloque en el archivo) en vez
   de un borde blanco encima del navy de antes. `.tv-appt-row` (Citas de
   hoy) no tiene equivalente en v5 (solo lista `.queue-card`) — mismo
   tratamiento por analogía: MISMA superficie, MISMO problema. `.tv-row-outline`
   se queda como gancho compartido por si un tercer rectángulo lo necesita. ══ */
.tv-row-outline, .tv-appt-row {
    background: linear-gradient(145deg, #081b3e, #03102a);
    border: 1px solid #2c8bff;
    box-shadow: inset 0 0 20px rgba(18,75,170,.08), 0 0 6px rgba(42,138,255,.18);
}
.tv-appt-row {
    display: flex; flex-wrap: wrap; align-items: baseline;
    /* `column-gap` y `row-gap` NO se tocan (R-313): ya están en `em`/`rem` chico y
       el `.35em` escala solo con la letra de la fila. */
    column-gap: .35em; row-gap: .15rem;
    /* R-313: 12 × 1,5610 = 18,7px — el radio también sube, si no la fila grande
       se ve con las esquinas "apretadas" respecto a la de antes.
       `background` de v5 vive arriba, en `.tv-row-outline, .tv-appt-row` —
       no repetido aquí para que una sola regla mande. */
    border-radius: 18.7px;
    padding: clamp(.859rem, 1.272vh, 1.717rem) clamp(1.249rem, 1.850vh, 2.498rem);
    /* La banda azul del rótulo "CITAS DE HOY" ya dice de qué lista es; este
       filete lo repite a nivel de fila para el que mira de reojo. */
    border-left: 4px solid #93C5FD;
}
/* R-313 · el NOMBRE es lo que el dueño juzgó desde lejos. ×1,5610 en el `vh` y en
   el TECHO (ver la trampa arriba) — que es donde manda en un televisor.
   🔧 EL MÍNIMO NO: se queda en 1.45rem (ver la nota completa en `.tv-name`). */
.tv-appt-who { font-size: clamp(1.45rem, 2.891vh, 3.903rem); font-weight: 700; }
/* El resto de la frase, un punto por debajo: el ojo cae primero en el NOMBRE
   (que es lo que cada cual busca) y de ahí sigue leyendo la línea. */
/* ══ 🔧 AJUSTE (el dueño 2026-08-06: "arregla las dos") — EL RENGLÓN DEL DETALLE
   CEDE PARA QUE LA FRASE NO SE PARTA ══════════════════════════════════════════
   🩸 LO MEDIDO: con el ×1,5610 del R-313, a 3840×2160 la frase de una cita ya no
   cabía en una línea. Ancho interior de la fila **795,1 px**; el peor caso real
   —el apodo de barbero más largo que existe en los datos, en ESPAÑOL y con hora
   de dos dígitos, `con QA-Naranja a las 12:30 pm`— medía **730,2 px** y con el
   nombre delante la suma llegaba a 1 011 px ⇒ **se pasaba 216 px** y la fila se
   partía en dos. Eso bajó las citas visibles de 14 a 6.
   ⚖️ LA DECISIÓN ES DEL DUEÑO y su razón manda: él aprobó **el tamaño del
   NOMBRE**, juzgado de lejos en el televisor. **El detalle es subordinado y es el
   que cede.** ⛔ Por eso `.tv-appt-who` NO se toca.
   📐 POR QUÉ ×0,55 Y NO OTRO — TABLA MEDIDA a 3840×2160 con un nombre
   realista de 10 caracteres y el apodo más largo de los datos:
        factor  letra   sobra    ¿aguanta un apodo más largo?
        0,65    34,11   −6,9  🔴 ni siquiera cabe el caso de hoy
        0,60    31,47  +29,7  ⚠️ ~2 caracteres de apodo: se vuelve a partir pronto
        0,55    28,86  +66,2  ✅ aguanta un apodo de 16 caracteres
        0,50    26,22 +102,8     encoge de más sin necesidad
   ⇒ **0,55 es el factor MÁS GRANDE que deja holgura de verdad** (ese mismo nombre de 10 caracteres con
   `con Chino El Barbero a las 12:30 pm`, un apodo 60% más largo que cualquiera
   que exista hoy, todavía entra en una línea).
   ⚠️ LO QUE ESTO **NO** RESUELVE, y hay que saberlo: el nombre del cliente puede
   comerse solo el 70% de la fila. Con `.tv-appt-who` congelado en 62,45 px, la
   frase entra en una línea mientras el nombre no pase de **387,8 px (≈13
   caracteres)**. Un nombre de 15 caracteres (466,9 px) SIGUE partiendo la fila
   — y NO hay tamaño de este renglón que lo evite sin tocar el nombre. Partirse no
   esconde a nadie (el paginador del 611 mide la altura REAL de cada fila), así
   que es seguro; pero es un límite del diseño, no un descuido.
   📌 Los tres valores bajan con el MISMO factor — es la trampa del techo del
   R-313 al revés: bajar solo el `vh` dejaría el techo mandando en 4K. ══════════ */
.tv-appt-rest { font-size: clamp(0.901rem, 1.336vh, 1.803rem); font-weight: 500; color: #CBD5E1; }
/* La marca "¡Ya es tu cita!" reusa el mismo distintivo ámbar del 590 — misma
   señal visual para el mismo hecho (te toca), en las dos listas. */
.tv-appt-row.called {
    background: rgba(252,211,77,.16);
    box-shadow: inset 0 0 0 2px rgba(252,211,77,.85), 0 0 16px rgba(252,211,77,.28);
    border-left-color: #FCD34D;
}
.tv-queue-row {
    /* R-313 · ×1,5610 en hueco, relleno y radio: la fila crece como UN bloque.
       Si solo subiera la letra, el texto se pegaría a los bordes de una caja que
       se quedó del tamaño de antes.
       🔄 20:00 (v5, LV-43): SUPERSEDE el borde blanco 1px del 17:45/18:30 — v5
       `.queue-card` resuelve "azul sobre azul no se ve" con un degradado MÁS
       CLARO + borde azul con brillo, no con un borde blanco encima del navy
       de antes. */
    display: flex; align-items: center; gap: clamp(1.249rem, 1.850vh, 2.498rem);
    background: linear-gradient(145deg, #081b3e, #03102a);
    border: 1px solid #2c8bff;
    box-shadow: inset 0 0 20px rgba(18,75,170,.08), 0 0 6px rgba(42,138,255,.18);
    border-radius: 18.7px;
    padding: clamp(.859rem, 1.272vh, 1.717rem) clamp(1.249rem, 1.850vh, 2.498rem);
}
.tv-queue-row.next {
    /* v5 `.queue-card.active`: degradado radial magenta + lineal rojo, primer
       stop oscurecido (misma `--grad-start` que TvMenuPanel, ~6% — aquí sin
       variable CSS porque app.css es compartido y NO scoped a la superficie
       TV: se escribe literal para no crear una var global). */
    background: radial-gradient(circle at 100% 100%, rgba(151,0,167,.75), transparent 36%),
                linear-gradient(135deg, #dc1436 0%, #a90043 100%);
    border-color: #ff5570;
    box-shadow: inset 0 1px 0 rgba(255,255,255,.22), 0 0 5px rgba(255,80,108,.9), 0 0 18px rgba(255,0,89,.28);
}
.tv-turn {
    /* R-313 · ×1,5610. El número de posición sube CON el nombre: es la otra mitad
       de lo que se lee de lejos, y si se quedara chico la fila se descuadra. */
    font-size: clamp(2.498rem, 3.700vh, 4.995rem); font-weight: 800;
    /* 🔑 `em`, NO `rem`: el ancho mínimo de la pastilla se mide contra SU PROPIA
       letra, así que sigue al tamaño de fila sin una segunda fórmula que mantener.
       1,5 em × 25,6 px = 38,4 px = los 2,4rem exactos de antes a 1080p. */
    min-width: 1.5em; text-align: center;
    /* R-313: 10 × 1,5610 = 15,6px. El `min-width` en `em` y el `padding` chico no
       se tocan — ya siguen a su propia letra (ver el 🔑 de arriba). v5
       `.queue-number`: degradado navy, no blanco translúcido plano. */
    background: linear-gradient(145deg, #2d477e, #182b5e);
    box-shadow: inset 0 1px 0 rgba(255,255,255,.1);
    border-radius: 15.6px; padding: .1rem .3rem;
}
.tv-queue-row.next .tv-turn { background: #fff; color: #C8102E; }
/* ══ LV-75 / ÍTEM 1179 pt.4 (Paul, 2026-09-06) — FILAS DE ESTADO DEL TABLERO ═══════
   «En la silla» (serving) y «Fuera ahora» (barbero pausado, R-428): mockup C-D de
   Kahlo. Misma fila que la cola, SIN pastilla de turno (no ocupan turno); el ícono
   lo pone QueueStateBadge a la altura del reloj de la fila (`.tv-queue-ico-img`) y
   el texto de estado va a la derecha, más chico que el nombre (mockup: 15/18 = .83)
   y al .85 de opacidad (el `.t` del mockup). Sin `.next`: nunca roja. */
.tv-queue-row.tv-queue-row-state { opacity: .9; }
.tv-queue-row.tv-queue-row-state .wl-st-ico { flex: 0 0 auto; opacity: .92; filter: drop-shadow(0 .1em .2em rgba(0,0,0,.5)); }
.tv-queue-state { margin-left: auto; opacity: .85; white-space: nowrap; font-size: calc(clamp(1.45rem, 2.891vh, 3.903rem) * .83); }
.tv-board .tv-queue-state { font-size: calc(clamp(1.45rem, 2.891vh, 3.903rem) * var(--tvf, 1) * .83); }
/* R-313 · EL NOMBRE DE LA LISTA DE ESPERA — es LA letra que el dueño aprobó
   mirando el televisor del socio. ×1,5610 en el `vh` y en el TECHO.
   ══ 🔧 AJUSTE (el dueño: "arregla las dos") — EL MÍNIMO SE QUEDA EN 1.45rem ══════
   🩸 LO QUE SE ROMPIÓ AL SUBIRLO A 1.951rem, MEDIDO EN EL DOM: en 1366×768 el
   `vh` NO manda (2.891vh × 7,68 = 22,2 px, por debajo del mínimo) ⇒ mandaba el
   MÍNIMO, la letra pasó de 20 → 31,2 px y un nombre de 15 caracteres dejó de caber:
   disponible 179,1 px contra 229 px de texto ⇒ **puntos suspensivos**. Antes
   sobraban 75,2 px. Un nombre cortado en la pantalla que existe para leer
   nombres es un fallo de producto, no un detalle.
   📐 POR QUÉ 1.45 Y NO 1.5 — TABLA MEDIDA con un nombre de 15 caracteres (el más
   largo que `PublicShortName` produce con datos realistas) sobre los 179,1 px
   disponibles a 1366×768:
        min       letra    ancho    sobra
        1.951rem  31,22    222,9    −43,8  🔴 corta (lo de hoy)
        1.60rem   25,60    182,8     −3,7  🔴 corta
        1.55rem   24,80    177,1     +2,0  ⚠️ dos píxeles: eso no es holgura
        1.50rem   24,00    171,4     +7,7  ⚠️ menos de UN carácter (≈11,4 px)
        1.45rem   23,20    165,7    +13,4  ✅ más de un carácter de margen
   ⇒ **1.45rem es el valor MÁS GRANDE cuyo margen aguanta un carácter más.** Los
   nombres varían de carácter en carácter, no de píxel en píxel: un colchón menor
   que una letra no es colchón.
   🔒 Y NO MUEVE NADA DE LO QUE EL DUEÑO APROBÓ: el mínimo solo manda por debajo de
   ~810 px de alto. A 1920×1080 gana el `vh` (31,22 px) y a 3840×2160 gana el
   techo (62,45 px) — **verificado midiendo antes y después: idénticos.**
   ⚠️ HONESTO: un nombre de 16 caracteres CON DÍGITOS (los fixtures `QA253-…`)
   sigue cortándose a 768 — a 22,2 px, que es el suelo que impone el `vh`, mide
   189,6 px contra 179,1 disponibles. **Es imposible de arreglar sin tocar el
   `vh`, y el `vh` ES lo que el dueño aprobó.** Se reporta, no se toca. */
.tv-name { font-size: clamp(1.45rem, 2.891vh, 3.903rem); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ÍTEM 892 (Héctor, 14:35): tijera en la fila EN TURNO, reloj en las que
   ESPERAN — como el panel 2 de su mockup. `flex:none` a propósito: el nombre
   YA tiene su propio mecanismo de recorte (overflow+ellipsis, arriba) y no
   hace falta un segundo — el ícono solo se suma al final sin robarle ancho a
   nadie más que a lo que el nombre ya cedía. */
.tv-queue-ico { flex: 0 0 auto; font-size: clamp(1.1rem, 1.7vh, 2.4rem); color: #fff; opacity: .92; }
/* ÍTEM 1116 (Oller, medido 2026-09-04) — el RELOJ del set glossy de Héctor,
   sustituye `bi-clock` en las tarjetas "Esperando" (15:1 de contraste sobre
   la navy, medido). Antes el glifo medía ~18px (el mínimo del clamp de
   arriba, `1.1rem`); "a la altura de la línea" = la misma escala que
   `.tv-name` usa más abajo (`.tv-board .tv-name`, `clamp(1.45rem,2.891vh,
   3.903rem) * var(--tvf,1)`) — el reloj real ya no es un glifo chico al
   lado del nombre, es del mismo porte que la línea que acompaña. `height`
   (no `font-size`): es un `<img>`, no una fuente. */
.tv-queue-ico-img { flex: 0 0 auto; height: clamp(1.45rem, 2.891vh, 3.903rem); width: auto; opacity: .92; }
.tv-board .tv-queue-ico-img { height: calc(clamp(1.45rem, 2.891vh, 3.903rem) * var(--tvf, 1)); }
/* ══ ÍTEM 1117 (Héctor, decisión 3(a) del 09:35 EDT: "la tijera gira solo en
   la fila roja del llamado") — LA TIJERA DEL «¡ES TU TURNO!» GIRA ═══════════
   Sprite de Oller: `tijeras-360-sprite.png`, 15 cuadros de 77×128 en tira
   horizontal (1155×128) — misma técnica de sprite+`steps()` que R-439 (el
   giro 360 de la foto de "La Recomendación de Hoy", que usa 8 `<img>`
   apiladas en vez de sprite porque ahí cada cuadro YA es una foto servida
   aparte; aquí es UN solo fichero, más barato para 15 cuadros).
   🔴 Oller midió los 5 cuadros de CANTO (índices 5-9, "casi de perfil", fila
   2 de la hoja original) como ILEGIBLES a 24 px (5.6-11.9% de tinta, "una
   raya gris") — el encargo deja elegir "sáltalos o repárteles menos tiempo";
   se SALTAN (jump-cut entre el cuadro 4 -frontal- y el 10 -trasero-, ambos
   con tinta real) en vez de repartir: repartir tiempo a un cuadro ilegible
   sigue enseñando una raya gris, solo que más rápido — saltarlo quita el
   defecto entero. `@@keyframes` con 10 paradas (no `animation-timing-
   function: steps(15)` liso, que SÍ pasaría por los 5 ilegibles) y
   `steps(1)` en cada tramo: mantiene el cuadro fijo hasta saltar al
   siguiente, sin interpolar posiciones intermedias.
   ⏱️ CADENCIA (Héctor no eligió entre los 2 GIF de Oller — `tijeras-360-
   preview-0.15s.gif` y `-0.8s.gif` — encargo: "toma la rápida y anótalo
   como default a confirmar"): 0,15 s/cuadro × 10 cuadros = 1,5 s por vuelta
   completa. ⚠️ DEFAULT A CONFIRMAR POR HÉCTOR, no una medición cerrada.
   🎨 `filter: drop-shadow` — MISMA receta que `.ui-icon-shadow` (más abajo,
   ítem 1118): resuelve el mismo problema de contraste que el comentario
   viejo de este archivo documentaba para `bi-scissors` (1.01:1 sobre la
   banda roja) sin disco — Héctor, LV-46, 23:19 EDT: "se ven mejor sin el
   disco blanco", misma variante SHADOW que el resto del set glossy.
   Tamaño: MISMA altura que `.tv-queue-ico-img` (el reloj de al lado, ítem
   1116) — las dos tarjetas ("Esperando"/"Llamado") enseñan un ícono del
   mismo porte, solo cambia el estado. Ancho por proporción real del cuadro
   (77:128), nunca un ancho fijo que recorte o estire el sprite. */
.tv-queue-ico-spin {
  flex: 0 0 auto; opacity: .92;
  height: clamp(1.45rem, 2.891vh, 3.903rem);
  width: calc(clamp(1.45rem, 2.891vh, 3.903rem) * 77 / 128);
  background-image: url('/images/ui-icons/tijeras-360-sprite.png');
  background-repeat: no-repeat; background-size: 1500% 100%; background-position: 0 0;
  filter: drop-shadow(0 .1em .2em rgba(0,0,0,.5));
  animation: tvQueueScissorsSpin 1.5s steps(1) infinite;
}
.tv-board .tv-queue-ico-spin {
  height: calc(clamp(1.45rem, 2.891vh, 3.903rem) * var(--tvf, 1));
  width: calc(clamp(1.45rem, 2.891vh, 3.903rem) * var(--tvf, 1) * 77 / 128);
}
@keyframes tvQueueScissorsSpin {
  /* 10 cuadros VISIBLES de los 15 (saltados los 5 de canto, índices 5-9) —
     posición = índice/(15-1)×100%, fórmula estándar de sprite por
     `background-position` en porcentaje sobre un `background-size` mayor
     que el contenedor. */
  0%    { background-position: 0% 0; }        /* cuadro 0 (frontal) */
  10%   { background-position: 7.142857% 0; }  /* cuadro 1 */
  20%   { background-position: 14.285714% 0; } /* cuadro 2 */
  30%   { background-position: 21.428571% 0; } /* cuadro 3 */
  40%   { background-position: 28.571429% 0; } /* cuadro 4 (último frontal legible) */
  50%   { background-position: 71.428571% 0; } /* cuadro 10 (1.º trasero legible — SALTA el 5-9) */
  60%   { background-position: 78.571429% 0; } /* cuadro 11 */
  70%   { background-position: 85.714286% 0; } /* cuadro 12 */
  80%   { background-position: 92.857143% 0; } /* cuadro 13 */
  90%   { background-position: 100% 0; }       /* cuadro 14 (trasero) */
  100%  { background-position: 0% 0; }         /* vuelve al frontal — SALTA el 10-14→0 */
}
/* ♿ Igual que el resto de animaciones del tablero (`.tv-turn-call`, el poste
   del llamado…, ver el `@@media` de más abajo): quien pide menos movimiento
   ve la tijera QUIETA en el primer cuadro (frontal, el mismo que abre el
   ciclo) — nunca sin ícono. El mensaje/estado no cambia, solo el giro. */
@media (prefers-reduced-motion: reduce) {
  .tv-queue-ico-spin { animation: none; }
}
.tv-empty { flex: 1; display: flex; align-items: center; justify-content: center; font-size: clamp(1.3rem, 1.926vh, 2.6rem); color: #93C5FD; }
.tv-footer { text-align: center; font-size: clamp(.8rem, 1.185vh, 1.6rem); color: #64748B; margin-top: clamp(.8rem, 1.185vh, 1.6rem); }
/* ÍTEM 590: `position:relative` es el ancla del cartel de turno, que va como
   CAPA absolute encima del anuncio (nunca lo reemplaza — si el <video> saliera
   del DOM se rompería el contrato { playing, duration } del ítem 137a y el TV
   se congelaría). No cambia nada del layout de hoy. */
.tv-ads { width: 75%; flex: 1; display: flex; align-items: center; justify-content: center; background: #000; position: relative; }
.tv-media { width: 100%; height: 100%; object-fit: contain; }
.tv-house-ad {
    width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
    background: linear-gradient(135deg, #14224E 0%, #C8102E 100%);
}
.tv-house-inner { text-align: center; max-width: 720px; padding: 2rem; }
.tv-house-inner h1 { font-size: 3rem; font-weight: 800; color: #fff; }
.tv-house-inner p { font-size: 1.4rem; color: #E2E8F0; }
.tv-house-contact { font-weight: 700; font-size: 1.6rem !important; color: #fff !important; }
.tv-house-brand { font-size: 1rem !important; color: #93C5FD !important; margin-top: 1.5rem; }

/* House ad: QR para pedir anunciarse */
.tv-house-qr { width: 160px; height: 160px; border-radius: 12px; background: #fff; padding: 8px; margin-top: .5rem; }
.tv-house-scan { font-size: 1.1rem !important; font-weight: 600; color: #fff !important; margin-top: .4rem; }

/* ══ 🟢 LV-25 — ETIQUETAS GRANDES DE COLOR EN LOS SLIDES DE BIENVENIDA ════
   El dueño, verbatim: *"eso eliminaría el riesgo de escanear el qr incorrecto"*.
   Kahlo MIDIÓ el problema: los dos slides se parecen demás de lejos — misma foto,
   mismo título, un QR — y lo único que los separa hoy es una línea de **17.6px**
   (`.tv-house-scan` a 1.1rem). A cinco metros eso no se lee: se adivina.
   📐 **2.125rem = 34px**, el doble EXACTO de esa scan-line. Va ARRIBA del QR
   porque de lejos se lee en este orden — COLOR → ÍCONO → TEXTO — y para cuando
   el ojo llega al código ya sabe cuál es.
   🎨 LOS COLORES SON LOS QUE APROBÓ LA LUZ VERDE: navy/blanco = CITA ·
   dorado = FILA. ⚠️ Y hay que decirlo con honestidad porque el documento lo da
   por hecho: **esa convención YA NO ESTÁ VIVA EN EL KIOSCO.** Nació allí (ítem
   294a: dorado/caramelo para la tarjeta de la fila) y el propio dueño la mandó
   a retirar en el ítem 305a (*"ponerlo con los mismos colores que tenemos en el
   card de reservar una cita"*) — hoy las dos tarjetas del kiosco son navy/rojo.
   ⇒ Aquí los colores se usan porque **en el TV sí hacen falta** (dos slides que se
   suceden en la misma pared, sin poderlos comparar lado a lado), no porque el
   kiosco los esté usando. Nadie debe leer "una sola convención en toda la casa"
   y salir a buscarla en el kiosco.
   📺 Colores FIJOS (no del tema): el TV es un dispositivo público de marca
   fija, mismo criterio que todo `.kiosk-*` y el resto de `.tv-*`. */
.tv-cta-band {
    /* 🔴 `display: flex` + `width: fit-content` + márgenes automáticos, NO
       `inline-flex`. La primera versión usó `inline-flex` y lo MEDÍ: la banda
       quedó en **y=455** y el QR en **y=418**, o sea AL LADO y más abajo, no
       encima. La causa: `.tv-house-qr` es un `<img>`, o sea INLINE — una banda
       inline se le sienta al lado en la misma línea. Así la banda vuelve a ser
       de nivel de bloque (se apila ARRIBA, que es lo que manda LV-25) sin
       perder la forma de píldora (se encoge al contenido y se centra sola). */
    display: flex;
    width: fit-content;
    align-items: center;
    justify-content: center;
    gap: .4rem;
    font-size: 2.125rem !important;   /* 34px — 2× la scan-line de 17.6px */
    font-weight: 800;
    line-height: 1.15;
    padding: .35rem .95rem;
    border-radius: 999px;
    margin: .6rem auto .1rem;
    white-space: nowrap;
}
.tv-cta-ico { font-size: 2.375rem; line-height: 1; }
/* 📅 CITA — navy sobre blanco. Tinta #14224E sobre #FFFFFF = 15.34:1 (medido). */
.tv-cta-book  { background: #FFFFFF; color: #14224E !important; border: 3px solid #14224E; }
/* ⏱️ FILA — dorado. Tinta #3A2404 sobre #E9B96E = 8.11:1 (medido) (la banda es lo más
   grande del slide: se lee de lejos, no se descifra de cerca). */
.tv-cta-queue { background: #E9B96E; color: #3A2404 !important; border: 3px solid #8A5A0B; }
/* El marco del QR repite el color de SU banda — si el ojo se salta el rótulo,
   el propio código sigue diciendo de cuál de los dos es. */
.tv-house-qr-book  { box-shadow: 0 0 0 6px #14224E; }
.tv-house-qr-queue { box-shadow: 0 0 0 6px #E0A458; }

/* ITEM 148 (Batch K): anuncio TARJETA — foto opcional (barbero/barberia) en
   circulo elegante sobre el mismo lienzo del house ad.
   ÍTEM 280: círculo centrado decapitaba retratos verticales
   — sesgo hacia ARRIBA, mismo tratamiento del kiosko. */
.tv-card-photo { width: 150px; height: 150px; object-fit: cover; border-radius: 50%;
                 object-position: 50% 15%;
                 border: 4px solid #fff; box-shadow: 0 6px 24px rgba(0,0,0,.35); margin-bottom: .75rem; }

/* ══ ÍTEM 590 (el dueño 2026-08-04) — EL CARTEL DE TURNO ══════════════════════
   La maqueta del dueño: "NEXT CUSTOMER: LEONARD M." ocupando el panel grande de
   los anuncios, el nombre ENORME. Se lee de pie desde el otro lado del local,
   que es el único sitio desde donde alguien lo va a leer de verdad.
   El tamaño va en `vw` con clamp Y TOPE EN PX: el tope es la medida EXACTA de la
   receta a 1920×1080 (el televisor real) y el `vw` deja que el mismo cartel se
   vea bien en la tableta con la que se prueba.
   `overflow-wrap:anywhere` porque un nombre largo NO puede desbordar la pantalla
   (la regla de la casa: nada se sale ni se corta a media cara).

   ══ EL ARTE — RECETA DE KAHLO, LUZ VERDE DEL DUEÑO 2026-08-05 ══════════════
   *"me encantó el mockup, aprobado!"* sobre `MBS\kahlo\mockup-590-cartel-turno.html`.
   📖 LA RECETA CON TODOS LOS PÍXELES: `MBS\kahlo\cartel-turno-spec.md`.
   Aquí se APLICA. Cambiar el aspecto = mockup nuevo + luz verde nueva.

   🔴 LA REGLA DE COLOR, Y ES UN BUG ARREGLADO, NO UN GUSTO:
      **EL NAVY #14224E ES CAMPO. EL ROJO #C8102E ES FORMA. Jamás se funden.**
   Hasta hoy el fondo era `linear-gradient(135deg,#14224E,#C8102E)`, y el punto
   medio de esa interpolación es **#6E1A3E — morado vinoso**. Ese morado NO LO
   ESCOGIÓ NADIE: es aritmética, y se comía el centro de un panel de 1440 px.
   Ahora el campo es navy con vignette y el rojo vuelve a ser lo que es en esta
   marca: pastilla del rótulo, bandas de barbería y disco del monograma.
   ⚠️ Y hay una razón DURA además de la estética: **#C8102E sobre #14224E da
   2,61:1** (la receta dice 1,8 — recalculado aquí con la fórmula WCAG; el número
   cambia, la conclusión no: está MUY por debajo del 4,5 exigido). El rojo NO
   PUEDE SER TEXTO sobre navy — ni rótulo, ni nombre, ni el "con".
   ⛔ No devolver el rojo a un color de TEXTO en este cartel.

   Contraste MEDIDO de lo que aquí se pinta (WCAG 2.x, calculado en el arnés):
     · nombre #FFFFFF sobre el campo navy #14224E .... 15,34:1  ✅ AAA
       (en lo más claro de la vignette, #1B2F63) ..... 12,86:1  ✅ AAA
     · "con [barbero]" #E2E8F0 sobre el campo navy ... 12,45:1  ✅ AAA
     · rótulo #FFFFFF sobre pastilla roja #C8102E .... 5,88:1   ✅ AA
     · monograma #FFFFFF sobre disco rojo #C8102E .... 5,88:1   ✅ AA
       (ANTES: navy #14224E sobre navy #14224E ....... 1,00:1   🔴 invisible)
   🎨 TODO EL ASPECTO VIVE EN ESTAS VARIABLES: rediseñar NO obliga a tocar una
   línea de lógica ni de Razor. */
.tv-turn-call {
    /* Campo NAVY con vignette. ⛔ Aquí NO vuelve un gradiente navy→rojo. */
    --tvc-bg: radial-gradient(125% 95% at 50% 44%, #1B2F63 0%, #14224E 46%, #0B1220 100%);
    --tvc-red: #C8102E;          /* el rojo, SIEMPRE como forma (pastilla/disco/banda) */
    --tvc-name: #fff;
    --tvc-with: #E2E8F0;
    --tvc-rule: rgba(255,255,255,.20);
    --tvc-photo: 128px;          /* diámetro de la foto del barbero (1 nombre) */
    --tvc-photo-ring: rgba(255,255,255,.9);
    /* 🔑 EL AGUJERO QUE SE TAPA: el monograma era un disco NAVY sobre un campo
       NAVY ⇒ un barbero sin foto dejaba lo que parecía un fallo de carga. Disco
       ROJO, blanco encima, 5,89:1: el respaldo se ve INTENCIONAL. */
    --tvc-photo-bg: var(--tvc-red);
    --tvc-gap: 1.75rem;          /* separación foto ↔ nombre */
    /* ⏱️ LA DURACIÓN NO SE QUEMA AQUÍ. `TvBoardView.razor` la inyecta en línea
       desde `Tv:TurnCallSeconds` (Services\TurnCallQueue.cs → TurnCallDisplay).
       Este 10s es SOLO el respaldo si alguien renderiza el cartel sin la
       variable; el día que el dueño lo baje a 8 s en Azure, la animación le sigue.
       ⛔ NO poner aquí un número fijo: un CSS de 10 s con un cartel de 8 s haría
       DESAPARECER el cartel de golpe a media salida. */
    --tvc-dur: 10s;

    /* ══ ÍTEM 605 (el socio + el dueño, TELEVISOR 4K REAL 2026-08-05) ═════════════
       > *"los postes estan muy lejos del nombre, creo que nos tenemos que acercar
       >  un poco al nombre, estan muy pegados de la orilla para esta resolucion."*
       🩸 LA CAUSA, y es de las que NO se ven en la máquina del que las escribe:
       el poste estaba clavado a la ORILLA del panel (`left: 24px`) mientras que el
       nombre vive en un bloque CENTRADO y TOPADO en 1100 px. A 1080p el panel son
       1440 px y la cuenta cierra clavada (1100 + 2×(132+24) = 1412, 14 px de aire
       a cada lado). **Pero el tope de 1100 no crece con la pantalla y la orilla
       sí**: en el panel 4K del socio el bloque sigue midiendo 1100 y el poste se
       queda a 700 px de distancia. Un desierto azul.
       ✅ EL ARREGLO: el poste se ancla AL BLOQUE DEL NOMBRE, no a la orilla — y
       con LA MISMA variable de ancho, para que la fórmula del poste y el ancho del
       bloque NO PUEDAN DISCREPAR NUNCA (que es exactamente lo que pasó: el 1100
       estaba escrito en dos sitios que nadie ataba).
       🔒 `max(--tvc-edge, …)` ES EL SEGURO, NO EL MECANISMO. En una pantalla
       estrecha (panel < 1392 px) la resta se vuelve NEGATIVA y el poste se saldría
       del panel. De eso se encarga, ANTES, el corte que los esconde (búscalo más
       abajo: `max-width: 1698.98px`, con su cuenta — era 1855.98 hasta el ítem
       1311, que midió el panel de verdad). Este tope existe para el día
       en que alguien toque `--tvc-w` y se olvide de rehacer aquel número: el poste
       se quedará donde ha estado siempre —a 24 px de la orilla— en vez de irse
       fuera de la pantalla. **Nunca peor que hoy, pase lo que pase.**
       📐 La cuenta a 1080p, que es la prueba de que no rompe nada:
          panel 1440 ⇒ 50% = 720 · 720 − 1100/2 − 132 − 14 = 720 − 550 − 146 = 24
          …o sea EXACTAMENTE el `left: 24px` de hoy.
          4K del socio: panel 2880 ⇒ 1440 − 550 − 146 = 744 px ⇒ el poste deja la
          orilla y se pega al nombre, que es lo que pidió el dueño.
       ⚠️ ESTO SOLO CUADRA PORQUE EL `50%` DEL POSTE Y EL `94%` DEL BLOQUE MIDEN
       CONTRA LO MISMO: el poste es `absolute` y su bloque contenedor es ESTE
       elemento (`.tv-turn-call` es `position:absolute`), y el bloque del nombre es
       hijo directo suyo. Como aquí NO hay padding ni borde, la caja de padding y
       la de contenido miden igual. ⛔ El día que alguien le ponga `padding` a
       `.tv-turn-call`, las dos referencias se separan y el poste se descoloca sin
       que nadie lo note a 1080p. */
    --tvc-w: min(1100px, 94%);   /* ancho del bloque del nombre — la ÚNICA fuente */
    --tvc-air: 14px;             /* aire entre el poste y el bloque del nombre */
    --tvc-edge: 24px;            /* mínimo histórico a la orilla: nunca menos */

    /* ══ ÍTEM 1311 (Héctor, 2026-09-13) — EL ADORNO SE ENCOGE, Y R-605 SIGUE MANDANDO ══
       > *"postes más pequeños que quepan en tablet"*
       🔑 LA REGLA QUE R-605 PROTEGE NO ES "POSTES GRANDES": ES **EL NOMBRE JAMÁS**.
       El *"no se encoge"* del 605 era el MEDIO, no el fin. Queda así:
       **el adorno se ENCOGE mientras siga cabiendo CON AIRE; si ni encogido cabe
       con aire, SE VA.**

       🩸 Y AL MEDIRLO APARECIÓ QUE EL FALLO MUDO DEL 605 NUNCA SE CERRÓ DEL TODO.
       La cuenta de más abajo (la que produce el `1855.98`) da el panel por
       `0,75 × ventana`. **El panel real es 42,37 px MÁS ESTRECHO**, y no por poco:
       `.tv-queue` NO es `border-box`, así que su `padding` (2 × 19,2 px) y su
       `border-right` (4 px) se SUMAN a su 25%. Medido con el markup real a seis
       anchos distintos, el desfase es constante:
          ventana 1920 → panel 1397,63  (la cuenta decía 1440)
          ventana 1856 → panel 1349,63  (la cuenta decía 1392)
          ventana 1280 → panel  917,63  (la cuenta decía  960)
       ⇒ CONSECUENCIA MEDIDA, y estaba VIVA en producción: el poste se enseñaba
         SOLAPANDO el bloque del nombre **−7,19 px a 1920 (el televisor 1080p) y
         −31,19 px a 1856**. Tapado por el `z-index` del bloque, igual que en 2026-08-05.
         El 605 movió el corte de 1199 a 1855,98 y mató el solape de 1200–1855,
         pero con un panel teórico ⇒ **dejó vivo el de 1856–1976.**

       ✅ POR ESO EL TAMAÑO YA NO SE ESCRIBE: SE CALCULA CONTRA EL CONTENEDOR.
       `--tvc-pole-fit` es el sitio que queda de verdad a cada lado del bloque del
       nombre, ya descontados la orilla y el aire. El poste nunca es más ancho que
       eso ⇒ **NO PUEDE SOLAPAR, por construcción, a ningún ancho.**
       📐 Cómo se lee, y cuadra con los números de arriba:
          50%  = medio panel              (el `%` mide contra `.tv-turn-call`, que es el panel)
          − --tvc-w/2                      = medio bloque del nombre
          − --tvc-air − --tvc-edge         = el aire al nombre y el mínimo a la orilla
       📌 El poste solo baja de 132 px cuando NO cabía: por encima de ~1976 px de
         ventana el `clamp` topa en 132 y **el televisor 4K y el 1080p grande no
         cambian ni un píxel.**

       🔴 Y LO QUE ESTO ARREGLA Y UNA MEDIA QUERY NO PODRÍA NUNCA: **el ancho de la
       cola es CONFIGURABLE** (`--tvcw`, clases `.tv-c0`…`.tv-c5`, del 25% al 50% —
       búscalas al final de este fichero). Con la cola al 50% el panel se parte por
       la mitad **sin que la ventana cambie**, así que ningún `max-width:` sobre la
       ventana puede saberlo: a 1920 px con `.tv-c5` el corte deja pasar el poste y
       el panel solo mide 917 px. La fórmula sí lo ve, porque mide el panel.
       ⇒ Es la LEY 14: *una compuerta que enumera casos es un inventario que caduca;
         preguntar por el ORIGEN (cuánto sitio hay) no ata a ninguna lista.*

       ⚠️ LO QUE ESTO **NO** ARREGLA, y está medido para que nadie lo busque otra vez:
       **en tablet horizontal el poste no cabe ni encogido**, y la causa NO es el
       poste. Con `--tvc-w` en 94% el bloque del nombre se come el panel entero:
          1366 → sobran 29,5 px por lado · 1280 → 27,5 · 1194 → 25,6 · 1080 → 23,0 · 1024 → 21,7
       y solo el aire (14) + la orilla (24) ya piden 38. **Aun con aire y orilla al
       mínimo, el poste saldría de 10 a 18 px de ancho: un pelo, no un poste.**
       🔬 La única palanca sería estrechar `--tvc-w`, Y SE MIDIÓ QUE CUESTA EL NOMBRE
       (5 resoluciones × 4 largos de nombre, comparando siempre contra el 94% de hoy):
          94% (hoy) → 0 nombres rotos · ningún poste útil
          90%       → 4 nombres rotos · poste de 14–27 px (sigue siendo un pelo)
          82%       → 8 nombres rotos · poste de 43–66 px
       ⇒ **No existe ningún valor que dé poste útil sin partir un nombre que hoy sale
         entero.** Por eso en tablet el poste SE VA, y eso no es un fallo: es R-605
         funcionando. 📖 El arnés que lo mide vive en `tools\arnes-1311-paul\`.

       ⛔ SI ALGUIEN QUIERE POSTE EN TABLET, la palanca NO es el tamaño del poste.
         Es el reparto del cartel (reubicarlo, o bajar foto/rótulo), y eso es
         rediseño: necesita mockup y luz verde. NO lo resuelvas apretando estos
         números — apretarlos parte el nombre. */
    --tvc-pole-cap: 132px;       /* el tope: el tamaño nativo del WebP (132×573) */
    --tvc-pole-fit: calc(50% - var(--tvc-w) / 2 - var(--tvc-air) - var(--tvc-edge));
    --tvc-pole-w: clamp(0px, var(--tvc-pole-fit), var(--tvc-pole-cap));
    /* 🩸 AQUÍ SE PERDIÓ UNA VUELTA Y LA TRAMPA MERECE QUEDAR ESCRITA (Paul, 1311).
       El primer intento sacaba el alto de una variable:
           --tvc-pole-h: calc(var(--tvc-pole-w) * 4.340909);   ← ROTO
       Y MIDIÓ PRECIOSO: aire de 14 px exactos a todos los anchos, cero solapes…
       **con el poste de ALTO 0, o sea invisible.** 🔑 La causa es que una variable
       CSS con un `%` dentro NO se resuelve al definirla, sino AL USARLA: el `50%`
       de `--tvc-pole-fit` mide contra el ANCHO en `width`, pero contra el ALTO en
       `height`. En `height` la resta daba negativo y el `clamp` la dejaba en 0.
       ⚠️ Es un fallo mudo perfecto: el informe habría dicho «no solapa a ningún
       ancho» — cierto — y el adorno no se veía en ninguna pantalla. Lo cazó el
       arnés porque imprime el ALTO además del ancho; con solo el aire, pasa.
       ⇒ El alto lo pone `aspect-ratio` sobre el ancho ya resuelto (abajo), que es
         la única forma de que la silueta no dependa de un `%` vertical. */
    /* ⚠️ ESTA FÓRMULA NO SE TOCA Y SIGUE SIENDO LA DE 605: al sustituir el poste
       encogido, `50% − w/2 − pole_fit − air` se simplifica a `--tvc-edge` exacto
       ⇒ el poste encogido queda a 24 px de la orilla con sus 14 px de aire al
       nombre, y el poste a tamaño completo se sigue colocando como el 605 mandó.
       Las dos ramas caen de la misma expresión; es imposible que discrepen. */
    --tvc-pole-x: max(var(--tvc-edge),
                      calc(50% - var(--tvc-w) / 2 - var(--tvc-pole-w) - var(--tvc-air)));

    position: absolute; inset: 0; z-index: 6; overflow: hidden;
    display: flex; align-items: center; justify-content: center; text-align: center;
    background: var(--tvc-bg);
    /* UNA sola línea de tiempo, y su duración ES la del cartel — por eso nunca
       se queda a medias: cuando el temporizador lo saca del DOM, la animación ya
       llegó sola a opacidad 0. (Medido por Kahlo: a 9,78 s va en 0,38.) */
    animation: tv-turn-call-life var(--tvc-dur) ease-out both;
}

/* ── Bandas de barbería, arriba y abajo ────────────────────────────────────
   ══ ÍTEM 609 (el dueño, mirando el TV, 2026-08-05) — LOS TRES COLORES DE LA CASA ══
   > *"notamos que las lineas de arriba y abajo no tienen el azul, se ven blanco y
   >  rojo, podemos usar los colores de la linea que hicimos para la aplicacion que
   >  tiene blanco azul y rojo?"*
   🔴 AQUÍ DECÍA "DE DOS COLORES, NO DE TRES", Y ESA REGLA YA NO ESTÁ VIGENTE — pero
   el PROBLEMA que la escribió sigue siendo verdad, así que hay que leer esto entero
   antes de tocar nada:
     · El `--barber-stripe` de la casa es rojo + blanco + **navy #14224E**.
     · Este cartel tiene CAMPO NAVY. Y no "parecido": MEDIDO — el fondo justo detrás
       de la banda de arriba es **exactamente #14224E** (la parada del 46% del
       radial-gradient cae ahí). ⇒ un navy PLANO sobre él da **1,00:1**: el tercio
       desaparece y la banda se lee como cinta de peligro con boquetes.
   ⚠️ Al dueño se le enseñó esa consecuencia ANTES de decidir. Decidió igual: **los
   tres colores de la casa, con el navy** — y el encargo fue **que el navy SE LEA**,
   no cambiarlo por otro azul.
   ✅ CÓMO SE LOGRA — EL LENGUAJE SE LE PRESTÓ AL POSTE DE CSS, que resolvía esto
   con RELIEVE: sombras interiores + un barrido de cristal en `::after`. ⚠️ Aquel
   `.pb-tube` YA NO EXISTE (borrado en el ítem 1230, 2026-09-08) — no lo busques:
   la técnica sobrevive AQUÍ, y esta es ya la única copia.
   Aquí va lo mismo a escala de cinta de 16 px: una capa de luz vertical encima del
   patrón (clara arriba, oscura abajo) + un filo blanco de 1 px arriba y otro oscuro
   abajo. El navy deja de ser "un agujero al fondo" y pasa a ser "la franja oscura de
   una cinta iluminada".
   📐 EL REPARTO: 15 px POR COLOR, no 12 como la casa. El ritmo y el ángulo de esta
   banda (15 px a 135°) NO se tocan — el dueño pidió COLOR, no densidad: al entrar el
   tercer color el ciclo pasa de 30 a 45 px y **cada segmento sigue midiendo
   exactamente lo que medía**. Repartir el ciclo de 30 px entre tres habría adelgazado
   la raya a 10 px, que es un cambio de aspecto que nadie pidió.
   ⛔ `--barber-stripe` NI SE ROZA: sobre fondo CLARO (kiosco, reconexión, tarjetas)
   sus tres colores se leen perfectamente. Esto es una EXCEPCIÓN de esta superficie
   por ser oscura, no un cambio del estándar de la casa. */
.tv-turn-call-band {
    position: absolute; left: 0; right: 0; height: 16px; opacity: .95; z-index: 1;
    /* La capa de LUZ va PRIMERO = va ENCIMA del patrón (orden de capas de CSS). */
    background:
        /* 1) El brillo de cinta, suave y para TODA la banda. */
        linear-gradient(180deg,
            rgba(255,255,255,.34) 0%,
            rgba(255,255,255,.16) 30%,
            rgba(255,255,255,.10) 62%,
            rgba(255,255,255,.20) 100%),
        /* 2) 🔑 EL REALCE QUE VA **SOLO** SOBRE EL TERCIO NAVY. Mismo ángulo y mismo
              ciclo que el patrón, así que cae clavado en su ranura (30→45 px). Es el
              truco que salva el rojo: subir la luz de toda la banda hasta que el navy
              se leyera dejaba el #C8102E en rosa (MEDIDO: #CE566C). Aquí la luz extra
              cae donde hace falta y el rojo se queda casi como estaba. */
        repeating-linear-gradient(135deg,
            rgba(255,255,255,0) 0 30px,
            rgba(255,255,255,.30) 30px 45px),
        /* 3) El patrón de la casa: rojo · blanco · NAVY, 15 px cada uno. */
        repeating-linear-gradient(135deg,
            var(--tvc-red) 0 15px,
            #FFFFFF 15px 30px,
            #14224E 30px 45px);
    /* 🔴 LA CAPA DE LUZ NUNCA BAJA A CERO, Y ESO ES LO QUE SE MIDIÓ: con el degradado
       "clara arriba, oscura abajo" del primer intento —el reflejo natural de un tubo—
       la MITAD INFERIOR del segmento navy volvía a dar **1,00:1** contra el campo.
       O sea: el boquete no desaparecía, se partía en dos. El suelo de luz (19%) es lo
       que mantiene TODO el segmento por encima del fondo; el repunte del final (30%)
       es el filo inferior de una cinta con brillo, no una sombra —una sombra sobre un
       campo oscuro no separa NADA, y eso también está medido.
       ⚠️ Precio aceptado: el rojo y el blanco quedan un punto más claros que antes.
       A cambio los TRES colores existen. */
    box-shadow: inset 0 1px 0 rgba(255,255,255,.62), inset 0 -1px 0 rgba(255,255,255,.38);
}
.tv-turn-call-band-top { top: 0; }
.tv-turn-call-band-bot { bottom: 0; }
/* Textura: MUY baja a propósito. Kahlo la probó al 4,5% y se veía A RAYAS,
   compitiendo con el poste. Al 2,8% da materia y no se "ve". */
.tv-turn-call-texture {
    position: absolute; inset: 0; pointer-events: none; z-index: 1;
    background: repeating-linear-gradient(135deg, rgba(255,255,255,.028) 0 2px, rgba(255,255,255,0) 2px 34px);
}

/* ── 💈 EL POSTE — LA CAJA ES LA MISMA PARA LOS CUATRO ESCALONES ───────────
   132 × 573 px, centrado vertical. 🔑 Que la caja NO cambie es lo que hace que
   bajar un escalón de la escalera de rescate sea cambiar UN valor de
   configuración y nada más se mueva de sitio.
   📌 ÍTEM 605: el poste YA NO se ancla a la orilla del panel sino AL BLOQUE DEL
   NOMBRE (`--tvc-pole-x`, definida arriba con la cuenta y el porqué). A 1080p da
   los mismos 24 px de siempre; en el 4K del socio se acerca al nombre en vez de
   quedarse en la orilla.
   📖 La escalera y cómo se baja: `TvBoardView.razor` → `PoleArt`. */
/* 📌 ÍTEM 1311: el alto ya NO es `573px` fijo y el centrado ya NO es `top:50%` con
   `margin-top:-286px`. El ancho sale de `--tvc-pole-w` (el sitio que de verdad hay)
   y el ALTO se deriva de ese ancho — ver arriba por qué NO puede salir de un
   `calc()` con `%` dentro.

   ══ 🩸 AQUÍ HUBO UN BLOQUEANTE Y LA TRAMPA SE QUEDA ESCRITA (Lupe lo cazó, 1311) ══
   La primera versión centraba con **`top: 0; bottom: 0; margin-block: auto`** y le
   colgaba el alto a `aspect-ratio`. **Y `aspect-ratio` NO GANA A DOS INSERCIONES:**
   con `top` y `bottom` los dos puestos, el alto de un elemento absoluto **se resuelve
   por los insets** y deja de ser automático ⇒ el poste medía el panel ENTERO, y el
   `<img>` con `height: 100%` estiraba el WebP dentro.
   📐 Medido por Lupe con el árbol ya sellado: **3840×2160 → poste de 132×2160** (×3,77)
   · 2560×1440 → 132×1440 · y a 2560×400 al revés, **aplastado** a 132×400.
   *"La bola de arriba sale como un huevo."* **255 de 255 filas deformadas; en el ANTES,
   0 de 188.** Viola R-425 (*«el asset se pinta a su tamaño nativo, el CSS no lo estira»*).
   🔑 Y LA LECCIÓN, que es de LEY 19 y me toca: **el comentario que había aquí decía
   «la silueta nativa del WebP: jamás se deforma». Era cierto sobre la INTENCIÓN, y por
   eso nadie fue a comprobar el EFECTO** — yo incluido, teniendo la columna «poste alto»
   impresa en mi propio arnés.

   🔴🔴 Y LO PEOR NO ES EL FALLO: ES QUE **DEPENDÍA DEL MOTOR**, y por eso hay que
   contarlo entero. Medido el mismo árbol (`8f1c4a6`), el mismo arnés y el mismo caso:
       Chrome 152 ....... 132×573   ratio 4,3409  → silueta NATIVA
       el motor de Lupe .. 132×2160  ratio 16,36   → DEFORMADO
   **Las dos medidas son ciertas.** Con `top` y `bottom` a la vez, `height:auto` y
   `aspect-ratio`, **quién gana —la relación de aspecto o las inserciones— no está
   decidido igual en todos los motores.** ⇒ Yo publiqué un verde honesto: en MI
   navegador el poste medía 132×573. **Y en el televisor de Harry habría salido un fideo.**
   📌 Es la LEY 6 con el binario cambiado de sitio: *al comparar dos medidas del mismo
   hecho, la primera pregunta no es cuál está mal, sino contra qué BINARIO corrió cada
   una* — y aquí el binario es el navegador, que en esta casa no lo elegimos nosotros
   (el TV del salón, la tablet Android del demo, el iPad del prospecto).
   ⇒ **Por eso el arreglo de abajo no es «otra forma de centrar»: es la forma que NO
     admite interpretación.** Con `bottom:auto` el alto no puede resolverse por insets
     en ningún motor, y con `height:auto` en la imagen el WebP no puede estirarse aunque
     alguien se equivoque otra vez. **Una geometría que depende de qué motor la lea no
     es una geometría: es una apuesta.**

   ✅ AHORA SON **DOS PUERTAS**, y hacen falta las dos (LEY 14):
   ① **El centrado no usa insets opuestos.** `bottom: auto` + `translate: 0 -50%`, así el
      alto no puede resolverse por inserciones en ningún navegador. Y `translate` es
      propiedad PROPIA (Transforms 2), no `transform`: se compone con el `translateX` de
      `@keyframes tv-turn-call-pole-l`/`-r` en vez de comérselo, así que el poste sigue
      DESLIZÁNDOSE al entrar. (Medido en la entrada pausada: translateX −70 → 0 con el
      aire bajando 80,22 → 36,01 → 14.) ⛔ No lo cambies a `transform: translateY(-50%)`:
      eso sí mata la entrada.
   ② **La imagen no puede estirarse aunque la caja se deforme** — abajo, `height: auto`.
      Es la red: si algún día alguien devuelve un `bottom`, el poste saldrá descolocado
      —visible— en vez de convertido en un fideo, que es la forma muda de romperse. */
.tv-turn-call-pole {
    position: absolute; top: 50%; bottom: auto; margin-block: 0;
    translate: 0 -50%;
    width: var(--tvc-pole-w); height: auto;
    aspect-ratio: 132 / 573;     /* la silueta nativa del WebP */
    z-index: 2; filter: drop-shadow(0 14px 26px rgba(0,0,0,.55));
}
/* ⚠️ Se mueve `left`/`right`, NUNCA la animación: los postes entran con
   `translateX` (ver `tv-turn-call-pole-l`/`-r`) y las dos cosas se componen sin
   estorbarse. Convertir esa entrada a left/right rompería el deslizamiento. */
.tv-turn-call-pole.left { left: var(--tvc-pole-x); }
.tv-turn-call-pole.right { right: var(--tvc-pole-x); }
/* Escalón 1-2 · el poste del socio, recortado a su silueta (163×707, 273 KB).
   El 80,5% del archivo viejo era transparencia que el Raspberry Pi descodificaba
   para nada, 24 veces por bucle. La imagen llena su caja: cero recortes. */
.tv-turn-call-pole picture { display: block; width: 100%; height: 100%; }
/* 📌 ÍTEM 1311: la imagen llena su caja (`100%`) en vez de llevar 132×573 quemados
   en un segundo sitio. Antes el ancho estaba escrito DOS veces —aquí y en
   `--tvc-pole-w`— y podían discrepar sin que nada fallara; ahora hay una sola fuente.
   ⚠️ Y AQUÍ SÍ HAY UNA CONCESIÓN, DICHA EN VOZ ALTA: R-425 manda *"el asset se pinta
   a su tamaño nativo, el CSS no lo estira"*, porque minificar un WebP ANIMADO de
   franjas finas es la causa raíz del moiré que el ítem 1230 vino a matar. Una escala
   CONTINUA no se puede pre-exportar, así que entre ~1690 y ~1976 px de ventana el
   compositor sí reescala (a 66–84% del nativo, nunca por debajo: más abajo el poste
   no sale). Se miró a esos anchos, no se razonó. Si alguien ve moiré ahí, el arreglo
   es exportar un segundo WebP y no volver a clavar un número en este sitio.

   🔒 SEGUNDA PUERTA DEL BLOQUEANTE DE LUPE — **`height: auto`, NUNCA `100%`.**
   Aquí decía `height: 100%`, y ES LO QUE DE VERDAD ESTIRÓ EL POSTE cuando la caja de
   arriba se resolvió por insets: la imagen obedeció a una caja de 2160 px de alto y
   pintó un fideo. Con `height: auto` la imagen deriva su alto de su PROPIA relación
   de aspecto intrínseca ⇒ **es físicamente incapaz de deformarse**, pase lo que pase
   con su contenedor. ⛔ No se lo devuelvas a `100%` ni a `object-fit`: `object-fit`
   arregla el aspecto pero deja la caja mintiendo sobre su tamaño, y de esa caja
   cuelgan el `drop-shadow` y el centrado. */
.tv-turn-call-pole img { width: 100%; height: auto; display: block; }

/* ══ 💈 R-425 — EL POSTE BONITO ES EL DE MARKETING; EL DE CSS, EL DE SISTEMA ══
   Regla del dueño (2026-08-24), verbatim: *"ese de CSS puede ser para cuando se
   cae la página pero no para los anuncios"* · *"el de CSS no se ve bien, hay que
   reemplazarlo, prioridad 1"*.
   ⇒ **Toda superficie que VENDE lleva el poste del socio** (el WebP animado):
      el llamado de turno, el anuncio de la casa del TV board, las bienvenidas.
   ⇒ 🔴 **ENMENDADO POR EL ÍTEM 1230 (2026-09-08): YA NO QUEDA NINGÚN POSTE
      DIBUJADO EN CSS.** Decía aquí que quedaba "solo para pantallas de SISTEMA"
      (el modal de reconexión, `bsp-pole-sm`). Héctor lo mandó borrar: *"no quiero
      que vuelva a salir en ningún lugar"*. Ese modal lleva el poste del socio.
   Ata con el principio 222 de íconos hiperrealistas: si los íconos son
   hiperrealistas nos sentimos más cerca del mundo que nos rodea.

   🪜 LA ESCALERA DE RESCATE SE FUE (el dueño 2026-08-24): *"olvídate de los
   dispositivos lentos, eso lo decidimos el socio y yo :)"* — ellos compran el
   hardware, así que el Raspberry Pi que justificaba los escalones 3-4 (los dos
   postes dibujados en CSS y su variante quieta) **no existe en producción**. Con
   el fantasma fuera, se borró el camino entero: `@keyframes pb-spin`,
   `.tv-turn-call-pole-still` y las medidas de 132×573 de las piezas `.pb-*`.
   Menos código, menos ramas, UN solo poste.

   📦 Y LA IMAGEN SE ABARATÓ IGUAL, porque es lo correcto (el TV corre 24/7):
   el WebP animado se re-exportó AL TAMAÑO EXACTO QUE SE PINTA (163×707 → 132×573)
   con remuestreo Lanczos por cuadro — mejor filtrado que el bilineal del
   compositor al escalar en vivo, y menos bytes por el cable. Ver el informe del
   lote para el peso antes/después.

   ══ 🪦 AQUÍ VIVÍA EL MINI-POSTE DE CSS — BORRADO EN EL ÍTEM 1230 ════════
   (Héctor, 2026-09-08): *"hay que borrar el feo en movimiento, no quiero que
   vuelva a salir en ningún lugar"* · *"prefiero el bonito en movimiento"*.
   Aquí estaban `.tv-turn-call-pole-b`, `.bsp-pole-sm`, `.pb-knob`, `.pb-cap`,
   `.pb-tube` (+ su `::after`) y las `@@keyframes pb-spin-sm`: el poste DIBUJADO
   que pintaba el modal de reconexión desde el 2026-08-22. **Borrados enteros.**
   ⇒ Ese modal lleva ahora el poste del socio — ver `.bsp-reconnect-pole` (arriba
     del todo en este fichero) y `ReconnectModal.razor.js`.
   ⚠️ **ESTO ENMIENDA R-425**: el párrafo de más arriba decía que el poste de CSS
     se quedaba "solo para pantallas de SISTEMA". Ya no se queda en ninguna.
   📌 SI OTRO COMENTARIO TE MANDÓ AQUÍ BUSCANDO `.pb-tube`: ya no existe. La
     técnica de RELIEVE que aquel tubo inventó (sombras interiores + barrido de
     cristal en `::after`) sigue viva y en uso en `.tv-turn-call-band`, que es
     donde se le copió — ahí está explicada. */

/* 💈 EL POSTE DEL SLIDE DE LA CASA (R-425). Es el MISMO poste del socio.
   📏 TAMAÑO AJUSTADO POR EL DUEÑO (2026-08-25), viendo el slide en DEV:
   *"creo que el pole se puede poner 50% más pequeño, como que ahí está muy
   grande"* ⇒ de 74×320 baja a LA MITAD: **37×160**. El slide recupera aire y el
   poste vuelve a ser la firma de marca en vez de tapar el mensaje.
   🔑 Y EL WEBP SE RE-EXPORTÓ A ESE TAMAÑO EXACTO (`pole-spin-house.webp`,
   37×160), no se encogió por CSS: pedirle al compositor que minifique un
   ANIMADO con franjas finas es exactamente la causa raíz del moiré que este
   lote vino a arreglar. Achicar la caja sin re-exportar habría reintroducido el
   bug con otro nombre. Se re-muestreó desde el ORIGINAL de 163×707 (nunca desde
   el de 74×320: remuestrear lo ya remuestreado acumula pérdida).
   📦 Y sale casi 47 KB más barato: 85 930 B → 39 338 B.
   ⚠️ SI ALGUIEN CAMBIA ESTAS MEDIDAS, HAY QUE RE-EXPORTAR EL WEBP. La regla de
   esta casa es "el asset se pinta a su tamaño nativo", no "el CSS lo estira". */
.tv-house-pole { display: flex; justify-content: center; margin-bottom: .5rem; }
.tv-house-pole img { width: 37px; height: 160px; display: block;
    filter: drop-shadow(0 5px 10px rgba(0,0,0,.55)); }
/* ══ EL POSTE SOLO SALE CUANDO DE VERDAD CABE ═════════════════════════════════
   📱 Antes este corte estaba en 1199 px "porque el poste no cabe por debajo". La
   frase era cierta; el NÚMERO estaba mal, y el ítem 605 lo destapó al medir la
   cuenta entera. 🩸 ENTRE 1200 Y 1855 PX EL POSTE TAMPOCO CABÍA Y SE ENSEÑABA
   IGUAL: se metía DEBAJO del bloque del nombre (a 1600 px de ventana, ~106 px de
   solape por lado). No se veía roto de milagro — el bloque tiene más `z-index` y
   el texto le pasa por encima — pero el poste estaba ahí atrás, tapado.
   🔴 Y ES LA BANDA DONDE LA FÓRMULA NUEVA, SOLA, SERÍA PEOR: el `calc` se vuelve
   NEGATIVO y el poste se saldría del panel (encima de la barra de la cola a la
   izquierda, fuera de pantalla a la derecha). Por eso este umbral y la fórmula
   son UNA SOLA COSA: la fórmula acerca el poste al nombre donde hay sitio; este
   corte lo retira donde no lo hay. **El adorno se va; el nombre jamás.**

   📐 DE DÓNDE SALE EL NÚMERO (la misma cuenta de `--tvc-pole-x`, resuelta):
     · el panel de anuncios es el 75% de la ventana (`.tv-ads` es el flex que se
       queda con lo que sobra tras el 25% de la cola — MEDIDO: 1440 px a 1920);
     · el poste cabe junto al nombre cuando  panel/2 ≥ 1100/2 + 132 + 14 = 696
       ⇒ panel ≥ 1392 px ⇒ ventana ≥ 1392 / 0,75 = **1856 px**.
   🔴 ESAS CUATRO LÍNEAS DE ARRIBA SON LA CUENTA VIEJA Y LAS DOS PREMISAS SON FALSAS
   (ítem 1311, 2026-09-13). **Se dejan escritas a propósito, tachadas, porque el
   siguiente que mire va a rehacer exactamente este razonamiento:**
     · «el panel es el 75% de la ventana» → NO: a 1920 el panel mide **1397,63**, no
       1440. El `padding` y el `border-right` de `.tv-queue` no son `border-box` y se
       suman a su 25%. El «MEDIDO: 1440» de arriba nunca se midió con el markup real.
     · «ventana ≥ 1856» → el corte correcto con el panel real es **1699** (abajo).
   ⚠️ NO SE PUEDE PONER `var()` EN UNA MEDIA QUERY — el CSS no lo permite, y meter
   un `calc()` aquí arriesga que un navegador viejo tire la regla ENTERA y deje el
   poste suelto. Por eso el número va escrito, con su cuenta al lado.
   📌 EL CORTE NO SE PONE EN 1920 —donde el poste cabría con sus 24 px de orilla
   completos— Y HAY UNA MEDICIÓN DETRÁS: una ventana de 1920 px reporta
   `innerWidth = 1904` en cuanto aparece la barra de desplazamiento. Con el corte
   en 1920, un televisor 1080p se quedaría SIN POSTES por 16 píxeles.

   ══ 🔴 ÍTEM 1311 (2026-09-13) — ESTE NÚMERO ERA 1855.98 Y ESTABA MAL ═════════
   🩸 Aquí decía que con el corte en 1855,98 el poste pisaba «como mucho 10 px de
   la caja del bloque» entre 1856 y 1920. **Ese 10 salía de `panel = 0,75 × ventana`,
   y ese panel no existe.** Medido con el markup real: `.tv-queue` no es `border-box`,
   así que sus 2×19,2 px de `padding` y sus 4 px de `border-right` se SUMAN a su 25%
   ⇒ **el panel es 42,37 px más estrecho de lo que decía la cuenta** (a 1920: 1397,63,
   no 1440). El solape real no era 10 px: era **−31,19 px a 1856 y −7,19 px a 1920**,
   o sea **el televisor 1080p de producción llevaba el poste metido debajo del bloque
   del nombre**. Mismo fallo mudo del 605, mismo `z-index` tapándolo, un año después.
   ⇒ **LA LECCIÓN, y por eso ya no se calcula así: un número derivado de un panel
     SUPUESTO se lee igual de bien que uno derivado de un panel MEDIDO.** El panel
     no se deduce de la ventana — el `padding`, el `border`, el `min-width: 260px` y
     `--tvcw` (la cola, configurable del 25% al 50%) están todos en medio.

   ✅ POR ESO EL MECANISMO YA NO ES ESTE CORTE: es `--tvc-pole-w`, que se calcula
   contra el PANEL y no puede solapar por construcción (arriba, con su cuenta).
   **Este corte pasa a ser lo que R-605 pide y la fórmula no sabe hacer: RETIRAR el
   adorno** cuando ni encogido merece la pena — y de paso ahorrarle al navegador
   descodificar un WebP animado de 24 fotogramas que mediría cuatro píxeles.

   📐 EL NÚMERO NUEVO, con la misma cuenta pero sobre el panel medido:
     · el poste deja de ser un poste por debajo de ~28 px de ancho (≈122 de alto);
     · cabe con 28 px cuando  panel ≥ 1100 + 2×(28 + 14 + 24) = 1232 px;
     · panel = 0,75 × ventana − 42,37  ⇒  ventana ≥ (1232 + 42,37) / 0,75 = **1699 px**.
   ⇒ `max-width: 1698.98px`. **Sale en MÁS anchos que antes, nunca en menos**: la
     franja 1699–1856, donde hoy no salía ninguno, ahora lleva poste de 28 a 87 px
     CON su aire; y 1856–1976, donde salía solapando, ahora encoge y deja aire.
   ⚠️ NO SE PUEDE PONER `var()` EN UNA MEDIA QUERY — el CSS no lo permite, y meter un
   `calc()` aquí arriesga que un navegador viejo tire la regla ENTERA. Por eso el
   número va escrito, con su cuenta al lado.
   👉 SI ALGUIEN CAMBIA `--tvc-w`, `--tvc-air` O `--tvc-edge`, ESTE 1698.98 HAY QUE
   REHACERLO — pero **olvidarse ya no produce un solape**, solo un poste que sale o
   no sale un poco antes: de eso se encarga la fórmula. Ese es el cambio de fondo.

   🔬 LO QUE ESTE CORTE NO PUEDE VER, dicho para que nadie se fíe de él solo:
   mide la VENTANA, y el sitio lo decide el PANEL. Con la cola al 50% (`.tv-c5`) a
   1920 px el corte deja pasar el poste y el panel solo mide 917 px; con una pantalla
   muy alta el `padding` de la cola crece (es `vh`) y el desfase pasa de 42 a 81 px.
   **En los dos casos quien protege es la fórmula, que mide el panel.** Lo peor que
   puede pasar es un poste pequeño donde antes había uno solapado. Nunca peor que hoy. */
@media (max-width: 1698.98px) {
    .tv-turn-call-pole { display: none; }
}

/* ── El bloque de contenido ────────────────────────────────────────────────
   1100 px MEDIDO: el panel de anuncios son 1440 px y los postes se comen
   2×(132+24) = 312 ⇒ quedan 1128. Con 1100 el nombre NUNCA toca el poste
   (14 px de aire a cada lado). 📌 Con UN solo poste el ancho NO sube: el bloque
   va centrado en el panel, así que crecer lo metería debajo del poste izquierdo.
   El `min()` es lo único que no está en la receta: mantiene el cartel dentro de
   la caja en la tableta con la que se prueba (a 1920 da exactamente 1100 px).
   🔑 ÍTEM 605 — ESTE ANCHO YA NO SE ESCRIBE AQUÍ: vive en `--tvc-w`, arriba, y de
   ahí lo lee TAMBIÉN la fórmula que coloca los postes. Estaban en dos sitios sin
   atar y por eso el poste se quedó en la orilla en el televisor 4K. **Cambiar el
   1100 ahora mueve las dos cosas a la vez; es imposible que discrepen.** */
.tv-turn-call-inner { position: relative; z-index: 3; width: var(--tvc-w); padding: 2rem 0; }
/* Rótulo = PASTILLA ROJA con texto blanco (5,89:1). Antes era texto ámbar
   suelto. 📌 El ámbar #FCD34D NO desaparece de la app: se queda donde ya estaba,
   en la marca "¡Ya es tu turno!" de la lista. Aquí el cartel habla en
   navy + rojo + blanco para no gastar el ámbar en dos sitios a la vez. */
.tv-turn-call-kicker { margin-bottom: 1.5rem; }
.tv-turn-call-kicker span {
    display: inline-block; text-transform: uppercase; letter-spacing: .2em; font-weight: 800;
    line-height: 1; color: #fff; background: var(--tvc-red);
    font-size: clamp(1.05rem, 2.5vw, 2.5rem);   /* tope 40 px = la receta a 1080p */
    padding: .62rem 1.7rem .58rem; border-radius: 999px;
    box-shadow: 0 6px 20px rgba(200,16,46,.45);
}
.tv-turn-call-name {
    color: var(--tvc-name); font-weight: 900; line-height: 1.02; overflow-wrap: anywhere;
    font-size: clamp(3rem, 11vw, 11rem); text-shadow: 0 6px 30px rgba(0,0,0,.45);
}
/* ── ESCALÓN POR LARGO DE NOMBRE — en vez de partirlo a media palabra ───────
   `overflow-wrap:anywhere` evita el desborde, pero un nombre de 15 caracteres en dos
   líneas NO se lee de un vistazo desde el fondo del salón, y de un vistazo es el
   único trabajo que tiene este cartel. A 132 px la caja de texto sigue midiendo
   ~8 cm en un televisor de 55" a 1080p: no se degrada nada, se evita una línea
   partida. El corte es por CONTEO DE CARACTERES del nombre ya abreviado — sin
   JS de medición, sin magia.
   🔴 2026-09-14 (ficha 1316) — AQUÍ DECÍA «ESTE TV NO CORRE INTER» Y ERA FALSO.
   Decía que la línea 2 de este archivo fija Helvetica/Arial con el peso 900
   SINTETIZADO. **Desde el ítem 876 / LV-31 este archivo hace lo contrario:** los
   `@font-face` de las líneas 15-30 sirven Inter SELF-HOSTED y la línea 52 pone
   `html, body { font-family: 'Inter', … }`. El televisor corre Inter.
   ⚠️ Y esa premisa falsa CALIBRÓ estos cortes (LEY 19: un comentario que suena
   medido apaga la siguiente mirada). Re-medido con Inter: el nombre de 12 car. a
   176 px sigue partiéndose (1.144 px), pero el de 15 car. a 132 px **CABE**
   (1.064,13 px, no 1.151,3) ⇒ el salto a 110 px en 13 caracteres es más agresivo
   de lo necesario, y por encima de 110 px **no hay escalón**: 20 caracteres se
   parten SIEMPRE. Los cortes se quedan en 9/12 hasta que se decida la ficha 1316.
   📖 La medición completa, el arnés para rehacerla y el reparto por largo:
   TvBoardView.razor → `NameStep`, y `tools/arnes-1316-paul/`. */
.tv-turn-call-name.step2 { font-size: clamp(2.6rem, 8.25vw, 8.25rem); }   /* 10–12 caracteres → 132 px */
.tv-turn-call-name.step3 { font-size: clamp(2.2rem, 6.9vw, 6.875rem); }  /* 13 o más        → 110 px */
.tv-turn-call-with {
    color: var(--tvc-with); font-weight: 600; margin-top: 1rem;
    font-size: clamp(1.2rem, 3.4vw, 3.5rem);   /* tope 56 px = la receta a 1080p */
}

/* ══ ÍTEM 590 (el dueño 2026-08-04) — LA FOTO DEL BARBERO EN EL CARTEL ════════
   *"podemos mostrar la foto de cada barbero también"*. La foto ACOMPAÑA, NO
   MANDA: el nombre conserva su tamaño (176 px con uno solo) y es el que se lee
   desde el fondo del salón; la foto pone cara a la silla.
   · Cascada de la casa: foto guardada → MONOGRAMA de iniciales. Un barbero sin
     foto jamás deja un hueco roto (mismo patrón de BarberCards y la lista).
   · `object-position: 50% 15%` = ítem 280: A UN BARBERO NO SE LE CORTA LA CARA.
   · Sin barbero identificable no se pinta nada — no se inventa una cara. */
.tv-turn-call-entry {
    display: flex; align-items: center; justify-content: center;
    gap: var(--tvc-gap); text-align: left;
}
.tv-turn-call-face { flex: 0 0 auto; }
.tv-turn-call-text { min-width: 0; }
.tv-turn-call-photo {
    width: var(--tvc-photo); height: var(--tvc-photo);
    object-fit: cover; object-position: 50% 15%;
    border-radius: 50%; border: 4px solid var(--tvc-photo-ring);
    box-shadow: 0 6px 24px rgba(0,0,0,.4); background: var(--tvc-photo-bg);
    display: flex; align-items: center; justify-content: center;
}
.tv-turn-call-mono {
    font-weight: 900; color: #fff; letter-spacing: .02em;
    font-size: calc(var(--tvc-photo) * .36);
}

/* ══ 🔑 CON UN SOLO NOMBRE EL BLOQUE SE APILA — Y ESTÁ MEDIDO ═══════════════
   Arregla DOS cosas de una vez, y ninguna es cosmética:
   1. 🔴 UN NOMBRE LARGO YA SE PARTÍA HOY, sin postes ni nada: con la foto AL
      LADO la caja del nombre eran 1.078 px y el nombre gasta ~99 px por
      carácter ⇒ un nombre de 12 caracteres mide 1.147 px y se iba a DOS LÍNEAS. Con
      los postes la caja bajaría a 872 px y ni un nombre de 9 cabría.
      Apilado, el nombre se lleva los 1.100 px ENTEROS.
   2. 💡 LA CARA DEL BARBERO PARECÍA LA DEL CLIENTE: pegada al nombre del
      cliente, de lejos se lee como su foto. Bajada junto a "con [barbero]",
      la cara dice lo que de verdad es.
   📌 Con 2 o 3 nombres NO hace falta y NO se toca: a 96 px un nombre de 15 caracteres
   mide 777 px y con la foto al lado suma 891 de 1.100 — la fila sobra de ancho. */
.tv-turn-call-n1 .tv-turn-call-entry { flex-direction: column; gap: 1.1rem; text-align: center; }
.tv-turn-call-n1 .tv-turn-call-text { width: 100%; }
.tv-turn-call-n1 .tv-turn-call-name { text-align: center; }
.tv-turn-call-n1 .tv-turn-call-with { margin-top: 0; }
.tv-turn-call-withrow { display: flex; align-items: center; justify-content: center; gap: 1.1rem; }

/* ── La vida del cartel: entra · aguanta · sale, en UNA línea de tiempo ─────
   Su duración ES la del cartel (`--tvc-dur` ← `Tv:TurnCallSeconds`), así que
   cuando el temporizador lo saca del DOM ya llegó sola a opacidad 0.
   3,5% = 350 ms de entrada · 96%→100% = los últimos 400 ms de salida (a 10 s). */
@keyframes tv-turn-call-life {
    0%   { opacity: 0; transform: scale(.955); }
    3.5% { opacity: 1; transform: scale(1); }
    96%  { opacity: 1; transform: scale(1); }
    100% { opacity: 0; transform: scale(.985); }
}
/* Los postes entran 250 ms DESPUÉS que el nombre (deslizan desde su borde hasta
   el 6% de la línea de tiempo). Deliberado: PRIMERO SE LEE, DESPUÉS SE ADORNA. */
@keyframes tv-turn-call-pole-l {
    0%   { opacity: 0; transform: translateX(-70px); }
    6%   { opacity: 1; transform: none; }
    96%  { opacity: 1; transform: none; }
    100% { opacity: 0; transform: translateX(-34px); }
}
@keyframes tv-turn-call-pole-r {
    0%   { opacity: 0; transform: translateX(70px); }
    6%   { opacity: 1; transform: none; }
    96%  { opacity: 1; transform: none; }
    100% { opacity: 0; transform: translateX(34px); }
}
.tv-turn-call-pole.left  { animation: tv-turn-call-pole-l var(--tvc-dur) ease-out both; }
.tv-turn-call-pole.right { animation: tv-turn-call-pole-r var(--tvc-dur) ease-out both; }
/* Un nombre que se SUMA a un cartel ya puesto entra solo, sin reiniciar la línea
   de tiempo del cartel (el `@key` del contenedor lo garantiza): el cartel CRECE,
   no parpadea. Esta animación solo dispara en el nodo nuevo. */
@keyframes tv-turn-call-entry-in {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}
.tv-turn-call-entry { animation: tv-turn-call-entry-in .25s ease-out both; }
/* ══ ÍTEM 590 (el dueño 2026-08-04) — EL CARTEL COMPARTIDO, HASTA 3 NOMBRES ════
   *"¿No podemos hacer un cartel dividido, por ejemplo de 3 en 3?"*. Con una fila
   pura el tercer cliente ve su nombre 20 s tarde; agrupados lo ven LOS TRES A LA
   VEZ.
   🔑 EL CASO NORMAL NO SE TOCA: sin la clase `-n2`/`-n3` manda la regla de
   arriba, o sea el nombre GRANDE de siempre (176 px medidos en 1080p). La letra
   solo baja cuando de verdad coinciden.
   ⚠️ POR QUÉ EL TOPE ES 3 Y NO 4: MEDIDO en 1920×1080 — 1 nombre 176 px ·
   2 nombres 115,2 px · 3 nombres 96 px. En un televisor de 55" a 1080p un píxel
   son ~0,63 mm, así que 96 px son ~60 mm de caja de texto: se lee de pie desde el
   fondo de una sala de espera. Con 4 caería por debajo de 80 px y dejaría de ser
   "de un vistazo" para volverse "acércate a leer" — que es justo lo que este
   cartel existe para evitar. El ajuste `Tv:TurnCallMaxPerPoster` no deja pasar de 3. */
.tv-turn-call-entry + .tv-turn-call-entry {
    margin-top: 1rem; padding-top: 1rem; border-top: 2px solid var(--tvc-rule);
}
.tv-turn-call-n2 { --tvc-photo: 120px; --tvc-gap: 1.3rem; }
.tv-turn-call-n3 { --tvc-photo: 96px;  --tvc-gap: 1.1rem; }
.tv-turn-call-n2 .tv-turn-call-name { font-size: clamp(2.4rem, 7.6vw, 7.2rem); }
.tv-turn-call-n2 .tv-turn-call-with { font-size: clamp(1.1rem, 2.6vw, 2.2rem); margin-top: .35rem; }
.tv-turn-call-n3 .tv-turn-call-name { font-size: clamp(2rem, 6vw, 6rem); }
.tv-turn-call-n3 .tv-turn-call-with { font-size: clamp(1rem, 2.2vw, 1.9rem); margin-top: .25rem; }
/* 🔑 CON DOS O TRES NOMBRES SE ALINEAN A LA IZQUIERDA, NO CENTRADOS. Un nombre
   grande se ve de un vistazo; TRES HAY QUE LEERLOS, y tres filas centradas dejan
   el borde izquierdo dentado — el ojo tiene que buscar dónde empieza cada una.
   Con un borde común baja en línea recta. El rótulo se queda centrado (hereda el
   `text-align:center` del cartel): es el título, no una fila que se escanee. */
.tv-turn-call-n2 .tv-turn-call-entry,
.tv-turn-call-n3 .tv-turn-call-entry { justify-content: flex-start; }

/* ══ ÍTEM 590 — LA MARCA "¡YA ES TU TURNO!" EN LA LISTA DEL TV ══════════════
   El cartel dura 10 s; el que estaba mirando el celular se lo pierde. Esta marca
   se queda 30 s junto al nombre (configurable, `Tv:TurnMarkSeconds`).
   Va DESPUÉS de `.tv-queue-row.next` a propósito: misma especificidad, gana la
   última — una fila llamada se ve llamada aunque además sea la primera.
   `inset box-shadow` en vez de `border`/`outline` porque `.tv-queue-list` tiene
   `overflow:hidden` y porque un borde real movería la altura de la fila.
   🔴 ÍTEM 1103 (Héctor, 2026-09-03): desde ese ítem, `.razor` enciende `.next` Y
   `.called` con LA MISMA condición (`called`) — las dos SIEMPRE coinciden ahora,
   nunca una sin la otra. Antes `.called` traía su PROPIO `background` opaco (16%
   de amarillo) que, por venir DESPUÉS en el cascade con la misma especificidad,
   TAPABA por completo el degradado rojo de `.next` — la fila llamada se veía
   amarilla translúcida, no roja, que es justo el bug que reportó Héctor (un
   cliente en turno "salió con el reloj, no con las tijeras" — la fila tampoco
   salía roja). Se quita
   el `background` de aquí a propósito: el rojo de `.next` queda como base y esta
   regla solo aporta el ANILLO (box-shadow) — la fila llamada ahora se ve ROJA con
   un aro dorado, no amarilla plana. */
.tv-queue-row.called {
    box-shadow: inset 0 0 0 2px rgba(252,211,77,.85), 0 0 16px rgba(252,211,77,.28);
    /* MEDIDO: en un televisor 1080p la marca y el nombre caben de sobra en una
       línea (nombre 200 px + marca 181 px en una columna de 522). Pero si alguien
       abre el TV en una tableta o en una pantalla vertical, la columna baja a
       ~300 px y la fila DESBORDABA. Con `wrap` la marca se baja sola a su propia
       línea en vez de empujar el nombre fuera de la caja. */
    flex-wrap: wrap; row-gap: .3rem;
}
.tv-queue-row.called .tv-turn { background: #FCD34D; color: #14224E; }
/* Con la marca al lado, el que cede espacio es el NOMBRE (sigue con su ellipsis
   de siempre): la marca jamás se parte ni se sale de la columna. */
/* ÍTEM 610: el suelo del nombre en `em` (2,8 × su propia letra = los 3,5rem de
   antes a 1080p) para que suba con la fila en 4K en vez de quedarse anclado a la
   raíz — si no, en 4K este mínimo dejaría de proteger nada. */
.tv-queue-row.called .tv-name { flex: 1 1 auto; min-width: 2.8em; }
.tv-turn-now {
    /* Sin `margin-left:auto` a propósito: el que crece es el NOMBRE
       (`flex:1 1 auto`), así la marca queda pegada a la derecha sola — y cuando
       la fila tiene que envolverse, la marca empieza a la IZQUIERDA de su línea
       en vez de quedar suelta en una esquina. */
    flex: 0 0 auto; white-space: nowrap;
    /* 1rem MEDIDO: la marca ocupa 181 px de los 522 de la columna y aún deja
       200 px al nombre — un nombre abreviado de 15 caracteres
       cabe sin puntos suspensivos. Más grande empezaría a comerse el nombre;
       más chica se pierde de lejos, que es justo lo que esta marca evita. */
    /* ÍTEM 610: la marca sube CON la fila (misma recta en `vh` que el nombre, con
       el 1rem de hoy como suelo). Si el nombre doblara en 4K y la marca no, la fila
       quedaría descuadrada — y esta marca existe justo para verse de lejos.
       El relleno pasa a `em` para que la pastilla crezca con su texto. */
    /* R-313 · ×1,5610 — la pastilla sube CON la fila por la misma razón que ya
       decía el ítem 610: si el nombre dobla y la marca no, la fila se descuadra,
       y esta marca existe justo para verse de lejos.
       El `padding` en `em` y el `border-radius:999px` NO se tocan: ya escalan. */
    font-size: clamp(1.561rem, 2.312vh, 3.122rem); font-weight: 900; letter-spacing: .03em;
    color: #14224E; background: #FCD34D; border-radius: 999px; padding: .16em .55em;
    animation: tv-turn-now-pulse 1.6s ease-in-out infinite;
}
@keyframes tv-turn-now-pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.06); }
}
/* Accesibilidad: quien pidió menos movimiento igual ve el cartel — lo que se
   quita es el movimiento, JAMÁS el mensaje. El cartel entra y sale igual (lo
   saca el temporizador del componente, no el CSS).
   ✅ R-425 — ESTO YA SE CUMPLE DE VERDAD, Y SIN ESCALERA. Antes decía aquí que
   con el WebP animado NO se podía cumplir (un WebP animado no se para desde CSS)
   y que la única salida era bajar al escalón `still`. Ese escalón se retiró y el
   problema se resolvió donde tocaba, en el MARKUP: el poste va en un `<picture>`
   con `<source media="(prefers-reduced-motion: reduce)">` que sirve el FOTOGRAMA
   QUIETO — y el navegador baja UNO SOLO de los dos archivos, así que respetar la
   preferencia no cuesta bytes de más a nadie.
   (Ver `TvBoardView.razor`: el cartel de llamado y el slide de la casa.) */
@media (prefers-reduced-motion: reduce) {
    .tv-turn-call { animation: none; }
    .tv-turn-call-entry { animation: none; }
    .tv-turn-call-pole.left, .tv-turn-call-pole.right { animation: none; }
    /* ÍTEM 1230: el poste del modal de reconexión YA NO se para aquí — es el
       WebP del socio y se resuelve en el MARKUP (`<picture>` + `media`), igual
       que el llamado de turno. Un WebP animado no se para desde CSS. */
    .tv-turn-now { animation: none; }
}

/* KIOSCO fullscreen: tablet sellada, salir con codigo */
/* FIX Vera/QA (2026-07-23, regla 238): padding:1rem (~16px) dejaba solo 20px
   de holgura real sobre el primer elemento (.kbs-crumb-btn) en 1024x600 —
   MISMA lección que rompió el kiosko antes, la regla dura exige ≥49px.
   El mockup aprobado (mockup-kiosko-barbero-servicios.html .kiosk-wrap) usa
   padding-top:52px — se sube SOLO el top a esa medida (izq/der/abajo intactos,
   0 impacto en el resto del layout); verificado con getBoundingClientRect en
   vivo: 52px + 4px de margin-top propio de .kbs-crumb = 56px de holgura real. */
.kiosk-root { position: fixed; inset: 0; background: var(--page-bg, #F8FAFC); color: var(--text-color, #0F172A); z-index: 1000; overflow: auto; padding: 52px 1rem 1rem; }
.kiosk-exit { position: fixed; right: 14px; bottom: 14px; z-index: 1001; opacity: .45; }
.kiosk-exit:hover { opacity: 1; }
/* ÍTEM 170: botón directo de idioma del kiosco — ARRIBA a la derecha (el
   candado de salida vive abajo a la derecha); grande, dedo-amigable */
.kiosk-lang { position: fixed; top: 14px; right: 14px; z-index: 1001;
    font-size: 1.05rem; padding: .55rem 1rem; touch-action: manipulation; }

/* ÍTEM 77 (mockup + Opción 1): CARD DE CITA de DOS FILAS —
   fila 1 columnas de datos, fila 2 barra de acciones a lo ancho. Sin scroll
   horizontal; en móvil todo apila con wrap. */
.appt-card {
    border: 1px solid var(--bs-border-color, #DEE2E6);
    border-radius: 10px;
    margin-bottom: .6rem;
    overflow: hidden;
    background: var(--card-bg, #fff);
}
.appt-card-cancelled { opacity: .72; }
.appt-card-data {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem 1.25rem;
    padding: .55rem .8rem;
    align-items: flex-start;
}
.appt-col { display: flex; flex-direction: column; min-width: 84px; }
.appt-lbl {
    font-size: .68rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    /* ÍTEM 270 cosmético (Lupe): las etiquetas DATE/TIME quedaban en 4.34:1 sobre
       el fondo glass-subtle de las cards (bajo AA). #5E6A7E sube a 4.99:1 (glass)
       / 5.47:1 (blanco) — AA cómodo en TODOS los temas (las cards son claras en
       todos: --card-bg no está definido), sin dejar de leerse muted. */
    color: var(--muted-color, #5E6A7E);
    font-weight: 600;
}
.appt-card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
    padding: .5rem .8rem;
    border-top: 1px dashed var(--bs-border-color, #E2E8F0);
    background: var(--bs-tertiary-bg, #F8FAFC);
}
.appt-sortbar { row-gap: .25rem; }

/* ══ 📅 R-434 · ÍTEM 868 — LA BARRA DE TIEMPO DE CITAS ═══════════════════════
   Esta es la pantalla que escondió la PRIMERA CITA REAL del sistema: el rango
   venía puesto en 1–31 de agosto, nadie lo había tocado, y todo salía recortado
   por ahí sin decirlo. Todo lo de este bloque existe para que el rango se VEA y
   se pueda mover CON EL DEDO.
   🎨 Los tintes son TRANSLÚCIDOS y la tinta se HEREDA — el patrón de
   `.bsp-help-trap-note`: el tinte compone sobre la superficie del tema, así que
   un solo bloque sirve para los 7 temas oscuros sin un color por tema. */

/* 👁️ R-430 — la ventana activa, dicha en voz alta y no deducida de dos campos
   de fecha diminutos. */
.appt-winbar {
    display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .6rem;
    padding: .5rem .7rem; margin-bottom: .75rem;
    border: 1px solid var(--bs-border-color, #E2E8F0); border-radius: 10px;
    background: rgba(128, 128, 128, .10);
    font-size: .82rem; line-height: 1.35;
}
.appt-win-k { color: var(--bs-secondary-color, #64748B); }
.appt-win-shape { color: var(--bs-secondary-color, #64748B); }

/* La corrección del rango y el "hay N fuera" son AVISOS, no errores: ámbar,
   nunca rojo. El barbero está mirando su agenda, no auditando un sistema. */
.appt-win-fix, .appt-win-out {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .18rem .55rem; border-radius: 999px; font-weight: 600;
    background: rgba(200, 163, 61, .18);
    border: 1px solid rgba(200, 163, 61, .55);
    color: inherit;
}

/* ══ ÍTEM 999 (Héctor 2026-08-31) — LA PÍLDORA QUE SE TOCA ═══════════════════
   El aviso pasa a ser un BOTÓN para el SuperAdmin, y **tiene que parecerlo**:
   si sigue leyéndose como una etiqueta, nadie lo va a tocar y el lote no sirve.
   Lo que lo separa del aviso mudo, y es a propósito acumulativo:
     · una LLAMADA A LA ACCIÓN escrita ("Verlas") — el texto es lo primero que
       delata que algo se puede pulsar, antes que cualquier sombra;
     · el borde y el fondo SUBEN de intensidad respecto al aviso pasivo;
     · `cursor: pointer` y un `:hover`/`:active` que responden.
   🎨 Se hereda de `.appt-win-out` en vez de re-declararse: el día que el ámbar
   del aviso cambie, el botón cambia con él. Aquí solo vive la DIFERENCIA. */
.appt-win-out-btn {
    font: inherit; cursor: pointer;
    background: rgba(200, 163, 61, .30);
    border-color: rgba(200, 163, 61, .95);
    padding: .28rem .7rem;
    /* 🩸 AUTORREPORTE (Jack, 2026-08-31, misma tarde) — ESTE BOTÓN SE SALÍA A 175%.
       Al medir la cabecera con el método bueno (`getBoundingClientRect().right`
       contra `documentElement.clientWidth`) salió que a 175% el borde derecho de
       ESTE botón caía en 429 con la ventana en 390 ⇒ 39 px fuera, recortados por
       el `overflow-x: clip` del `body` y por tanto INALCANZABLES con el dedo.
       🔴 En el reporte del ítem 999 yo había escrito *"sin desborde horizontal"*
       apoyándome en `scrollWidth > clientWidth` — que es EXACTAMENTE la métrica
       que `clip` vuelve muda. El número era real; la pregunta que contestaba, no
       era la mía. (Ley 16: un detector que se equivoca a favor de tu hipótesis no
       lo caza nadie — a mí me lo cazó medir OTRA cosa.)
       ✅ El arreglo es el mismo que el de la cabecera, y por la misma razón: que
       ENVUELVA en vez de irse de la pantalla. `max-width` lo ata al ancho de su
       fila; `flex-wrap` deja que el número y la llamada a la acción caigan a dos
       líneas dentro del propio botón. */
    flex-wrap: wrap; max-width: 100%;
}
.appt-win-out-btn:hover, .appt-win-out-btn:focus-visible {
    background: rgba(200, 163, 61, .48);
    border-color: rgba(200, 163, 61, 1);
}
.appt-win-out-btn:active { transform: translateY(1px); }
/* La CTA va dentro del mismo botón, separada por una barrita: el número explica
   POR QUÉ hay algo que hacer y la CTA dice QUÉ. Separarlos en dos elementos
   habría dejado un "Verlas" huérfano al envolver la línea en 390 px. */
.appt-win-out-cta {
    display: inline-flex; align-items: center; gap: .25rem;
    margin-left: .45rem; padding-left: .5rem;
    border-left: 1px solid rgba(200, 163, 61, .8);
    text-decoration: underline;
}
/* Cuando el botón parte en dos líneas, la barrita vertical de separación deja de
   tener sentido (ya no separa nada horizontalmente) y el margen izquierdo
   descuadra la segunda línea. Se quitan SOLO en ese caso. */
.appt-win-out-btn > .appt-win-out-cta:not(:first-child):last-child { flex: 0 1 auto; }
@media (max-width: 575.98px) {
    .appt-win-out-btn { text-align: left; }
}
/* El ⓘ viaja pegado a su botón (`.appt-chipwrap`), pero ese envoltorio es
   `inline-flex` SIN wrap: con el botón ya ancho, el ⓘ lo empujaba fuera. Que
   pueda bajar, y que el conjunto no exceda su fila. */
.appt-chipwrap { flex-wrap: wrap; max-width: 100%; }

/* ↩️ LA VUELTA (condición 2 de Héctor). Neutro a propósito: el ámbar es del
   AVISO, y esto no avisa de nada — deshace. Si fuera ámbar competiría con la
   píldora por la mirada justo cuando la píldora ya no está. */
.appt-win-undo {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .28rem .7rem; border-radius: 999px; font-weight: 600;
    font: inherit; font-weight: 600; cursor: pointer;
    background: var(--surface-color, #fff);
    border: 1px solid var(--bs-border-color, #E2E8F0);
    color: var(--bs-body-color, #14224E);
}
.appt-win-undo:hover, .appt-win-undo:focus-visible {
    background: var(--primary-color, #14224E); color: #fff;
    border-color: var(--primary-color, #14224E);
}

/* 📏 44 px DE DEDO, y colgados del PUNTERO y no del ancho — la corrección R-238
   que este mismo fichero documenta doce líneas más abajo: el MISMO teléfono
   acostado mide 844 de ancho y se caería del lado "escritorio" perdiendo los 44
   justo cuando el dedo sigue siendo el mismo dedo. */
@media (pointer: coarse) {
    .appt-win-out-btn, .appt-win-undo { min-height: 44px; }
}

/* ══ ÍTEM 1074 (Héctor: "SÍ: botón de la compuerta de reserva a 44 px con
   ícono") — el único CTA hacia adelante de GuestBooking.razor cuando el
   correo ya existe en la app ("Crear cuenta y reservar", `gb_register_btn`)
   medía 199×31 (rect) / 201×33 (táctil real, sonda por `elementFromPoint` —
   LEY 24: el rect crudo sub-reporta) con dedo real a 390×844 — por debajo
   de los 44 px que R-238 exige en TODA superficie cara-al-cliente.
   🟡 Decisión de Héctor fue el botón puntual, no la regla general que
   proponía Vera para las otras 8 apariciones de `btn-sm` en el mismo
   archivo (LEY 14: "una compuerta que enumera casos no es una compuerta")
   — esa regla más ancha sigue sin construir, reportado, no escondido.
   Mantiene su ícono (👤, ya lo tenía) — R-347/casa: botón = ícono + etiqueta. */
@media (pointer: coarse) {
    .gb-cta-44 {
        min-height: 44px;
        display: inline-flex; align-items: center; justify-content: center;
    }
}

/* ── ◀ ▶ (R-434 punto 8 / ítem 218) ──────────────────────────────────────────
   Mismo lenguaje visual que la Agenda, pero con el dedo de verdad: 44 px, no
   los 34 de `.sch-navbtn`.
   ══ ÍTEM 934 (el dueño 2026-08-28): se fueron `.appt-datenav` (la cajita que
   envolvía al «Hoy» oscuro) y `.appt-todaybtn` (ese botón). Las FLECHAS se
   quedan — ahora viven integradas en la fila de períodos — así que
   `.appt-navbtn` sigue viva y con sus 44 px. Medido con grep antes de borrar:
   las otras dos no aparecían en ningún markup, solo en este archivo. */
.appt-navbtn {
    min-height: 44px; min-width: 44px; border-radius: 8px; font-size: 1rem;
    border: 1px solid var(--bs-border-color, #E2E8F0);
    background: var(--surface-color, #fff); color: var(--bs-body-color, #14224E);
    display: inline-flex; align-items: center; justify-content: center; line-height: 1;
}
.appt-navbtn:hover {
    background: var(--primary-color, #14224E); color: #fff;
    border-color: var(--primary-color, #14224E);
}

/* Cada control y SU ⓘ viajan pegados: con `flex-wrap` en el contenedor padre,
   un ⓘ suelto acabaría en la línea de abajo, huérfano del botón que explica. */
.appt-shortcut, .appt-field, .appt-chipwrap {
    display: inline-flex; align-items: center; gap: .25rem;
}
.appt-shortcut > .btn { min-height: 38px; }

/* 📏 Aquí los controles suben a 44 px: es donde el dedo compite con 8 chips y
   4 atajos.
   🔴 ÍTEM 951 — ANTES ESTO COLGABA DE `max-width: 575.98px`, Y ERA UN FALLO
   MEDIDO: el MISMO teléfono, ACOSTADO, mide 844 de ancho y se caía del lado
   "escritorio" ⇒ perdía los 44 justo cuando el dedo sigue siendo el mismo dedo.
   Medido a 844×390: los tres controles bajaban a 38 · 38 · 28,5 px.
   ✅ La compuerta correcta es el PUNTERO: un `pointer: coarse` no cambia al
   girar el teléfono, y además alcanza la tablet del mostrador y el kiosco, que
   se tocan con dedo y por ancho quedaban fuera. (Es la misma corrección que el
   bloque de la ⓘ; las dos son R-238.) */
@media (pointer: coarse) {
    .appt-shortcut > .btn,
    .appt-field > .form-control,
    .appt-field > .form-select { min-height: 44px; }
    /* 📏 Medido con Playwright a 390 px: los 8 contadores de estado salían de
       30 px de alto. Se suben SOLO aquí (`.appt-chipwrap > .st-chip`) y no en
       `.st-chip` a secas: esa clase la comparte el tracker y este lote no toca
       pantallas que no le tocan. */
    .appt-chipwrap > .st-chip { min-height: 44px; }
}

/* ⚠️ `.appt-winbar` SE QUEDA COLGADA DEL ANCHO, y no es un olvido: vivía dentro
   del bloque de arriba, pero es TIPOGRAFÍA, no blanco táctil. Mudarla a
   `pointer: coarse` con el resto le habría achicado la letra a la tablet del
   mostrador —pantalla ancha, con dedo— sin que nadie lo pidiera, y sin fallar.
   El dedo decide los tamaños de TOQUE; el ancho sigue decidiendo el TEXTO. */
@media (max-width: 575.98px) {
    .appt-winbar { font-size: .8rem; }
}

/* ── ÍTEM 228 (mockup Kahlo): decisión de reembolso — las DOS caras EN VIVO ──
   "Reembolsas al cliente $X" (verde, dinero que sale) · "El negocio retiene $Y"
   (neutro). Ningún número de dinero sin decir DE QUÉ es (regla fija). */
.rf-faces { display: flex; gap: .5rem; margin-top: .6rem; }
.rf-face {
    flex: 1; border: 1.5px solid; border-radius: 11px;
    padding: .5rem .65rem; display: flex; flex-direction: column; gap: .1rem;
}
.rf-face-l { font-size: .72rem; font-weight: 700; display: flex; align-items: center; gap: .3rem; }
.rf-face-v { font-size: 1.25rem; font-weight: 800; line-height: 1.1; }
.rf-face-give { background: rgba(22, 163, 74, .12); border-color: #16A34A; }
.rf-face-give .rf-face-l, .rf-face-give .rf-face-v { color: #15803D; }
.rf-face-keep { background: var(--bs-tertiary-bg, #F1F5F9); border-color: var(--bs-border-color, #CBD5E1); }
.rf-face-keep .rf-face-l { color: var(--bs-secondary-color, #64748B); }
.rf-face-keep .rf-face-v { color: var(--bs-secondary-color, #475569); }
.rf-quick { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .55rem; }

/* ══════════════════════════════════════════════════════════════════════════
   R-384 · ÍTEM 800 — LA TECLA DE LA CASA MIDE 88 px (el dueño 2026-08-19).
   Verbatim: "88 porfavor... para cegatos jajajaja... y dedos grandes... los
   gringos le dicen fatfinguer." Se descartó el 76 del POS.
   POR QUÉ NO ES COSMÉTICO: (1) la tableta del mostrador SE VOLTEA hacia el
   cliente, y un cliente que no ve bien de cerca es cliente igual; (2) el
   mínimo táctil publicado es 44 pt (Apple) / 48 dp (Google) — 88x64 les pasa
   por encima a los dos con margen; (3) paridad R-379: un solo tamaño de tecla
   en toda la casa.
   👉 SI UNA PANTALLA NO AGUANTA 88 NO SE ENCOGE LA TECLA: se rediseña la
   pantalla (ver .ci-scroll / .ci-actions aquí abajo, que es justo lo que se
   hizo para el móvil acostado) o se trae al dueño.
   MEDIDO: pad completo 3x88 + 2x10 = 284 px de ancho y 4x64 + 3x10 = 286 px
   de alto — cabe con holgura en 390 px de ancho (sobran 106).
   📌 `.kiosk-pad` / `.kiosk-key` (línea ~1959) tienen HOY los mismos valores y
   se quedan donde están A PROPÓSITO: el kiosco NO se refactoriza antes de
   septiembre (decisión #4 del ítem 800, "no arriesguemos el kiosko"). Cuando
   se mude, esas dos reglas se borran y el kiosco usa estas. */
/* TECLA: 88 -> 81 (el dueño 2026-08-20, item 851). En el celular las teclas de 88
   se salian del margen de la tarjeta: el teclado medía ~284px contra ~244px de
   contenido disponible. El dueño decidio achicar la TECLA y NO tocar el relleno
   de la tarjeta — eso deroga la parte de R-384 que decia "no se encoge la tecla".
   ⚠️ PENDIENTE DE VER EN EL TELEFONO: a 81 con 10px de gap el teclado mide ~263px,
   que segun la medicion del screenshot TODAVIA se sale de los 244. Si al verlo
   sigue rebasando, el numero que entra sin tocar la tarjeta es 78 (gap 5) o 76
   (gap 8). Medir el ANCHO DEL CONTENEDOR, no el del viewport — ese fue el error
   del arnes original. */
/* ITEM 851 (el dueño 2026-08-20): SE QUITA EL CENTRADO. La tecla NO se toca.
   El defecto que el dueño cazo: en pantalla ancha el teclado quedaba CENTRADO
   mientras la etiqueta, el campo del telefono y los dos botones iban a la
   IZQUIERDA. Lupe lo midio: 323px de sangria a 1280. En el celular no se nota
   (el teclado casi llena el ancho); en la laptop del mostrador salta a la vista,
   y esa es la superficie real de In and Out.
   `start` en vez de `center` = el teclado se alinea con el resto del bloque.
   OJO: `.kiosk-pad` (~1959) conserva el centrado A PROPOSITO — el kiosco es
   pantalla completa y ahi centrar SI es lo correcto. No unificarlas. */
/* ══ 🩹 EL TECLADO SE MONTABA SOBRE EL BORDE DERECHO (el dueño 2026-08-23, con
   captura, celular vertical, POS paso 1 "¿A quién atendemos?") ═══════════════
   Verbatim: *"hay que mover los botones un poco a la izquierda para que no queden
   encima de la línea visible que está a la derecha."*

   🩸 LA CAUSA, EN UNA LÍNEA: `repeat(3, 81px)` son **263 px FIJOS** (3×81 + 2×10).
   Una rejilla de columnas FIJAS **no se encoge** — cuando la caja baja de 263 px,
   se DERRAMA a la derecha, justo donde está dibujado el borde de la tarjeta. Es
   exactamente el "PENDIENTE DE VER EN EL TELÉFONO" que quedó escrito aquí arriba
   en el ítem 851: *"a 81 con 10px de gap el teclado mide ~263px, que… TODAVÍA se
   sale de los 244."* El dueño ya lo vio en el teléfono. Sale.

   🔴 LA ASIMETRÍA QUE ÉL CAZÓ TIENE NOMBRE: en esta misma pantalla conviven dos
   reglas. Los botones de acción SÍ obedecen la caja (`.ci-actions > .btn
   {width:100%}` bajo 576px, aquí abajo); el teclado iba por libre. Por eso el
   botón rojo se ve encajado y el teclado no.

   ✅ EL ARREGLO: columnas FLEXIBLES con techo. `minmax(0, 1fr)` + `max-width:263px`
   deja la tecla en sus **81 px exactos mientras haya sitio** (tableta y laptop del
   mostrador — la superficie real de esta pantalla) y **solo encoge cuando la
   alternativa es salirse del recuadro**. La rejilla ya no puede desbordar: su
   ancho máximo ES el ancho del contenido.

   ⚖️ R-384 — POR QUÉ ESTO NO LA VIOLA, y está medido, no razonado:
   · La parte de R-384 que decía "no se encoge la tecla" **ya fue derogada por
     el dueño para `.bsp-pad`** en el ítem 851 (ver la nota de arriba: *"el dueño
     decidió achicar la TECLA y NO tocar el relleno de la tarjeta"*) — 88 → 81.
   · Esa misma nota **nombra el piso aceptable**: *"el número que entra sin tocar
     la tarjeta es 78 (gap 5) o **76 (gap 8)**."* Con el gap a 8 en celular, un
     contenedor de 244 px da (244−16)/3 = **76 px CLAVADOS** — el número que el dueño
     ya aprobó, alcanzado solo, sin cablearlo.
   · Y derramarse tampoco respetaba R-384: **la parte de la tecla que cae fuera del
     recuadro no es un blanco táctil más pequeño, es un blanco táctil que no está.**
   ⛔ `.kiosk-pad` (~1959) NO SE TOCA: tablet montada a 1024x600 fija, jamás
   desborda, y R-384 la excluye con todas sus letras (*"el Kiosco está funcionando
   perfectamente, no cambiemos el layout de eso please!"*). No se unifican. */
.bsp-pad { display: grid; grid-template-columns: repeat(3, minmax(0, 81px)); gap: 10px; justify-content: start; max-width: 263px; }
@media (max-width: 575.98px) {
    /* Celular: SOLO baja el gap de 10 a 8. Las columnas ya son elásticas con techo
       de 81, así que en un contenedor de 244 px la tecla aterriza en (244−16)/3 =
       **76 px CLAVADOS** — el número que el ítem 851 dejó aprobado por escrito.
       El techo de 81 sigue mandando en cuanto haya sitio: esto no achica nada que
       hoy quepa, solo reparte los 2 px del gap donde no cabía. */
    .bsp-pad { gap: 8px; }
}
.bsp-key { height: 60px; font-size: 1.3rem; font-weight: 600; touch-action: manipulation; }
/* ÍTEM 237e heredado: las teclas CON PALABRA ("Borrar") no se parten en 2 líneas */
.bsp-key-txt { font-size: .9rem; white-space: nowrap; padding-left: 2px; padding-right: 2px; }

/* ── ÍTEM 800 · EL CONTROL DE IDENTIFICAR CLIENTE, EN HORIZONTAL ────────────
   MEDIDO en el navegador: con la tecla de 88 el bloque mide 460 px de alto y
   la pantalla acostada (844x390) solo tiene 390 ⇒ los botones de acción se
   iban debajo del borde. La salida NO es achicar la tecla (R-384): es ANCLAR
   la fila de acciones, patrón que la casa ya tenía en el kiosco
   (.kiosk-new-scroll, ítem 313e / LV-09) — solo el CONTENIDO desplaza y la
   botonera queda fija al fondo del bloque, siempre a la vista. */
.ci-scroll { min-height: 0; }
.ci-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
@media (max-height: 520px) {
    .ci-scroll { max-height: 44vh; overflow-y: auto; }
    .ci-actions {
        position: sticky; bottom: 0; z-index: 4;
        background: var(--surface-color, #fff);
        padding-top: .4rem; padding-bottom: .1rem;
    }
}
/* En 390 vertical los dos botones se APILAN con el primario arriba: en línea
   quedarían de 168 px y "Abrir ficha nueva con este número" no cabe. */
@media (max-width: 575.98px) {
    .ci-actions > .btn { width: 100%; }
}

/* ÍTEM 86a: keypad numérico del POS estilo Kiosk —
   touch-friendly para tablet/celular; algo más compacto que el del kiosco */
.pos-pad { display: grid; grid-template-columns: repeat(3, 76px); gap: 8px; }
/* ÍTEM 237e: los textos de tecla ("Borrar") NO se parten
   en 2 líneas — nowrap + versión reducida para teclas con palabra */
.pos-key { height: 56px; font-size: 1.25rem; font-weight: 600; white-space: nowrap; padding-left: 2px; padding-right: 2px; }
.pos-key-txt { font-size: .9rem; }

/* ── BATCH J (ítem 131a): fila Save/Cancel PEGAJOSA en los forms largos ──────
   EmployeeEdit / StoreEdit / CompanyEdit / CustomerEdit / Ads: la fila de
   botones (con su copia del StatusAlert del ítem 124) queda fija al FONDO del
   viewport mientras se scrollea el form — position:sticky (no fixed): al
   llegar al final vuelve a su lugar natural y NUNCA tapa el cierre del form.
   Fondo = superficie del TEMA (todas las 8 hojas definen --surface-color) y
   z-index para que el contenido no se transparente al pasar por debajo. */
.bsp-sticky-actions {
    position: sticky;
    bottom: 0;
    z-index: 5;
    background: var(--surface-color, #fff);
    padding: .6rem 0 .75rem;
    margin-top: .75rem;
    border-top: 1px solid var(--border-color, #DEE2E6);
    /* que el fondo cubra de borde a borde del card-body (sangra el padding) */
    box-shadow: 0 -6px 8px -8px rgba(0, 0, 0, .25);
}
/* el alert dual del ítem 124 vive DENTRO de la barra pegajosa: sin margen
   extra abajo para que alert + botones queden compactos */
.bsp-sticky-actions .alert { margin-bottom: .5rem; }

/* ── ÍTEM 189 v1: FONDO personalizado de la página del barbero (/b/{slug}) ──
   Capas (de atrás hacia delante): FOTO (fallback, object-fit cover) → IFRAME
   de video (embed construido server-side por BackgroundMedia — si no carga,
   se transparenta y queda la foto) → OVERLAY oscuro para legibilidad (mismo
   tratamiento del hero del Home). Todo fixed detrás del contenido
   (z-index:-1) y sin eventos (decoración pura).
   TEMAS OSCUROS: los 5 dark (rose/onyx/jade/hc-dark/deep) definen
   --public-bg-media:none y APAGAN el bloque completo — réplica del patrón
   --auth-bg-image:none; los temas claros no definen la variable y el
   fallback la deja en block. */
.barber-bg {
    position: fixed;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
    display: var(--public-bg-media, block);
}
.barber-bg-photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
/* El iframe 16:9 se sobredimensiona para CUBRIR el viewport en cualquier
   proporción (patrón cover para video embebido): al menos 100vw de ancho y
   100vh de alto, manteniendo 16:9 (177.78vh / 56.25vw), centrado. */
.barber-bg-frame {
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100vw, 177.78vh);
    height: max(100vh, 56.25vw);
    transform: translate(-50%, -50%);
    border: 0;
    pointer-events: none;
}
.barber-bg-overlay {
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom, rgba(20, 34, 78, 0.35) 0%, rgba(20, 34, 78, 0.60) 100%);
}

/* El BODY pinta el color+gradiente del tema y tapaba la capa z-index:-1 —
   con fondo de barbero presente se destapa. En temas OSCUROS .barber-bg está
   apagado por --public-bg-media y el body conserva su fondo vía
   --public-bg-body (definido solo en las 5 hojas dark = var(--background-color)).
   Sin soporte de :has() el fondo queda tapado — degradación elegante. */
body:has(.barber-bg) {
    background-color: var(--public-bg-body, transparent);
    background-image: none;
}

/* ── 🎭 BATCH 195: IMPERSONACIÓN DE BARBEROS ──────────────────────────────
   Banner permanente e INCONFUNDIBLE (rayas de precaución, sticky, siempre
   arriba de todo) mientras el SuperAdmin actúa como un barbero — regla fija: que jamás opere creyendo ser él mismo. */
.bimp-banner {
    position: sticky;
    /* 🔴 ÍTEM 968 (Vera lo midió, Kahlo lo diseñó, el dueño dio luz verde 2026-08-31):
       ANTES decía `top: 0` — el MISMO `top` que `.app-header`. Al hacer scroll los
       dos se pegaban al mismo sitio, el encabezado (opaco) quedaba encima y tapaba
       **21.840 px²** de banda a 390×844: el texto «🎭 Actuando como {barbero}» al
       **100%** y el botón «Volver a mi cuenta» al **38,7%** — y esos 12 px el toque
       se lo llevaba el ENCABEZADO. Un indicador que se tapa DEJA DE INDICAR, y
       debajo se cobra y se cancela de verdad (R-261: «que JAMÁS opere creyendo ser
       él mismo»).
       ⛔ NO SE ARREGLA BAJANDO EL Z-INDEX, y está MEDIDO en los dos sentidos: con el
       encabezado en 1050 la intersección **sigue siendo 21.840** porque el encabezado
       es OPACO — el z-index solo decide quién recibe el TOQUE, nunca quién se VE — y
       encima reintroduce el bug de la campanita que el comentario de arriba
       (`MainLayout.razor.css`) documenta. La raíz nunca fue el apilado: era que
       NADIE le decía a la banda dónde pegarse. */
    top: var(--bsp-header-h);
    z-index: 1050; /* por encima del header/sidebar/toasts */
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: .75rem;
    padding: .5rem .75rem;
    background: repeating-linear-gradient(45deg, #b45309, #b45309 16px, #92400e 16px, #92400e 32px);
    color: #fff;
    font-weight: 600;
    text-shadow: 0 1px 2px rgba(0, 0, 0, .45);
    box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
}

.bimp-banner-btn {
    font-weight: 600;
    white-space: nowrap;
}

/* Panel del selector de barberos en el NavMenu (lista agrupada + búsqueda) */
.bimp-picker {
    background: rgba(255, 255, 255, .08);
    border-radius: .375rem;
    padding: .4rem;
}

.bimp-list {
    max-height: 260px;
    overflow-y: auto;
}

.bimp-group {
    padding: .25rem .25rem .1rem;
    opacity: .85;
}

.bimp-item {
    color: inherit;
    border: 0;
    padding: .2rem .5rem;
}

.bimp-item:hover {
    background: rgba(255, 255, 255, .18);
    color: inherit;
}

/* ═══════════════════════════════════════════════════════════════════════
   REDISEÑO DE LA AGENDA (Tito 2026-07-19, ítems 215-224, mockup de Kahlo)
   Glass tokens (223), header sticky (218), celda 2-líneas + iconos (215),
   scroll horizontal + auto-ancho + gutter sticky (216), calendario de
   capacidad dropdown/bottom-sheet con #+% (221), modal de la cita (224).
   ═══════════════════════════════════════════════════════════════════════ */

/* ── ÍTEM 223: GLASS tokenizado. color-mix sobre la superficie del tema →
   se adapta solo a los 8 temas (claro y oscuro). Fallback sólido primero
   (si color-mix o backdrop-filter no están, queda el fondo sólido). ── */
:root { --bsp-glass-blur: 10px; }
.bsp-glass-strong {
    background: var(--surface-color, #fff);
    background: color-mix(in srgb, var(--surface-color, #fff) 84%, transparent);
    backdrop-filter: blur(var(--bsp-glass-blur)) saturate(1.15);
    -webkit-backdrop-filter: blur(var(--bsp-glass-blur)) saturate(1.15);
}
.bsp-glass-subtle {
    background: var(--bs-body-bg, #f7f5f0);
    background: color-mix(in srgb, var(--bs-body-bg, #f7f5f0) 74%, transparent);
    backdrop-filter: blur(calc(var(--bsp-glass-blur) * .7)) saturate(1.1);
    -webkit-backdrop-filter: blur(calc(var(--bsp-glass-blur) * .7)) saturate(1.1);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .bsp-glass-strong, .bsp-glass-subtle { background: var(--surface-color, #fff); }
}
/* PERFORMANCE KIOSCO (RPi4): backdrop-filter:blur es caro en GPU → sin blur */
html[data-kiosk="1"] .bsp-glass-strong,
html[data-kiosk="1"] .bsp-glass-subtle { backdrop-filter: none; -webkit-backdrop-filter: none; }

/* ── ÍTEM 218: HEADER STICKY arriba, compacto y táctil ── */
.sch-toolbar {
    display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
    padding: .45rem .7rem; position: sticky; top: 0; z-index: 20;
    border-bottom: 1px solid var(--bs-border-color, #E2E8F0);
    box-shadow: 0 2px 8px -4px rgba(11, 18, 32, .2);
    border-top-left-radius: var(--bs-card-inner-border-radius, .5rem);
    border-top-right-radius: var(--bs-card-inner-border-radius, .5rem);
}
.sch-head-ico { width: 30px; height: 30px; border-radius: 8px; object-fit: cover; flex: none; }
.sch-head-title { font-size: 1rem; }
.sch-head-spacer { flex: 1 1 auto; }
.sch-datenav { display: flex; align-items: center; gap: .28rem; }
.sch-navbtn, .sch-todaybtn, .sch-datepick, .sch-capbtn {
    height: 34px; border-radius: 8px; border: 1px solid var(--bs-border-color, #E2E8F0);
    background: var(--surface-color, #fff); color: var(--bs-body-color, #14224E);
    display: inline-flex; align-items: center; justify-content: center; line-height: 1;
}
.sch-navbtn { min-width: 34px; font-size: .9rem; }
.sch-navbtn:hover { background: var(--primary-color, #14224E); color: #fff; border-color: var(--primary-color, #14224E); }
.sch-todaybtn { padding: 0 .7rem; font-size: .8rem; font-weight: 600;
    background: var(--primary-color, #14224E); color: #fff; border-color: var(--primary-color, #14224E); }
.sch-datepick { width: auto; max-width: 148px; padding: 0 .5rem; font-size: .8rem; font-weight: 600; }
/* ÍTEM 910: `.sch-storepick` (max-width:175px; height:34px) BORRADA — su único
   dueño era el <select> de tienda de la Agenda, que pasó a `StoreSelect`. Se
   quita en el mismo lote que la causa: una regla huérfana en un app.css de
   4.000 líneas es la que mañana hace perder media hora buscando por qué "no
   aplica el ancho". El tope de ancho ahora lo pone el parámetro `MaxWidth`. */
.sch-capbtn { gap: .35rem; padding: 0 .7rem; font-size: .8rem; font-weight: 600; }
.sch-capbtn.on, .sch-capbtn:hover { border-color: var(--accent-color, #C8102E); color: var(--accent-color, #C8102E); }
/* selector Día | Semana | Mes */
.sch-viewseg { display: inline-flex; background: var(--bs-body-bg, #EEF1F7);
    border: 1px solid var(--bs-border-color, #E2E8F0); border-radius: 9px; padding: 2px; gap: 2px; }
.sch-seg { height: 30px; padding: 0 .7rem; border: 0; background: transparent;
    color: var(--bs-secondary-color, #64748B); font-size: .78rem; font-weight: 600; border-radius: 7px; }
.sch-seg.on { background: var(--accent-color, #C8102E); color: #fff; }

/* ── Semana/Mes = placeholder (scope aparte) ── */
.sch-soon { padding: 2.4rem 1.2rem; text-align: center; max-width: 560px; margin: 1rem auto; }
.sch-soon-badge { display: inline-block; background: var(--bs-body-bg, #EEF1F7);
    color: var(--bs-secondary-color, #64748B); font-weight: 700; font-size: .74rem;
    letter-spacing: .06em; text-transform: uppercase; padding: .25rem .8rem;
    border-radius: 999px; margin-bottom: .7rem; }

/* ── ÍTEM 216: agenda scroll horizontal + gutter STICKY + columnas AUTO-ANCHO ── */
.sch-agenda-wrap { overflow: auto; position: relative; max-height: calc(100vh - 250px);
    border: 1px solid var(--bs-border-color, #E2E8F0); border-radius: 10px; }
.sch-agenda { width: max-content; min-width: 100%; }
.sch-time-gutter { position: sticky; left: 0; z-index: 4; background: var(--surface-color, #fff);
    box-shadow: 2px 0 6px rgba(11, 18, 32, .06); }
.sch-agenda-head { z-index: 5; }
.sch-agenda-head .sch-time-gutter { z-index: 6; background: var(--sidebar-bg, #14224E); }
.sch-barber-col { flex: 0 0 auto; min-width: 0; max-width: none; }

/* ── ÍTEM 349 (Tito 2026-07-26) — AQUÍ VIVÍA EL CASO ESPECIAL DEL ÍTEM 285 ──
   Era: `.sch-time-gutter .sch-gutter-label:first-child { top:2px !important;
   transform:none !important; }`. Resolvía que la 1ra hora quedara tapada por el
   header sticky, pero al sacarla de su centrado la bajaba ~8-10px y la dejaba
   PEGADA a la siguiente — que es justo lo que se vio: "8am y 8:15am se ven
   bien apretados".
   SUSTITUIDO por un respiro superior en el CUERPO de la agenda
   (`GutterPadTopPx`, Schedule.razor): ahora cabe centrada igual que todas y no
   hay nada que tapar. El relleno va en el padre flex del gutter Y de las
   columnas, así que etiquetas, líneas y citas se mueven JUNTAS y siguen
   cuadrando. Regla borrada a propósito: el arreglo es no tener excepción. */

/* ── ÍTEM 215: celda = nombre arriba + apellido(s) 2da línea + iconos ── */
.sch-appt-block { cursor: pointer; }
.sch-appt-row { align-items: flex-start; gap: 4px; }
.sch-appt-name { display: flex; flex-direction: column; line-height: 1.1; overflow: hidden;
    flex: 1 1 auto; min-width: 0; white-space: normal; }
.sch-appt-nf { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: .76rem; }
.sch-appt-nl { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    font-size: .7rem; opacity: .68; }
/* iconitos de celda ✂️/✅/👁️ — táctiles, meaningful, sin depender de hover */
.sch-cell-ic { width: 26px; height: 26px; border-radius: 7px; flex: none; padding: 0;
    border: 1px solid var(--bs-border-color, #E2E8F0); background: var(--surface-color, #fff);
    color: var(--bs-body-color, #14224E); display: inline-flex; align-items: center; justify-content: center; }
.sch-cell-ic svg { width: 14px; height: 14px; }
.sch-cell-ic-serve { color: #16A34A; border-color: rgba(22, 163, 74, .45); }
.sch-cell-ic-serve:hover { background: #16A34A; color: #fff; }
.sch-cell-ic-finish { color: #fff; background: #16A34A; border-color: #16A34A; }
.sch-cell-ic-eye { color: var(--primary-color, #14224E); }
.sch-cell-ic-eye:hover { background: var(--primary-color, #14224E); color: #fff; border-color: var(--primary-color, #14224E); }
/* FIX 1 (Vera QA 2026-07-19): en TEMA OSCURO el ojo 👁️ usaba --primary-color
   (navy) sobre superficie navy → contraste ~1:1 (casi invisible). En los 5
   temas oscuros (onyx/deep/jade/rose/hc-dark) usa el color de texto del body
   para que se vea. Mismo mecanismo de clase-ancestro en .page que ya usa
   .sidebar.theme-hc-dark. Solo el ojo — ✂️/✅/⚠️ ya contrastan bien. */
.page.theme-onyx  .sch-cell-ic-eye,
.page.theme-deep  .sch-cell-ic-eye,
.page.theme-jade  .sch-cell-ic-eye,
.page.theme-rose  .sch-cell-ic-eye,
.page.theme-hc-dark .sch-cell-ic-eye { color: var(--bs-body-color, #E2E8F0); }
/* ÍTEM 219: indicador de nota = ⚠️ AMARILLO (señala; SIN texto) */
.sch-note-warn { flex: none; display: inline-flex; align-items: center; line-height: 0; }
.sch-note-warn svg { width: 16px; height: 16px; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .18)); }
/* FIX 2 (Vera QA 2026-07-19): en cita de 15 min el bloque mide ~22px; la 2da
   línea (apellidos, .7rem) y los iconos (26px) NO caben → el apellido se veía
   como una tira de ~7px y el ícono recortado. En bloque compacto (duración =
   mínimo de 15 min, altura < 24px) se OCULTA el apellido (el nombre de pila
   queda legible; el modal trae el nombre completo) y los iconos se fijan a un
   tamaño tappable mínimo (22px, no ~4px), fila centrada y padding reducido para
   que quepan enteros. La clase la aplica Schedule.razor según la duración. */
.sch-appt-block--compact { padding-top: 1px; padding-bottom: 1px; }
.sch-appt-block--compact .sch-appt-row { align-items: center; }
.sch-appt-block--compact .sch-appt-nl { display: none; }
.sch-appt-block--compact .sch-cell-ic { width: 22px; height: 22px; border-radius: 6px; }
.sch-appt-block--compact .sch-cell-ic svg { width: 13px; height: 13px; }

/* ── ÍTEM 221: # de clientes en el calendario de capacidad ── */
.sch-cap-cnt { font-size: .9rem; font-weight: 700; line-height: 1; align-self: center; }
.sch-cap-cnt small { font-size: .5rem; font-weight: 600; opacity: .75; margin-left: 1px; }
.sch-cap-cell { justify-content: flex-start; gap: 1px; }
.sch-cap-cell .sch-cap-daynum { position: absolute; top: 2px; left: 4px; font-size: .56rem; opacity: .6; font-weight: 600; }
.sch-cap-cell .sch-cap-pct { align-self: center; font-size: .58rem; opacity: .85; }

/* ── ÍTEM 221: dropdown (desktop) ↔ bottom-sheet (móvil) ── */
.sch-cap-back { position: fixed; inset: 0; z-index: 1040; background: transparent; }
.sch-cap-panel { position: absolute; top: 4px; right: 8px; z-index: 1050; width: 330px; max-width: calc(100vw - 24px);
    border: 1px solid rgba(255, 255, 255, .5); border-radius: 12px; box-shadow: 0 18px 50px rgba(11, 18, 32, .34); padding: 11px; }
.sch-cap-grab { display: none; }
.sch-cap-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.sch-cap-title { font-weight: 600; flex: 1; text-transform: capitalize; }
.sch-cap-nav { display: flex; gap: 5px; align-items: center; }
.sch-cap-nav button { width: 28px; height: 28px; border-radius: 7px; border: 1px solid var(--bs-border-color, #E2E8F0);
    background: var(--bs-body-bg, #EEF1F7); color: var(--bs-body-color, #14224E); display: inline-flex; align-items: center; justify-content: center; }
.sch-cap-nav .sch-cap-todaybtn { width: auto; padding: 0 .6rem; font-size: .72rem; font-weight: 600;
    border-color: var(--primary-color, #14224E); background: var(--primary-color, #14224E); color: #fff; }
/* ÍTEM 367-bis: esta X era CUADRADA y con colores de tema — quedó fuera del
   barrido del 367 porque no usa .btn-close. Ahora toma el ADN del bloque
   compartido (ver .btn-close/.sch-m-close al final del archivo). Aquí solo
   quedan el tamaño y la fuente propios. */
.sch-cap-x { width: 28px; height: 28px; font-size: 1rem; line-height: 1; }
.sch-cap-dowrow { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; margin-bottom: 3px; }
.sch-cap-legend { display: flex; gap: .7rem; flex-wrap: wrap; font-size: .64rem;
    color: var(--bs-secondary-color, #64748B); margin-top: 8px; align-items: center; }
.sch-cap-legend span { display: inline-flex; align-items: center; gap: .25rem; }

@media (max-width: 640px) {
    /* móvil: MISMA experiencia como bottom-sheet táctil (≥44px) */
    .sch-cap-back { background: rgba(11, 18, 32, .5); z-index: 1050; }
    .sch-cap-panel { position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: 100%; max-width: 100%;
        border-radius: 20px 20px 0 0; padding: 8px 14px 18px; max-height: 86vh; overflow: auto; }
    .sch-cap-grab { display: block; width: 44px; height: 5px; border-radius: 3px;
        background: var(--bs-border-color, #E2E8F0); margin: 4px auto 10px; }
    .sch-cap-nav button, .sch-cap-x { width: 44px; height: 44px; font-size: 1.1rem; }
    .sch-cap-nav .sch-cap-todaybtn { height: 44px; padding: 0 .9rem; font-size: .84rem; }
    .sch-cap-cell { min-height: 52px; }
    .sch-cap-cnt { font-size: 1.1rem; }
}

/* ── ÍTEM 224: MODAL DE LA CITA ── */
.sch-modal-back { background: rgba(11, 18, 32, .55); }
.sch-modal-dialog { max-width: 420px; }
.sch-modal { border-radius: 16px; border: 1px solid rgba(255, 255, 255, .4);
    box-shadow: 0 18px 50px rgba(11, 18, 32, .34); overflow: hidden; }
.sch-m-top { position: relative; padding: 14px 16px 12px; border-bottom: 1px solid var(--bs-border-color, #E2E8F0); }
/* ÍTEM 367-bis: LA X CUADRADA — el botón de cerrar del modal de
   la cita no usa .btn-close (es un <button> propio con &times;) y quedó fuera
   del círculo del 367. Se une al bloque compartido (final del archivo); aquí
   solo quedan posición y tamaño propios. */
/* ÍTEM 367-ter: la POSICIÓN (absolute/top/right) se declara UNA sola vez, en el
   bloque compartido del final del archivo — si se repitiera aquí volvería a
   depender del orden del archivo quién gana. Aquí solo tamaño y fuente. */
.sch-m-close { width: 34px; height: 34px; font-size: 1.1rem; line-height: 1; }
.sch-m-eyebrow { font-size: .64rem; text-transform: uppercase; letter-spacing: .16em; color: var(--accent-color, #C8102E); font-weight: 600; }
.sch-m-name { font-size: 1.25rem; font-weight: 600; margin: 2px 0 0; }
.sch-m-sub { font-size: .82rem; color: var(--bs-secondary-color, #64748B); margin-top: 1px; }
.sch-m-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 9px; }
.sch-m-total { display: flex; align-items: baseline; gap: 5px; }
.sch-m-total b { color: var(--gold-color, #B45309); font-weight: 700; font-size: 1.4rem; line-height: 1; }
.sch-m-total span { font-size: .6rem; text-transform: uppercase; letter-spacing: .1em; color: var(--bs-secondary-color); font-weight: 600; }
.sch-state-tag { display: inline-flex; align-items: center; gap: 5px; border-radius: 999px;
    padding: 2px 9px; font-size: .7rem; font-weight: 600; }
.sch-state-active { background: rgba(22, 163, 74, .18); color: #16A34A; }
.sch-state-booked { background: rgba(37, 99, 235, .14); color: #2563EB; }
.sch-state-done { background: rgba(100, 116, 139, .16); color: #64748B; }
/* REMATE 2026-08-11: estados ATH (AwaitingAthPayment/AwaitingBarberConfirm) —
   badge ámbar "Pendiente de confirmación de pago"; antes caían a "Reservada" */
.sch-state-athwait { background: rgba(245, 158, 11, .18); color: #B45309; }
/* NOTA AL TOPE (219): acento amarillo para que salte a la vista */
.sch-note-card { margin: 11px 16px 2px; border-left: 4px solid #F5B301; border-radius: 8px;
    padding: 8px 10px; font-size: .82rem; background: var(--bs-body-bg, #EEF1F7); }
.sch-note-lbl { display: flex; justify-content: space-between; gap: 8px; align-items: center;
    font-size: .62rem; text-transform: uppercase; letter-spacing: .08em; color: var(--bs-secondary-color); margin-bottom: 3px; }
.sch-note-warnico { width: 14px; height: 14px; vertical-align: -3px; margin-right: 2px; }
.sch-note-lock { color: var(--accent-color, #C8102E); text-transform: none; letter-spacing: 0; }
.sch-note-txt { line-height: 1.35; }
/* zona de pago */
.sch-pay { padding: 10px 16px 4px; }
.sch-pay-lbl { font-size: .66rem; text-transform: uppercase; letter-spacing: .06em;
    color: var(--bs-secondary-color); margin-bottom: 7px; }
.sch-pay-hint { text-transform: none; letter-spacing: 0; }
/* 🐛 FIX QA VERA 237j (2026-07-20): en 375px el grid desbordaba — el min-content
   de los labels nowrap inflaba las columnas (repeat(3,1fr) SIN minmax(0,..) =
   min-width:auto → 645px en un viewport de 375; 6 de 11 métodos inalcanzables).
   RAÍZ arreglada aquí porque estas clases son GLOBALES (modal agenda 224 + POS):
   minmax(0,1fr) + min-width:0 SIEMPRE, y en móvil 2 columnas con labels en wrap. */
.sch-pay-methods { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; }
.sch-pm { min-height: 46px; border-radius: 11px; border: 1px solid var(--bs-border-color, #E2E8F0);
    background: var(--surface-color, #fff); color: var(--bs-body-color, #14224E); display: flex;
    align-items: center; justify-content: center; gap: 6px; font-weight: 600; font-size: .82rem;
    padding: 4px 8px; position: relative; white-space: normal; text-align: center;
    line-height: 1.15; min-width: 0; }
@media (max-width: 480px) {
    .sch-pay-methods { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .sch-pm { font-size: .78rem; padding: 4px 6px; }
    .sch-pm-sub { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* FIX menor QA Vera: Google Pay tenía la clase de Venmo — color alusivo propio */
.sch-pm-gpay { background: #3C4043; border-color: #3C4043; color: #fff; }
.sch-pm:hover { box-shadow: 0 6px 22px rgba(20, 34, 78, .16); transform: translateY(-1px); }
.sch-pm-ic { width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; flex: none; font-weight: 800; }
.sch-pm-ic svg { width: 18px; height: 18px; }
.sch-cardart { width: 26px !important; height: 18px !important; }
.sch-pm-cash { background: #2E7D5B; border-color: #2E7D5B; color: #fff; }
.sch-pm-card { background: #14224E; border-color: #14224E; color: #fff; }
.sch-pm-mobile { background: #4F46E5; border-color: #4F46E5; color: #fff; }
.sch-pm-ath { background: #B0562B; border-color: #B0562B; color: #fff; }
.sch-pm-zelle { background: #7C4DD4; border-color: #7C4DD4; color: #fff; }
.sch-pm-venmo { background: #2478B0; border-color: #2478B0; color: #fff; }
.sch-pm-cashapp { background: #0B8A4B; border-color: #0B8A4B; color: #fff; }
.sch-pm-paypal { background: #24408E; border-color: #24408E; color: #fff; }
.sch-pm-generic { background: var(--surface-color, #fff); color: var(--bs-body-color); border-style: dashed; }
.sch-pm-badge { position: absolute; top: -7px; right: 6px; font-size: .54rem; background: #16A34A;
    color: #fff; border-radius: 999px; padding: 1px 6px; font-weight: 700; }
.sch-pm-star { margin-left: auto; font-size: .58rem; font-weight: 800; background: rgba(255, 255, 255, .28);
    border-radius: 999px; padding: 1px 7px; }
.sch-pm-chev { transition: transform .15s ease; font-size: .8rem; }
.sch-pm-mobile.open .sch-pm-chev { transform: rotate(180deg); }
.sch-pm-sub { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
    padding: 7px; border: 1px dashed var(--bs-border-color, #E2E8F0); border-radius: 11px; background: var(--bs-body-bg, #EEF1F7); }
.sch-pm-sub .sch-pm { min-height: 42px; font-size: .8rem; }
.sch-pm-athfirst { grid-column: 1 / -1; box-shadow: 0 0 0 2px rgba(176, 86, 43, .35); }
.sch-pm-split { grid-column: 1 / -1; background: transparent; border-style: dashed; color: var(--accent-color, #C8102E); }
/* ══ ÍTEMS 332 + 331-A — BLOQUE DE ACCIONES POSITIVAS DEL MODAL DE LA CITA ══
   Se pidió "un boton de Atender a la izquierda de No-show". Se construye
   APARTE y no como tercer hermano de esa fila, porque `.sch-m-actions` contiene
   SOLO acciones destructivas (No-show ámbar + Cancelar rojo): un botón positivo
   pegado a ellas invita al toque equivocado en una pantalla táctil.
   · Una sola columna a ancho completo (en ES "Añádela a tu calendario" no cabe
     a media anchura del modal de 420px sin partirse).
   · Línea inferior = frontera visual con lo destructivo.
   · Verde LLENO #2E7D5B: es el verde que la casa ya usa en `.sch-pm-cash`, y
     emparenta el botón con la TIJERITA verde de la celda (misma acción, mismo
     color). NO se usa `--primary-700` a propósito: ese token es amarillo en
     hc-dark (#CCCC00) y casi idéntico a la superficie en onyx (#161C2A sobre
     #1C2133 = 1.05:1) — sería invisible en 2 de los 8 temas (la "bomba de
     tiempo" que documentó Kahlo). El verde fijo mide 5.02:1 de texto blanco y
     no depende del tema, igual que los botones de método de pago de al lado.
   ⚠️ EL ORDEN IMPORTA: `.sch-mbtn-serve` tiene la MISMA especificidad (0,1,0)
   que la base `.sch-mbtn`, así que las variantes van DESPUÉS de la base o gana
   la base. Aquí solo vive el contenedor; los modificadores están abajo, junto a
   `-warn` y `-danger`. (Se cazó midiendo: con las reglas puestas ANTES, el botón
   se pintaba blanco — el CSS "estaba" y no hacía nada.) */
.sch-m-go { padding: 10px 16px 12px; display: grid; grid-template-columns: 1fr; gap: 9px;
    border-bottom: 1px solid var(--bs-border-color, #E2E8F0); }
/* acciones delicadas */
.sch-m-actions { padding: 10px 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.sch-mbtn { min-height: 46px; border-radius: 12px; border: 1px solid var(--bs-border-color, #E2E8F0);
    background: var(--surface-color, #fff); color: var(--bs-body-color, #14224E); display: flex;
    align-items: center; justify-content: center; gap: 8px; font-size: .9rem; font-weight: 600; padding: 0 10px; }
.sch-mbtn svg { width: 20px; height: 20px; flex: none; }
.sch-mbtn-warn { color: #B45309; border-color: rgba(180, 83, 9, .4); }
.sch-mbtn-danger { color: var(--accent-color, #C8102E); border-color: rgba(200, 16, 46, .4); }
/* ÍTEM 332 — Atender: LLENO en verde (ver el bloque .sch-m-go de arriba). Va
   aquí, DESPUÉS de la base `.sch-mbtn`, porque comparten especificidad. */
.sch-mbtn-serve { background: #2E7D5B; border-color: #2E7D5B; color: #fff; }
.sch-mbtn-serve:hover, .sch-mbtn-serve:active { background: #246147; border-color: #246147; color: #fff; }
/* ÍTEM 350 — Terminar: MISMO verde fijo #2E7D5B que Atender, a propósito.
   (1) Nunca son visibles a la vez (Booked/Confirmed vs InProgress), así que no
   compiten; (2) reusar el color YA MEDIDO evita estrenar un tono sin verificar
   en los 8 temas — el #16A34A de la palomita de la celda da 3.30:1 con blanco
   (vale para un ÍCONO por 1.4.11, pero se queda corto para TEXTO, y este botón
   lleva etiqueta), mientras #2E7D5B da 5.00:1; (3) empareja el par
   ✂️/✅ del modal igual que ya empareja el par de la celda.
   Va DESPUÉS de la base `.sch-mbtn` por la misma razón de especificidad de
   arriba (trampa ④ del estándar de QA). */
.sch-mbtn-finish { background: #2E7D5B; border-color: #2E7D5B; color: #fff; }
.sch-mbtn-finish:hover, .sch-mbtn-finish:active { background: #246147; border-color: #246147; color: #fff; }
/* ÍTEM 331-A — Añadir a mi calendario: neutro a propósito (ni positivo fuerte
   ni destructivo); hereda superficie+tinta del tema, legible en los 8. */
.sch-mbtn-cal { color: var(--bs-body-color); }
/* ÍTEM 386-bis: "💳 Pagar por adelantado" es SECUNDARIA — mismo contorno que
   "Añádela a tu calendario", debajo de ella. El verde lleno se reserva para
   Atender/Terminar (las principales); pintar esta igual las empataría. */
.sch-mbtn-prepay { color: var(--bs-body-color); }
/* detalles plegables */
.sch-det-toggle { width: calc(100% - 32px); margin: 4px 16px 14px; min-height: 42px;
    border: 1px solid var(--bs-border-color, #E2E8F0); background: var(--bs-body-bg, #EEF1F7);
    border-radius: 11px; color: var(--bs-body-color, #14224E); font-weight: 600; font-size: .82rem;
    display: flex; align-items: center; justify-content: center; gap: 6px; }
.sch-det-chev { transition: transform .15s ease; }
.sch-det-toggle.open .sch-det-chev { transform: rotate(180deg); }
/* 👆 R-238 / ítem 951 — 44 px DONDE HAY DEDO, atado al PUNTERO y no al ancho.
   MEDIDO el 2026-08-31 al construir el plegable del historial: `.sch-det-toggle`
   nacía con `min-height: 42px`, DOS por debajo del mínimo escrito ("tap targets
   ≥44×44px", R-238). Va en el selector BASE a propósito: los dos plegables de este
   modal (Detalles e Historial) son el mismo botón, y dejar uno en 42 y otro en 44
   sería exactamente la incoherencia que el patrón compartido viene a evitar.
   El `px` es deliberado (misma excepción que el 951): el dedo no crece con la letra. */
@media (pointer: coarse) { .sch-det-toggle { min-height: 44px; } }
/* Fila "plegable + ⓘ" del HISTORIAL (remate 2026-08-31). El ⓘ es hermano del botón,
   nunca hijo: `HelpHint` es un `<button>`. Los 12 px son los mismos que separan la ⓘ
   del botón de reservar (R-238, ítem 951) — contra un blanco táctil de 44 px es lo
   que los mantiene DISJUNTOS: ningún toque puede caer en los dos. Quien baje ese 12
   los pega. `min-width: 0` en el botón para que el rótulo en español (más largo) no
   infle la fila y saque el ⓘ del diálogo de 420 px. */
.sch-hist-head { display: flex; align-items: center; gap: 12px; }
.sch-det-toggle-sub { width: auto; flex: 1 1 auto; min-width: 0; margin: 0; }
.sch-det-body { padding: 0 16px 14px; display: flex; flex-direction: column; gap: 7px; }
.sch-drow { display: flex; gap: 10px; font-size: .82rem; align-items: baseline; }
.sch-drow-k { color: var(--bs-secondary-color, #64748B); flex: none; min-width: 84px; }
.sch-drow-v { font-weight: 500; }
.sch-svc-list { border: 1px solid var(--bs-border-color, #E2E8F0); border-radius: 10px; overflow: hidden; }
.sch-svc { display: flex; justify-content: space-between; gap: 10px; padding: 5px 10px; font-size: .82rem; border-top: 1px solid var(--bs-border-color, #E2E8F0); }
.sch-svc:first-child { border-top: 0; }
.sch-svc-price { color: var(--gold-color, #B45309); font-weight: 600; flex: none; font-variant-numeric: tabular-nums; }
.sch-svc-total { background: var(--bs-body-bg, #EEF1F7); font-weight: 600; }
/* ÍTEM 1082 · LV-42 (Héctor, 2026-09-03): las sub-líneas de un pago/depósito
   (Orden #, Ref., Recibido, Cambio, chip "Depósito acreditado") sangradas bajo
   su pago y en texto secundario/gris — para que su cifra no compita con la
   columna de importes de arriba (`.sch-svc-price`) ni invite a sumarse con
   ella. Cubre la etiqueta Y el valor: los dos van grises. */
.sch-pay-sub { margin-left: 14px; }
.sch-pay-sub .sch-drow-v { color: var(--bs-secondary-color, #64748B); font-weight: 500; }
/* confirmación */
.sch-confirm { padding: 20px 18px; text-align: center; }
.sch-conf-ico { width: 52px; height: 52px; margin: 2px auto 10px; display: block; }
.sch-conf-warn { color: #B45309; }
.sch-conf-danger { color: var(--accent-color, #C8102E); }
.sch-confirm h4 { font-size: 1.2rem; margin-bottom: 5px; }
.sch-confirm p { font-size: .86rem; color: var(--bs-secondary-color, #64748B); margin-bottom: 14px; }
.sch-confirm-actions { display: flex; gap: 10px; }
.sch-confirm-actions .sch-mbtn { flex: 1; }
@media (max-width: 480px) { .sch-modal-dialog { max-width: 100%; margin: .4rem; } }

/* ══ ÍTEM 682 (el dueño 2026-08-11, probando en STG) — MODAL DE COBRO MÁS ANCHO EN
   LAPTOP/TABLET ══
   Verbatim: "¿hay manera de que esta pantalla sea un poco más ancha cuando
   estamos en laptops o tablets? que quepan todos los botones de 'le dejas
   propina a tu barbero' en una línea."
   El .modal-dialog de Bootstrap topa en 500px desde 576px — la fila de propinas
   (Sin propina · 10% · 15% · 18% · 20% · Otra cantidad, TipSelector) mide ~640px
   con las etiquetas en español y partía en 2 líneas. A ≥768px se sube el tope a
   700px (cabe la fila y aún deja los márgenes de 1.75rem en un tablet de 768).
   MÓVIL <768 NO CAMBIA (sigue el ancho fluido de Bootstrap). La clase vive SOLO
   en el modal del CheckoutPanel — ningún otro modal se toca (mismo criterio de
   clase-marcador que el ancho del 636). Especificidad doblada a propósito para
   ganarle al .modal-dialog { max-width:500px } de Bootstrap sin depender del
   orden de los stylesheets. */
@media (min-width: 768px) { .modal-dialog.chk-modal-dialog { max-width: 700px; } }

/* ═══ ÍTEM 250 — ADMIN SUBSCRIPTION PLANS: tabla COMPACTA sin scroll horizontal
   (verbatim "columnas un poco menos anchas para que quepan
   todas"): fuente 1 punto menor, paddings recortados, inputs angostos y sin
   min-widths gordos — los 5 tiers caben completos en 1280/1366; en móvil el
   scroll queda CONTENIDO en .table-responsive. */
.plans-compact { font-size: .85rem; }
.plans-compact th, .plans-compact td { padding: .22rem .3rem; }
.plans-compact th { font-size: .85rem; white-space: nowrap; }
.plans-feat-col { min-width: 150px; }
/* Pulido de Vera (2026-07-21): anchos en EM, no px — con las fuentes de
   accesibilidad al 130-175% los px fijos RECORTABAN el valor ("199."); en em
   el encasillado crece con la letra y el número entero siempre se ve. */
.plans-price { width: 7.5em; }
.plans-price .input-group-text { padding: .18rem .3rem; font-size: .8rem; }
.plans-price .form-control { padding: .18rem .3rem; font-size: .8rem; }
.plans-limit { width: 5em; padding: .18rem .25rem; font-size: .8rem; }

/* ═══ ÍTEM 263 — CAMPANITA DE NOTIFICACIONES (header) ═══════════════════════
   Bell blanco sobre el header navy; badge ROJO de la casa; panel porcelana
   estilo glass (patrón sch-cap/bsp-glass). Portrait-first: en móvil el panel
   se pega al borde derecho con ancho fluido — jamás scroll-x. */
.notif-wrap { margin-left: auto; position: relative; display: flex; align-items: center; }
.notif-bell-btn {
    position: relative;
    background: transparent; border: 0; padding: .25rem .35rem;
    border-radius: .5rem; cursor: pointer; line-height: 1;
}
.notif-bell-btn:hover { background: rgba(255, 255, 255, .12); }
.notif-bell-icon { width: 28px; height: 28px; display: block; }
.notif-badge {
    position: absolute; top: -2px; right: -4px;
    background: #C8102E; color: #fff;
    font-size: .66rem; font-weight: 700; line-height: 1;
    border-radius: 999px; padding: .18rem .32rem;
    border: 2px solid #fff; min-width: 1.15rem; text-align: center;
}
.notif-back { position: fixed; inset: 0; z-index: 1060; background: transparent; }
.notif-panel {
    position: absolute; top: calc(100% + .5rem); right: 0; z-index: 1061;
    width: min(360px, calc(100vw - 1rem));
    background: #fff; color: #14224E;
    border: 1px solid #E2E8F0; border-radius: .75rem;
    box-shadow: 0 10px 30px rgba(20, 34, 78, .25);
    overflow: hidden;
}
.notif-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: .6rem .8rem; border-bottom: 1px solid #E2E8F0;
    background: #F7F5F0;
}
/* 263-V1 (Vera): color EXPLÍCITO — en hc-dark el btn-link heredaba el amarillo
   #FFFF00 del tema sobre el head porcelana (1.01:1, invisible). El panel es
   porcelana en TODOS los temas (patrón card del tracker) → navy de la casa. */
.notif-markall { font-size: .8rem; text-decoration: none; color: #14224E !important; }
.notif-empty { padding: 1.1rem .9rem; }
.notif-list { max-height: 380px; overflow-y: auto; }
.notif-item {
    display: block; width: 100%; text-align: left;
    background: #fff; border: 0; border-bottom: 1px solid #F1F5F9;
    padding: .55rem .8rem; cursor: pointer; color: inherit;
}
.notif-item:hover { background: #F8FAFC; }
.notif-item.notif-unread { background: #F0F6FF; }
.notif-item.notif-unread:hover { background: #E6F0FD; }
.notif-item-title { display: block; font-weight: 600; font-size: .86rem; color: #14224E; }
.notif-item-body { display: block; font-size: .8rem; color: #475569; margin-top: .1rem; }
.notif-item-when { display: block; font-size: .72rem; color: #94A3B8; margin-top: .15rem; }
.notif-dot {
    display: inline-block; width: .5rem; height: .5rem; border-radius: 50%;
    background: #C8102E; margin-right: .35rem; vertical-align: middle;
}

/* ══ El dueño 2026-08-13 — descartar notificaciones (swipe móvil / ✕ desktop):
   la fila es un flex (contenido + ✕); el descarte anima una salida corta
   hacia la izquierda antes de persistir el soft delete. ══ */
.notif-row {
    display: flex; align-items: stretch;
    border-bottom: 1px solid #F1F5F9;
    transition: transform .18s ease, opacity .18s ease;
    touch-action: pan-y; /* el navegador scrollea vertical; el swipe X es nuestro */
}
.notif-row .notif-item { border-bottom: 0; flex: 1 1 auto; min-width: 0; }
.notif-row.notif-dismissing { transform: translateX(-85%); opacity: 0; }
/* 🟡 BAJA (Lupe 2026-08-13): el ✕ medía 2.2rem ≈ 35px — POR DEBAJO del mínimo
   táctil de 44px. Es el único camino de descarte para quien no hace swipe (y en
   móvil también se toca), así que sube a 44px de ancho Y de alto. El ícono no
   crece: lo que crece es el ÁREA que acepta el dedo. */
.notif-x {
    flex: 0 0 auto; width: 44px; min-width: 44px; min-height: 44px;
    border: 0; background: transparent;
    color: #94A3B8; cursor: pointer; display: flex; align-items: center;
    justify-content: center; font-size: .85rem;
}
.notif-x:hover { color: #C8102E; background: #F8FAFC; }
.notif-undo-bar {
    display: flex; justify-content: space-between; align-items: center;
    gap: .5rem; padding: .4rem .8rem; font-size: .8rem;
    background: #FFF8E1; border-bottom: 1px solid #F1E4B8; color: #475569;
}

/* Cosmético QA (backlog hito 2026-07-21): en móvil (≤576px) el botón flotante
   de accesibilidad tapaba las últimas líneas del footer — aire extra abajo
   para que el texto NUNCA quede debajo del botón (el botón no se mueve:
   regla dura de posiciones estables). */
@media (max-width: 576px) {
    .bsp-footer { padding-bottom: 5.5rem !important; }
}

/* Móvil (≤640px): el hamburger flotante se ancla arriba-derecha SOBRE el
   header — la campanita se corre a su izquierda (barrido anti-overlap del
   propio build: el badge quedaba debajo del hamburger en 390). */
@media (max-width: 640px) {
    .notif-wrap { margin-right: 3.2rem; }
}

/* Móvil: el panel de la campanita se ancla al VIEWPORT (fixed bajo el header),
   no al wrap corrido — a 390 el ancla relativa lo sacaba por la izquierda. */
@media (max-width: 640px) {
    .notif-panel {
        position: fixed;
        top: 3.9rem;
        left: .5rem;
        right: .5rem;
        width: auto;
    }
}

/* Campana bi-bell-fill blanca + SWING animado en hover (mientras llega el
   Lordicon bell.json del próximo lote de íconos) */
.notif-bell-icon { font-size: 1.35rem; color: #fff; display: block; line-height: 1; }
@keyframes notif-swing {
    0% { transform: rotate(0); } 20% { transform: rotate(14deg); }
    45% { transform: rotate(-11deg); } 70% { transform: rotate(7deg); }
    85% { transform: rotate(-4deg); } 100% { transform: rotate(0); }
}
.notif-bell-btn:hover .notif-bell-icon {
    animation: notif-swing .9s ease-in-out;
    transform-origin: top center;
}

/* ══ ÍTEM 318 — MARKETING: COMPARTIR EL LINK/QR DEL BARBERO (Tito 2026-07-25) ══
   PRIMERO EL MÓVIL: todo se dimensionó a 390x844 (el barbero lo usa de pie) y
   el escritorio solo lo centra. Contraste segun el estandar de Kahlo (315):
   el bloque del QR replica la receta .qr-holder (blanco solido + borde navy,
   3.92:1 medido) y NO sigue el tema — un QR sobre fondo oscuro no escanea. */
.mkt-wrap { max-width: 560px; margin: 0 auto; padding: .25rem 0 1rem; }

.mkt-head { display: flex; align-items: center; gap: .75rem; min-width: 0; }
.mkt-photo {
    width: 56px; height: 56px; flex: 0 0 auto; border-radius: 50%;
    object-fit: cover; border: 2px solid var(--primary-900, #14224E);
    background: var(--surface-color, #fff);
}
.mkt-photo-empty {
    display: flex; align-items: center; justify-content: center;
    font-size: 1.5rem; color: var(--bs-body-color);
}
.mkt-headtext { min-width: 0; }
.mkt-name { font-weight: 600; font-size: 1.1rem; line-height: 1.25; overflow-wrap: anywhere; }
.mkt-store { font-size: .85rem; line-height: 1.25; overflow-wrap: anywhere; }

.mkt-lead { margin: .9rem 0 .25rem; font-size: .95rem; line-height: 1.4; }

/* Separador de seccion: "Cara a cara" / "A la distancia" */
.mkt-sect { display: flex; align-items: center; margin: 1.1rem 0 .55rem; }
.mkt-sect::after {
    content: ""; flex: 1 1 auto; height: 1px; margin-left: .6rem;
    background: var(--control-edge, rgba(20,34,78,.35));
}
/* ITEM 966 — AQUI VIVIA `white-space: nowrap`. SE QUITO A PROPOSITO, y no se repone:
   con el nowrap "ENVIA UNA INVITACION BONITA POR CORREO" (38 car. en ES) pedia 325,4 px
   en una seccion de 308 => se salia de la tarjeta y se comia la raya `::after`; a 360 px
   quedaba 6,4 px FUERA del viewport sin scroll con el que alcanzarlo. Medido por Vera:
   5.360 aserciones en 60 configuraciones => SIN el nowrap, 0 desbordes en las 10
   combinaciones (2 idiomas x 5 escalas) a 390 Y a 360. `Cara a cara` no se parte en
   ninguna escala; `¿No lo tienes al lado?` solo a 150%+, donde HOY ya se sale.
   ⛔ Descartadas y medidas: `ellipsis` (esconde 27 px del titulo), bajar `font-size`
   (haria falta -49% al 175%) y `flex-wrap: wrap` (cero efecto). */
.mkt-sect-label {
    font-size: .8rem; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase;
}

/* El QR ES el boton (tocarlo abre la pantalla completa) */
.mkt-qrbtn {
    display: block; width: 100%; padding: .8rem .5rem; cursor: pointer;
    background: #FFFFFF; border: 3px solid #14224E; border-radius: 14px;
}
/* ITEM 969 — el `max-width: 100%` NO es decorativo: es lo unico que ata la imagen a la
   caja de su boton. `min(62vw,230px)` esta en px/vw y NO sigue la escala de accesibilidad,
   pero el `padding` de `.mkt-qrbtn` esta en rem y SI crece => al 175% el contenido
   disponible baja por debajo de 230 y la imagen sobraba 16 px (medido por Vera, EN y ES;
   limpio de 100 a 150%). Misma familia que el 951: una medida en px dentro de un
   contenedor que escala en rem se rompe sola en cuanto alguien sube la letra. */
.mkt-qrbtn img { display: block; width: min(62vw, 230px); max-width: 100%; height: auto; margin: 0 auto; }
.mkt-qrcap {
    display: block; margin-top: .5rem; text-align: center;
    color: #14224E; font-size: .85rem; font-weight: 600;
}

/* Boton principal: alto de DEDO, icono + texto (estandar de la casa) */
.mkt-cta {
    display: flex; align-items: center; justify-content: center; gap: .5rem;
    width: 100%; min-height: 56px; margin-top: .7rem;
    font-size: 1.05rem; font-weight: 600;
}

.mkt-linkbox {
    border: 1px solid var(--control-edge, rgba(20,34,78,.55));
    border-radius: 10px; padding: .55rem .7rem;
    background: var(--surface-color, #fff);
}
.mkt-link-label { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; opacity: .8; }
.mkt-link { display: block; overflow-wrap: anywhere; word-break: break-word; font-size: .95rem; }

/* Confirmacion del copiado: SIEMPRE ocupa su linea (solo cambia la opacidad)
   para que aparecer no empuje los botones de abajo (regla anti-overlap). */
.mkt-flash {
    min-height: 2.4em; margin: .45rem 0 .2rem; font-size: .88rem; font-weight: 600;
    color: var(--bs-success, #198754); opacity: 0; transition: opacity .15s ease;
}
.mkt-flash-on { opacity: 1; }
.mkt-flash-err { color: var(--bs-danger, #DC3545); }

.mkt-actions { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
.mkt-act {
    display: flex; align-items: center; justify-content: center; gap: .4rem;
    min-height: 50px; padding: .4rem .5rem; text-align: center; line-height: 1.15;
}
.mkt-act span { overflow-wrap: anywhere; }
.mkt-act-wide { grid-column: 1 / -1; }
.mkt-hint { margin: .55rem 0 0; font-size: .82rem; opacity: .85; line-height: 1.35; }
.mkt-pick { margin-top: 1.2rem; }

/* Una sola columna en pantallas MUY angostas: en espanol "Enviar por correo"
   no cabe a dos columnas por debajo de ~360px sin apretar el icono. */
@media (max-width: 359.98px) {
    .mkt-actions { grid-template-columns: 1fr; }
}

/* ── CAPA DE PANTALLA COMPLETA (el caso de uso #1) ────────────────────────
   Blanco solido + tinta navy FIJOS a proposito: los escaneres sufren con
   fondo oscuro o con foto detras. z-index por encima de todo (header 1055,
   paneles 1061). */
.mkt-full { display: none; }
.mkt-full.mkt-full-on {
    display: flex; position: fixed; inset: 0; z-index: 2000;
    flex-direction: column; align-items: center; justify-content: space-between;
    padding: .9rem .75rem 1rem; gap: .5rem;
    background: #FFFFFF; color: #14224E; color-scheme: light;
}
.mkt-full-name {
    flex: 0 0 auto; font-size: 1.15rem; font-weight: 700; text-align: center;
    color: #14224E; overflow-wrap: anywhere;
}
.mkt-full-qr { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; width: 100%; min-height: 0; }
.mkt-full-qr img { display: block; width: min(92vw, 100%); max-height: 100%; height: auto; object-fit: contain; }
.mkt-full-foot { flex: 0 0 auto; width: 100%; max-width: 520px; }
.mkt-full-cap { text-align: center; font-weight: 600; color: #14224E; font-size: 1rem; }
.mkt-full-link { text-align: center; color: #14224E; font-size: .9rem; overflow-wrap: anywhere; margin-top: .15rem; }
.mkt-full-close {
    display: flex; align-items: center; justify-content: center; gap: .5rem;
    width: 100%; min-height: 60px; margin-top: .6rem;
    background: #14224E; color: #FFFFFF; border: none; border-radius: 12px;
    font-size: 1.05rem; font-weight: 600;
}
.mkt-full-close:active { background: #0E1837; }

/* ══ ÍTEM 320 (probando en vivo) — LA FILA "label + toggle"
   DEL PASO DE IDENTIFICACIÓN DEL KIOSKO ════════════════════════════════════
   Verbatim: "mover el boton Por email para la derecha para que haya espacio
   entre el boton y el label de Escribe tu numero de telefono. Yo creo que ya
   habia reportado esto." — y sí: el solapamiento se había arreglado, pero
   quedó PEGADO, que era justo lo que pedía evitar.
   CAUSA: la fila usaba `gap-2` (8px) y el chip porcelana `.kiosk-chip-wash`
   pinta un halo de box-shadow de 8px+9px que se sale de su caja de layout →
   del gap de 8px solo sobraba ~-1px VISIBLE (el halo llegaba a tocar el botón).
   ARREGLO: 24px de gap ⇒ 24 − 9 (halo pintado) = 15px VISIBLES entre el borde
   pintado del chip y el botón. Sin wash el halo no existe y quedan los 24px
   completos — más aire, que es lo que se buscaba en los dos casos.
   ⚠️ SE SEPARAN LOS DOS EJES A PROPÓSITO. A 1024×600 (el BAR-01 real) la fila
   NO envuelve ni con 8px ni con 24px — medido: label 293.5/340.9px + botón
   93/98.6px sobre 640px de fila. A 390×844 SÍ envuelve, pero **ya envolvía
   antes de este cambio** (271.3+93+8 = 372.3 > 358 disponibles) — o sea el
   wrap es PREEXISTENTE, no lo trae el gap nuevo. Si el gap fuera de los dos
   ejes, envolver sumaría 16px de alto (64.9→80.9) y eso sí empujaría el
   keypad (el problema del teclado del 313 e-bis). Con `row-gap` fijo en los
   8px de siempre, el caso envuelto queda EXACTO a como estaba hoy. */
.kiosk-idrow { column-gap: 1.5rem; row-gap: .5rem; }

/* ══ ÍTEM 329 (tras publicar) — EL LABEL DE CORREO PEGADO
   AL CAMPO ══════════════════════════════════════════════════════════════════
   Verbatim: "entre el titulo de correo electronico y el textbox no hay
   espacio, podriamos bajar un poquito el textbox?"
   CAUSA (la de siempre, 5ª vez): `.kiosk-chip-wash` pinta anillos de 8px + 9px
   de box-shadow FUERA de su caja de layout — el navegador NO los cuenta para
   el acomodo. La fila usaba `mb-2` = 8px, o sea el halo (9px) se comía el
   margen entero y sobraba ~-1px VISIBLE: el chip llegaba a TOCAR el input.
   ARREGLO: margen inferior REAL de 1.5rem = 24px ⇒ 24 − 9 (halo pintado) =
   15px VISIBLES entre el borde PINTADO del chip y el campo. Es el mismo
   número que el eje horizontal de esta misma fila (ítem 320, `column-gap`),
   así que la fila queda con el mismo aire en los dos ejes.
   ⚠️ SOLO la rama de CORREO y SOLO con wash ON:
     · La rama del TELÉFONO NO lo lleva — ahí abajo va el keypad numérico y
       16px de más lo empujan hacia el teclado del RPi (ítem 313 e-bis).
       La rama de correo no tiene keypad: holgura vertical de sobra.
     · Con el flag OFF no hay chip ni halo, así que la fila conserva su `mb-2`
       de siempre y esa pantalla queda EXACTA a como está hoy.
   NOTA: esto es una 5ª compensación local. La cura de raíz es el ítem 330
   (que el chip cargue con su propio halo y se quiten las 5 compensaciones a
   la vez) — refactor visual con su propio turno, jamás en caliente. */
.kiosk-idrow-emailwash { margin-bottom: 1.5rem; }

/* ══ ÍTEM 367 — LA X DE CERRAR, VISIBLE SOBRE CUALQUIER FONDO ══
   Verbatim: "la X para cerrar esa pantalla es Negra y casi no se ve con el borde
   azul del modal" + su propuesta: "quizas podamos poner una x mas estilizada para
   que se vea, quizas sobre un circulo blanco!".

   🔴 ERA SISTÉMICO, NO UNA PANTALLA: 38 `btn-close` en 27 archivos, y
   `btn-close-white` no se usaba ni una sola vez en código propio ⇒ TODAS eran la
   X negra por defecto de Bootstrap, invisible sobre cualquier cabecera oscura.

   ✅ POR QUÉ EL CÍRCULO Y NO `btn-close-white`: la X blanca solo sirve sobre
   fondo oscuro — el día que un modal tenga cabecera clara desaparece, y se
   cambia un problema por el contrario. El círculo blanco con la X oscura funciona
   sobre AMBOS porque EL CONTROL SE TRAE SU PROPIO FONDO en vez de depender de lo
   que haya detrás. Es la misma regla del par del chip de porcelana del kiosko
   (KAHLO-ESTANDAR-CONTRASTE-CONTROLES.md): relleno propio + borde propio.

   📌 UN SOLO SITIO, NO 38 PARCHES: se redefine `.btn-close` de Bootstrap aquí y
   con eso quedan cubiertos los 38 usos actuales Y cualquiera que se añada
   mañana, sin que nadie tenga que acordarse (ítem 323). app.css carga DESPUÉS de
   bootstrap.min.css y ningún tema toca `.btn-close`, así que este override gana
   en los 8 temas. */
.btn-close {
    position: relative;
    box-sizing: border-box;
    width: 2rem;
    height: 2rem;
    padding: 0;
    /* Bootstrap la deja en .5 y eso deslavaría el círculo entero. */
    opacity: 1;
    /* Relleno PROPIO + borde PROPIO = no depende del fondo de detrás. */
    background-color: #fff;
    /* X propia (navy de marca) en vez de la negra de Bootstrap: ~14:1 contra el
       blanco, muy por encima del 4.5:1 que pide el estándar. Va embebida para no
       depender de una variable interna de Bootstrap que puede cambiar de versión. */
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2314224E' stroke-width='2.2' stroke-linecap='round'%3e%3cpath d='M4 4l8 8M12 4l-8 8'/%3e%3c/svg%3e");
    background-repeat: no-repeat;
    background-position: center;
    background-size: .85rem .85rem;
    /* ⚠️ MEDIDO, NO ESTIMADO: con el borde a .22 el círculo daba solo 1.63:1
       contra una cabecera BLANCA — por debajo del 3:1 que pide el estándar (la X
       seguía legible, pero el círculo se perdía). A .5 el borde compuesto sobre
       blanco da ~3.4:1 y cumple, sin ensuciar nada sobre la cabecera navy (ahí
       lo que separa es el relleno blanco, 15.3:1). */
    border: 1px solid rgba(11, 18, 32, .5);
    border-radius: 50%;
    box-shadow: 0 1px 3px rgba(11, 18, 32, .28);
    transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease;
}

/* ÁREA TÁCTIL ≥44px SIN AGRANDAR EL CÍRCULO. El círculo se ve de 32px (discreto,
   a propósito) pero el dedo acierta en 44px, que es el mínimo de a11y.
   Un pseudo-elemento transparente centrado: no ocupa layout ni tapa nada. */
.btn-close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.btn-close:hover {
    background-color: #fff;
    opacity: 1;
    transform: scale(1.06);
    box-shadow: 0 2px 7px rgba(11, 18, 32, .34);
}

.btn-close:active { transform: scale(.97); }

/* Foco visible propio: el de Bootstrap era un box-shadow que sobre cabecera
   oscura casi no se distinguía. El outline se ve sobre claro y sobre oscuro. */
.btn-close:focus,
.btn-close:focus-visible {
    outline: 3px solid #C8102E;
    outline-offset: 2px;
    box-shadow: 0 1px 3px rgba(11, 18, 32, .28);
    opacity: 1;
}

/* La variante blanca de Bootstrap, si alguien la usa, no debe volver a la X
   invisible: se normaliza al mismo control. */
.btn-close-white {
    background-color: #fff;
    filter: none;
}

/* ══ ÍTEM 367-bis — LAS X QUE NO SIGUEN EL PATRÓN, TRAÍDAS AL REDIL ══════════
   Reporte: la X del modal de la cita quedó CUADRADA. Causa: `.sch-m-close` (y su
   prima `.sch-cap-x`, calendario de capacidad en Agenda y SupportTracker) son
   <button> propios con &times; — no llevan .btn-close y el barrido del 367 no
   los alcanzó. Aquí toman el MISMO ADN del círculo, en el MISMO bloque (nada de
   parche aparte): relleno blanco propio, X navy, borde medido (.5 ⇒ 3.51:1
   sobre cabecera clara), sombra y foco visible. Sus tamaños/posiciones siguen
   viviendo en sus reglas de siempre. Barrido del resto hecho: las demás ×
   sueltas del repo son "quitar elemento" (persona/parte del split), no cerrar
   ventana — semántica distinta, no entran. */
.sch-m-close,
.sch-cap-x {
    background: #fff;
    color: #14224E;
    border: 1px solid rgba(11, 18, 32, .5);
    border-radius: 50%;
    box-shadow: 0 1px 3px rgba(11, 18, 32, .28);
    transition: transform .12s ease, box-shadow .12s ease;
    /* ⛔ ÍTEM 367-ter: AQUÍ NO VA `position`. Ver el bloque de abajo. */
}

/* ══ ÍTEM 367-ter (EN PRODUCCIÓN, con captura) ═════════════
   "parece que la x del detalle de la cita está fuera de lugar."
   CAUSA REAL (no era el círculo): este bloque compartido declaraba
   `position: relative` y vive MÁS ABAJO en el archivo que
   `.sch-m-close { position: absolute; top: 10px; right: 10px; }` (~línea 2451).
   Misma especificidad (1 clase) ⇒ **gana el último** ⇒ el botón dejó de estar
   absolutamente posicionado en la esquina de `.sch-m-top` y pasó a ser un
   elemento EN FLUJO (primer hijo de la cabecera = arriba-IZQUIERDA) al que
   `top:10px; right:10px` ya solo le aplican como DESPLAZAMIENTO relativo —
   `right:10px` lo corre 10px MÁS a la izquierda: justo la X flotando
   arriba-izquierda y medio fuera que él fotografió.
   `position: relative` se había puesto para que el `::after` de 44px tuviera
   ancestro posicionado — pero `absolute` YA es ancestro posicionado, así que
   `.sch-m-close` nunca lo necesitó; la única que lo necesita es `.sch-cap-x`
   (que vive en el flex de `.sch-cap-nav` y no se posiciona sola).
   🟢 LV-05: el DISEÑO del círculo (relleno, X navy, borde, sombra, foco, área
   táctil 44×44) NO se toca — esto RESTAURA la posición aprobada, no la cambia.
   Se re-declara la posición DESPUÉS del bloque compartido para que el orden del
   archivo no vuelva a decidir quién gana. */
.sch-cap-x { position: relative; }
.sch-m-close { position: absolute; top: 10px; right: 10px; }

/* área táctil ≥44px sin agrandar el círculo — mismo truco del .btn-close */
.sch-m-close::after,
.sch-cap-x::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}
.sch-m-close:hover,
.sch-cap-x:hover { transform: scale(1.06); box-shadow: 0 2px 7px rgba(11, 18, 32, .34); }
.sch-m-close:focus-visible,
.sch-cap-x:focus-visible { outline: 3px solid #C8102E; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
    .sch-m-close, .sch-cap-x { transition: none; }
    .sch-m-close:hover, .sch-cap-x:hover { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .btn-close { transition: none; }
    .btn-close:hover, .btn-close:active { transform: none; }
}

/* ══ ÍTEM 378 — LA AGENDA SIEMPRE CUBRE LO QUE TIENE QUE ENSEÑAR ═════════════
   Caso real: la tienda cierra a las 5:00 pm y se atendió a un walk-in a las
   7:36 pm; la cita salía FUERA de la rejilla, sin etiqueta de hora, flotando en
   blanco — "parece hasta un error del sistema". No es un caso raro: un
   walk-in se sella con la hora en que se le atendió, y el último cliente del día
   casi siempre sale pasada la hora de cierre.
   La rejilla se estira para cubrirlas, y ESTA banda marca dónde estaba el
   horario REGULAR: lo de fuera se sigue viendo, pero se ve DISTINTO. La
   información de que aquello fue una excepción no se borra — se muestra. */
.sch-biz-band {
    position: absolute;
    left: 0;
    right: 0;
    z-index: 0;                 /* detrás de líneas, celdas y citas */
    background: var(--surface-color, #fff);
    pointer-events: none;       /* jamás roba un clic a las celdas de 15 min */
}

/* El fuera-de-horario queda con el fondo atenuado del contenedor. Se consigue
   tiñendo la COLUMNA entera y devolviendo el fondo normal solo a la banda. */
.sch-barber-col:has(.sch-biz-band) {
    background: repeating-linear-gradient(45deg,
        rgba(100, 116, 139, .10) 0 8px, rgba(100, 116, 139, .04) 8px 16px);
}

/* ══ ÍTEM 379 — MÁS TABS SIN DESBORDAR EL MÓVIL ══════════════════════════════
   Con el reagrupamiento, las pantallas de configuración pasan de 5-7 tabs a
   9-10. En 390px Bootstrap los ENVUELVE en dos/tres filas y la barra se come
   media pantalla. Patrón elegido: UNA fila con scroll horizontal — el mismo
   gesto que ya usa la agenda (sch-agenda-wrap) y las apps móviles que el
   barbero conoce. El degradado del borde derecho es la señal de "hay más". */
.ent-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    /* que el activo nunca quede tapado por el borde al enfocarlo */
    scroll-padding-inline: 2rem;
}
.ent-tabs .nav-link { white-space: nowrap; }

/* ══ ÍTEM 388 — EL NO-SHOW CON CARA PROPIA EN LA AGENDA ══════════════════════
   El bug reportado ("le di a No-show y la cita dice Reservada") era de PINTURA:
   el estado sí cambiaba, pero ni el bloque de la celda ni la etiqueta del modal
   tenían caso NoShow y caían al default de reservada. */
.sch-appt-noshow {
    opacity: .55;
    background: repeating-linear-gradient(45deg,
        rgba(180, 83, 9, .18) 0 6px, rgba(180, 83, 9, .06) 6px 12px);
    border-left: 3px solid #B45309;
}
/* (sin !important a propósito: las hermanas .sch-appt-active/done tampoco lo
   usan — misma especificidad, y la lección del 330 es no taparse con
   excepciones que luego producen el siguiente síntoma) */
.sch-state-noshow { background: #FEF3C7; color: #7C2D12; border: 1px solid #B45309; }

/* ══ ÍTEMS 385/389 (2026-07-27) — checkout modal con pasos + "¿con cuánto
   paga?" ══════════════════════════════════════════════════════════════════ */
/* 389: captura de efectivo (componente CashTender — citas, dividido y POS) */
.cash-quick { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .5rem; }
.cash-quick .btn { min-height: 38px; font-weight: 600; }
/* El CAMBIO es lo que el barbero le canta al cliente: grande y visible */
.cash-change { margin-top: .4rem; font-size: 1.15rem; font-weight: 700; color: #157347; }
.cash-hint { font-size: .72rem; margin-top: .25rem; }
/* 385: rejilla de métodos del paso 2 (ícono + texto, táctil) */
.chk-methods { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .5rem; max-width: 520px; }
.chk-methods .btn { min-height: 44px; text-align: left; }

/* ══ ÍTEM 410 (EN PRODUCCIÓN, desde un TELÉFONO) ═══════════
   "la X del detalle de la cita queda ESCONDIDA DETRÁS DEL MENÚ en el teléfono
    — por suerte cerré el detalle y pude darle a la X."

   🔎 CAUSA REAL (medida, no supuesta) — NO es el modal ni el diseño de la X:
   · Bootstrap 5.3 pinta los modales en `--bs-modal-zindex: 1055`
     (wwwroot/lib/bootstrap/.../bootstrap.min.css) y el fondo en 1050.
   · El BOTÓN FLOTANTE DE LA HAMBURGUESA (`.sidebar-float-wrap`,
     Components/Layout/MainLayout.razor.css:62-69) vive en **z-index: 1060**, o
     sea POR ENCIMA de cualquier modal de la app. Y en móvil
     (MainLayout.razor.css:101-106) está clavado **arriba a la DERECHA**
     (`top:.5rem; right:1rem`) — exactamente donde toda X de cerrar se pinta.
   · Su propio truco lo confirma: al CERRAR el bloque "Detalles" el modal se
     encoge, deja de empezar pegado al borde superior, la X baja unos píxeles y
     se destapa. Es una colisión de capas, no un fallo de posición del botón.
   ⇒ Afecta a TODOS los modales de la app en teléfono, no solo al de la cita.

   🟢 LV-05 RESPETADO: no se toca ni un byte del diseño del círculo (relleno,
   X navy, borde, sombra, foco, área táctil 44×44). Aquí solo se decide QUIÉN VA
   ENCIMA DE QUIÉN — es la misma clase de arreglo que el 367-ter.

   ARREGLO EN DOS CAPAS (a propósito: si una falla, la otra sostiene):
   (1) el modal sube por encima de la hamburguesa (1060) y del panel de la
       campanita (1061), y se queda POR DEBAJO de la pantalla de reconexión
       (1080) — esa tiene que poder taparlo todo;
   (2) mientras haya un modal abierto, el botón flotante se esconde: en un
       teléfono con un modal delante ese botón no sirve para nada y solo estorba.
       (`:has()` — el mismo mecanismo que ya usa `body:has(.barber-bg)` más
       arriba; sin soporte, la capa (1) ya resuelve el caso.) */
.modal {
    --bs-modal-zindex: 1070;
}
.modal-backdrop {
    --bs-backdrop-zindex: 1065;
}
/* El calendario de capacidad de la Agenda es un panel propio (no `.modal`) y su
   X (`.sch-cap-x`) cae en la MISMA esquina ⇒ mismo tratamiento. */
.sch-cap-back { z-index: 1070; }
.sch-cap-panel { z-index: 1071; }

/* capa (2): fuera los flotantes mientras hay algo delante.
   ══ ÍTEM 737 (bug 2, Lupe) — regla UNIFICADA, global y no modal-por-modal ══
   El botón flotante de accesibilidad (.a11y-trigger-btn, z 1060 — mismo piso
   que esta hamburguesa) quedaba CLICABLE por encima de los scrims que viven
   POR DEBAJO de 1060 (el modal de borrar de /admin/users: backdrop-bsp
   1050 / diálogo 1055; los modales del kiosko: 1002/1003 inline) y
   visible-pero-muerto DEBAJO del resto de la familia (1065-1085). Un cliente
   podía abrir el panel de a11y "a través" del velo. Mismo remedio que ya
   probó el 410 con la hamburguesa: con CUALQUIER modal de la casa delante,
   los flotantes NO PINTAN — un flotante encima/bajo un velo no sirve y
   estorba. (Se escoge ESCONDER y no bajar el z del botón: bajarlo por debajo
   del scrim más bajo (kiosko 1002) lo enterraría bajo el kiosk-root (1000) y
   el header (1055) por un margen de 2, frágil; y debajo de un scrim seguiría
   visible-muerto.)
   La lista del :has() es LA FAMILIA COMPLETA de scrims de la casa:
   · .modal.show      — Bootstrap y los inline (cita, checkout/CashTender,
                        AddToCalendar, PhoneVerify, kiosko, ATH)
   · .modal-backdrop  — el backdrop explícito del modal ATH
   · .sch-cap-back    — calendario de capacidad de la Agenda
   · .svrot-scrim     — modal de fotos del servicio (664)
   · .svccert-scrim   — modal de certificación de fotos (725)
   · .vmp-scrim       — picker de modelos virtuales (R-423 · LV-24)
   · .modal-backdrop-bsp / -bcp — scrims custom (borrar de /admin/users)
   · #bsp-cropper-overlay[style*="flex"] — el cropper de fotos (706). OJO al
     [style*="flex"]: photo-cropper.js CREA el overlay una vez y lo deja en el
     DOM para siempre, alternando style.display entre 'flex' (abierto) y
     'none' (cerrado) — sin el filtro por estado, el primer recorte escondería
     los flotantes PARA SIEMPRE. El #bcp-cropper-overlay del CSS viejo NO se
     lista: es herencia muerta del port de BCP (ningún JS/razor lo crea).
   Un modal nuevo que siga el patrón de la casa (.modal.show) queda cubierto
   solo. OJO: toasts, dropdowns y la campanita (.notif-back) NO están en la
   lista a propósito — no son modales y el botón de a11y debe seguir vivo.
   (Sin soporte de :has(), la capa (1) sostiene lo grueso, como en el 410.)

   ══ ÍTEM 1089 (2026-09-03) — EL BOTÓN DE ACCESIBILIDAD SE SEPARA DE ESTA
   LISTA: dos QA (Vera en Schedule, Lupe en el detalle de la cita) midieron
   que con CUALQUIER modal abierto no se podía cambiar de idioma — el único
   selector de idioma de la app quedaba inalcanzable. La ley bilingüe de la
   casa exige poder cambiar de idioma en cualquier momento; esconderlo aquí
   se lo impedía A PROPÓSITO.
   🔑 LA CAUSA RAÍZ NO ERA "faltaba esconderlo mejor": era que el z-index de
   1060 (capa 1, arriba) YA NO ganaba desde que el 737 subió `.modal` a 1070
   y `.sch-cap-panel` a 1071 — el botón se pintaba DETRÁS del modal aunque
   esta regla no existiera. Con `.a11y-trigger-btn`/`.a11y-panel` en 1075/1074
   (AccessibilityPanel.razor.css) quedan POR ENCIMA de TODA la familia de
   diálogo normal y siguen por DEBAJO de la reconexión (1080): ya no hay
   "clicable a través del velo" (está delante, no detrás) ni "visible pero
   muerto" (ya no hay scrim por encima) — los DOS problemas que el 737
   resolvió escondiendo. Esconder ya no hace falta para esa familia.
   ⛔ La hamburguesa (`.sidebar-float-wrap`) SIGUE fuera de esta excepción:
   nadie pidió tocarla y su z-index (1060) no se movió — seguiría detrás del
   modal si no se escondiera. */
body:has(.modal.show, .modal-backdrop, .sch-cap-back, .svrot-scrim, .svccert-scrim, .vmp-scrim, .modal-backdrop-bsp, .modal-backdrop-bcp, #bsp-cropper-overlay[style*="flex"])
    .sidebar-float-wrap {
    display: none;
}
/* El botón/panel de accesibilidad solo se esconde ante las herramientas de
   PANTALLA COMPLETA que no son diálogos de la casa — un recorte de foto o el
   picker de modelos virtuales no tienen "Guardar/Cancelar" con los que el
   botón pueda competir, y esconderlo ahí no le quita al usuario ningún
   selector de idioma que necesite EN ESE MOMENTO (no hay texto que traducir
   en un lienzo de recorte). El calendario de capacidad (`.sch-cap-back`)
   entra en la misma familia por ser el mismo tratamiento del ítem 410, no un
   diálogo con contenido bilingüe que alguien necesite leer en otro idioma. */
body:has(.sch-cap-back, .svrot-scrim, .svccert-scrim, .vmp-scrim, #bsp-cropper-overlay[style*="flex"])
    :is(.a11y-trigger-btn, .a11y-panel) {
    display: none;
}

/* ══ FRENTE 13 · vuelta 19 de Lupe — LA FILA DE PERMISOS NO ENVOLVÍA ═════════
   SÍNTOMA MEDIDO: los 6 permisos de /admin/users quedaban en UNA sola línea en
   desktop 1280, tablet 768 y móvil 390, y el 6º —el nuevo "Ver Reporte de Renta
   de Silla", el de etiqueta más larga— se salía del contenedor. Justo el que el
   dueño tiene que encontrar para delegarlo era el menos visible.

   CAUSA: el bloque vive en un `<td colspan="10">` (Users.razor:261) dentro de un
   `.table-responsive` (:134). Una tabla de layout AUTOMÁTICO dimensiona la celda
   a su MAX-CONTENT, así que el contenedor flex se hacía tan ancho como sus 6
   hijos y `flex-wrap: wrap` no tenía contra qué envolver — nunca fue un no-op
   suyo, es que le faltaba un ancho definido. El desbordamiento se lo comía el
   scroll del `.table-responsive`, por eso no había scroll de PÁGINA.

   ARREGLO: darle un tope de ancho con DOS LONGITUDES DEFINIDAS. Nada de `100%`
   —un porcentaje dentro de una celda dimensionada por contenido es circular y se
   ignora para el cálculo intrínseco, que es exactamente por qué esto falló—.
     · 46rem  = tope de legibilidad en pantallas anchas
     · calc(100vw - 4rem) = jamás más ancho que la ventana, con margen para el
       sidebar y los paddings
   `min()` de dos longitudes SÍ es definido ⇒ el contenedor envuelve de verdad en
   los tres viewports, sin depender de ningún ancestro. */
.perm-checks {
    max-width: min(46rem, calc(100vw - 4rem));
}

/* ══ ÍTEM 1333 — EL MISMO TOPE PARA EL BLOQUE DE LA CUENTA DEL MOSTRADOR ═════
   🔴 AUTORREPORTE (Jack): el bloque nuevo vive en el MISMO `<td colspan="10">`
   dentro del MISMO `.table-responsive`, y cayó en el MISMO fallo — a 390 sus
   tres casillas salían en una línea y la segunda empezaba fuera de pantalla.
   La causa está explicada entera aquí arriba y yo edité TRES LÍNEAS por debajo
   de ella: *una regla escrita no protege si se lee como paisaje.* Lo cazó la
   captura de 390, no el arnés — el arnés medía el ALTO tocable de las casillas
   y no si el control se VEÍA entero; ese detector ya está puesto.

   Por qué una clase propia y no reusar `.perm-checks`: el tope va en el bloque
   ENTERO (título, casillas y los dos renglones de nota), no solo en la fila de
   casillas — así los textos también envuelven en vez de cortarse. `.perm-checks`
   describe la fila de permisos; usarla aquí obligaría al siguiente a averiguar
   por qué el nombre no cuadra con el sitio.

   📌 DICHO Y NO ARREGLADO (preexistente, fuera de este lote): los renglones de
   nota del bloque de PERMISOS — `usr_perm_money_note`, `usr_perm_refund_note`,
   `usr_perm_cust_note` — quedan FUERA de `.perm-checks` (solo la envuelve la
   fila de casillas) y se cortan igual a 390. Medido en la misma corrida. */
.cta-acct-checks {
    max-width: min(46rem, calc(100vw - 4rem));
}

/* ══ ÍTEM 452 · CONTADOR DE CARACTERES ═══════════════════════════════════════
   Compañero de <CharCount /> + bsp-charcount.js. Hereda el tamaño y el gris de
   `.form-text` (es una ayuda, no un aviso) y solo cambia de color CUANDO YA
   TOPÓ — ahí sí el usuario necesita saber por qué el campo dejó de aceptar
   letras. Alineado a la derecha para que no compita con el texto de ayuda que
   suele ir debajo del mismo campo. */
.bsp-charcount {
    text-align: right;
}

.bsp-charcount-full {
    color: var(--bs-danger, #dc3545);
    font-weight: 600;
}

/* ══ ⓘ ÍTEM 453 · EL CIRCULITO DE AYUDA (Tito 2026-08-01, luz verde LV-12) ════
   Compañero de <HelpHint /> + bsp-help.js. Diseño de Kahlo
   (`MBS\kahlo\mockup-453-iconos-ayuda.html`).

   🎨 TODO EN VARIABLES DE BOOTSTRAP, NINGÚN COLOR QUEMADO. El mockup era una
   página suelta y usaba `#fff` fijo; aquí NO se puede: la app tiene 8 temas
   —onyx, deep y hc-dark son OSCUROS— y un globo blanco con letra blanca dentro
   de `hc-dark` sería un rectángulo ilegible. `--bs-body-bg` / `--bs-body-color`
   siguen al tema, así que el globo se lee en los 8 sin una regla por tema.

   ⚠️ Y NO SE TOCA `.form-label`: el ⓘ va AL LADO de la etiqueta, nunca dentro
   —un `<button>` dentro de un `<label>` es contenido interactivo anidado (HTML
   inválido) y además el clic activaría el campo asociado—. La fila la arma
   `.bsp-help-label` en el llamador. */
.bsp-help {
    display: inline-flex;
    align-items: center;
    vertical-align: text-bottom;
}

/* Fila "etiqueta + ⓘ": lo que sustituye a meter el botón dentro del <label>.

   ⚠️ MEDIDO EN PANTALLA, NO SUPUESTO (2026-08-01, /Account/Login con el CSS real):
   la primera versión hacía flex el contenedor y el ⓘ quedaba centrado
   VERTICALMENTE contra la etiqueta. Con una etiqueta corta se ve bien, pero
   "Horas de antelación para 100% de reembolso" en una `col-md-3` ocupa TRES
   líneas y el ⓘ quedaba flotando a media altura, sin pertenecerle a nada.
   Dejando el `<label>` en `display:inline`, el ⓘ FLUYE detrás de la última
   palabra y envuelve con el texto — que es como lo dibujó Kahlo ("junto a la
   etiqueta"). Sin flex, sin gap, sin alinear nada a mano. */
.bsp-help-label {
    margin-bottom: .5rem; /* el mismo aire que traía el .form-label solo */
}

.bsp-help-label > .form-label {
    display: inline;
    margin-bottom: 0;
    margin-right: .3rem;
}

/* El ⓘ. Redondo, discreto, y con área de toque de sobra: en la tablet del
   mostrador el objetivo real es el dedo, no el puntero. */
/* 🎨 POR QUÉ EL COLOR SALE DE `--bs-body-color` Y **NO** DE `--bs-primary`
   (medido en pantalla, 2026-08-01 — no supuesto):
   ningún tema de la casa redefine `--bs-primary`, así que se queda en el AZUL
   POR DEFECTO DE BOOTSTRAP (#0d6efd). Sobre el crema del tema `theshop` eso da
   **4.13:1** — por DEBAJO del piso de 4.5:1 que fija el estándar de contraste
   de la casa (`MBS\kahlo\KAHLO-ESTANDAR-CONTRASTE-CONTROLES.md`, WCAG 2.1 AA).
   Tampoco sirve `--bs-primary-text-emphasis`: es un navy oscuro fijo que en los
   6 temas OSCUROS quedaría negro sobre negro.
   `--bs-body-color` sí lo redefine CADA tema (navy #14224E en claro, blanco en
   oscuro) ⇒ en el tema claro el ⓘ y las etiquetas salen del MISMO navy que
   dibujó Kahlo, y en los oscuros salen blancos. Un solo token, 8 temas, siempre
   AA. La distinción visual de las etiquetas la carga el `font-weight`. */
.bsp-help-btn {
    width: 1.15rem;
    height: 1.15rem;
    flex: 0 0 auto;
    padding: 0;
    border: 1.5px solid var(--bs-body-color, #14224E);
    border-radius: 50%;
    background: transparent;
    color: var(--bs-body-color, #14224E);
    font: 700 .7rem/1 Georgia, "Times New Roman", serif;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    /* ⛔ CORRECCIÓN DE UN COMENTARIO FALSO (ítem 951, 2026-08-31). Aquí decía,
       verbatim: *"El botón se ve de 18px pero se TOCA en ~28px (recomendación
       mínima de objetivo táctil) sin empujar la etiqueta"*.
       🔴 Era falso por partida doble, y NO se borra —se corrige— porque un
       comentario que promete una seguridad que no existe hace que el próximo
       que mida 28 crea que cumple:
         (a) la recomendación mínima son **44**, no 28 (R-238), y
         (b) ni siquiera se tocaban 28: eran **25,98 px**. Su autor resolvió el
             `inset` contra la caja de BORDE, y el `inset` de un absoluto se
             resuelve contra la de PADDING. La cuenta buena, entera:
             18,3906 − 2×1 (el borde de 1.5px que Chrome computa a 1px) + 2×4,8
             = **25,99**. Dos fuentes decían "~28" y las dos se equivocaron
             igual: COINCIDIR NO ES VERIFICAR.
       📏 Y la fórmula era `28s − 2` (s = escala), o sea que ni al **150%** se
       llegaba: 39,97 — por CUATRO píxeles. El cruce real estaba en 164,3%.
       ✅ Lo que hay HOY: con ratón se sigue tocando en 25,98 (nadie apunta peor
       con un ratón); DONDE HAY DEDO son 44 clavados — ver el bloque
       `@media (pointer: coarse)` justo debajo. */
    outline-offset: 2px;
    position: relative;
}

.bsp-help-btn::after {
    content: "";
    position: absolute;
    inset: -.3rem;
}

/* ══ 👆 ÍTEM 951 · 44 px DONDE HAY DEDO ═══════════════════════════════════════
   LUZ VERDE del dueño (2026-08-31): el mínimo táctil se ata al PUNTERO, no al
   ancho de pantalla ni al público. `pointer: coarse` da 44 en todo lo que se
   toca con el dedo —kiosco, tablet del mostrador, teléfono— y deja el ratón
   exactamente como estaba.
   🔴 Y de paso cierra un fallo YA MEDIDO: los 44 de la casa colgaban de
   `max-width: 575.98px`, así que **el mismo teléfono ACOSTADO los perdía**
   (medido a 844×390: 38 · 38 · 28,5 px). Un puntero no cambia al girar el
   teléfono; un ancho sí.

   🔑 POR QUÉ EL SITIO LO PIDE `.bsp-help` Y NO EL BOTÓN — esto no es gusto, es
   lo que salió de grepear al escritor (ley 14). La propuesta era poner
   `min-width/min-height` en `.bsp-help-btn`; el barrido de la clase sacó DOS
   puertas más que ya pintan el CÍRCULO sobre ese mismo selector:
   `ServiceGalleryEditor.razor.css` :224 (ⓘ ámbar de la caja de pendientes) y
   :346 (ⓘ claro de la cabecera de certificación), las dos con `border-color`,
   `color` y `background`. Si el botón crece a 44, esos dos ⓘ dibujan un
   CÍRCULO DE 44 PX en ámbar y en blanco — y no fallan: se quedan callados.
   ⇒ El botón sigue siendo el DIBUJO (1,15 rem, intacto, y esos dos overrides
   siguen funcionando); el SITIO lo pide el envoltorio, que es de este
   componente y de nadie más. El resultado es el aprobado: blanco de 44, dibujo
   idéntico, y 0 % robado al vecino — porque el sitio se PIDE, no se roba.
   📌 Bonus medido: `bsp-help.js` coloca el globo con el rect del BOTÓN; como el
   botón no cambia de tamaño, el globo sigue apareciendo exactamente donde hoy.

   ⛔ MEDIDAS DESCARTADAS, NO OLVIDADAS (Kahlo las midió; no las reintentes):
   estirar el `::after` a 44 roba hasta **28,6 %** del blanco del vecino («Hoy»
   pierde 28,6 %, «Próximas» 13,5 %) · cajas de 32 o 36 px cuestan dos tercios
   del alto y NO cumplen · `2.75rem` escala con la letra y engorda un formulario
   **+156 px** y la barra de filtros **+143,8**.
   🔑 EL 44 VA EN `px`, NUNCA EN `rem`: el dedo no crece cuando crece la letra.
   Es la excepción justificada al "todo en rem" de la casa. */
@media (pointer: coarse) {
    .bsp-help {
        position: relative;      /* el ::after del botón se resuelve contra ESTO */
        min-width: 44px;
        min-height: 44px;
        justify-content: center;
        /* `text-bottom` alineaba una caja de 18 px con el pie del texto; con una
           caja de 44 el círculo quedaría flotando 22 px por encima de la línea.
           `middle` lo deja centrado sobre el texto, que es como lo dibujó el
           mockup (`.i-caja { vertical-align: middle }`). */
        vertical-align: middle;
    }

    .bsp-help-btn {
        position: static;        /* cede el anclaje del ::after al envoltorio */
    }

    /* El blanco táctil pasa a ser el envoltorio ENTERO: 44×44 exactos, centrados
       sobre el círculo. No sobresale de la caja que el layout ya le dio ⇒ no
       puede pisar a nadie. */
    .bsp-help-btn::after {
        inset: 0;
    }
}

/* Abierto/hover = colores invertidos contra el propio tema. Nada de `#fff`
   quemado: en un tema claro la letra blanca sobre navy se lee, pero en un tema
   oscuro el fondo sería blanco y la letra blanca desaparecería. */
.bsp-help-btn:hover,
.bsp-help-btn[aria-expanded="true"] {
    background: var(--bs-body-color, #14224E);
    color: var(--bs-body-bg, #fff);
}

/* El foco por teclado TIENE que verse: este control se opera con Tab. */
.bsp-help-btn:focus-visible {
    outline: 2px solid var(--bs-warning, #C8A33D);
    outline-offset: 2px;
}

/* ── El globo ────────────────────────────────────────────────────────────────
   `position: fixed` porque un popover vive en la CAPA SUPERIOR y ya no tiene
   padre del que colgarse; las coordenadas se las pone bsp-help.js con el rect
   del ⓘ. El `inset/margin` en 0 anula el centrado que el navegador le da por
   defecto a los popovers. */
.bsp-help-panel {
    position: fixed;
    inset: auto;
    margin: 0;
    z-index: 1090; /* por encima de modales de Bootstrap (1055) */
    width: min(22rem, calc(100vw - 1.5rem));
    max-height: min(70vh, 32rem);
    overflow-y: auto;
    padding: .75rem .85rem;
    border: 1px solid var(--bs-border-color, #E2E8F0);
    border-radius: .6rem;
    background: var(--bs-body-bg, #fff);
    color: var(--bs-body-color, #14224E);
    box-shadow: 0 .75rem 2rem rgba(0, 0, 0, .22);
    font-size: .85rem;
    line-height: 1.4;
}

/* Sin Popover API el atributo `popover` se ignora y el globo se vería SIEMPRE.
   Estas dos reglas dan el mismo mostrar/ocultar en los dos mundos: la nativa
   (`:popover-open`) y la del plan B de bsp-help.js (`.bsp-help-open`). */
.bsp-help-panel {
    display: none;
}

.bsp-help-panel:popover-open {
    display: block;
}

.bsp-help-panel.bsp-help-open {
    display: block;
}

/* En pantalla estrecha deja de ser globo y se vuelve hoja pegada abajo: el
   texto en ESPAÑOL es el caso peor (más largo que el inglés) y en 390px un
   globito anclado se sale de la pantalla o queda ilegible. */
@media (max-width: 575.98px) {
    .bsp-help-panel {
        left: .5rem !important;
        right: .5rem !important;
        top: auto !important;
        bottom: .5rem;
        width: auto;
        max-height: 60vh;
    }
}

.bsp-help-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: .5rem;
    margin-bottom: .4rem;
}

.bsp-help-head strong {
    color: var(--bs-body-color, #14224E);
    font-size: .88rem;
}

.bsp-help-x {
    flex: 0 0 auto;
    border: 0;
    background: transparent;
    color: var(--bs-secondary-color, #64748B);
    font-size: 1.1rem;
    line-height: 1;
    padding: 0 .15rem;
    cursor: pointer;
}

.bsp-help-x:hover {
    color: var(--bs-body-color, #14224E);
}

.bsp-help-line {
    margin: 0 0 .4rem;
}

.bsp-help-line:last-child {
    margin-bottom: 0;
}

/* 📋 Las OPCIONES de la línea "¿Qué hago aquí?" (HelpHint.PutItems). Cuelga de la
   línea de arriba, así que va SIN margen superior y con la sangría justa para que
   se lea como sub-lista y no como otra sección. */
.bsp-help-list {
    margin: 0 0 .4rem;
    padding-left: 1.15rem;
}

.bsp-help-list li {
    margin-bottom: .2rem;
}

.bsp-help-list li:last-child {
    margin-bottom: 0;
}

/* La etiqueta de cada línea de la anatomía ("¿Para qué es?", "¿Qué pongo?"...).

   ⚠️ EL MARGEN NO ES COSMÉTICO — MEDIDO EN PANTALLA (2026-08-01): Razor BORRA
   los nodos de texto que solo tienen espacios cuando están pegados a un bloque
   `@if`, así que la línea del default salía literalmente "Si lo dejas en
   blanco:24 horas— es el default". El espacio NO se puede dejar en el markup:
   se pone aquí, donde ningún compilador lo puede comer. */
.bsp-help-k {
    font-weight: 700;
    color: var(--bs-body-color, #14224E);
    margin-right: .25rem;
}

/* 🔑 EL DEFAULT REAL destacado: es el dato que hoy no sale en NINGUNA pantalla
   y por el que se pidió el ítem. Si se lee como el resto, se pierde. */
.bsp-help-def {
    font-weight: 700;
    background: rgba(200, 163, 61, .18);
    border-radius: .25rem;
    padding: 0 .2rem;
    margin: 0 .15rem 0 0; /* ver la nota de `.bsp-help-k`: Razor se come el espacio */
}

.bsp-help-inherit {
    color: var(--bs-secondary-color, #64748B);
}

/* La TRAMPA. Rojo de aviso, no de error del formulario — el campo puede estar
   perfectamente válido y aun así el número ser un desastre (el 9.3658%). */
.bsp-help-trap {
    background: rgba(200, 16, 46, .10);
    border: 1px solid rgba(200, 16, 46, .35);
    border-radius: .45rem;
    padding: .45rem .5rem;
}

/* 🎨 LV-26 — LA VARIANTE **NOTA** DEL MISMO AVISO (`TrapMuted="true"`).
   Mismo cuadro, misma ⚠️, sin el rojo. Se usa donde el aviso INFORMA y además
   termina tranquilizando (el ⓘ del catálogo de Modelos Virtuales: "…esas son
   tuyas y no se muestran en otras barberías"). Un rojo de peligro que acaba
   calmando se contradice solo.
   🔁 PARA REVERTIRLO A ROJO: se apaga en UN solo sitio —
   `VirtualModelPicker.TrapAsNote` (Components/Shared/VirtualModelPicker.razor).
   Esta regla puede quedarse aquí sin efecto; nadie más la usa.
   El tinte es EL MISMO de `.bsp-help-lock` (gris neutro translúcido) a
   propósito: un navy se vuelve invisible sobre el negro de `hc-dark`. */
.bsp-help-trap-note {
    background: rgba(128, 128, 128, .16);
    border: 1px solid var(--bs-border-color, #E2E8F0);
    border-radius: .45rem;
    padding: .45rem .5rem;
}

/* El límite duro ("lo que esto NUNCA hace"). Tinte GRIS NEUTRO y translúcido:
   un tinte navy (rgba(20,34,78,…)) es invisible sobre el negro de `hc-dark`. */
.bsp-help-lock {
    background: rgba(128, 128, 128, .16);
    border: 1px solid var(--bs-border-color, #E2E8F0);
    border-radius: .45rem;
    padding: .45rem .5rem;
}

.bsp-help-panel ul {
    margin: .25rem 0 0;
    padding-left: 1.1rem;
}

.bsp-help-panel ul li {
    margin-bottom: .2rem;
}

/* ══ 📱 ÍTEM 572-b · LOS BOTONES DE STRIPE, ALCANZABLES EN EL CELULAR ════════
   HALLAZGO del barrido de ancho (ítem 572): `/setup/payments` a 390 px CORTABA
   "Continuar el registro" y "Actualizar estado" — los dos botones con los que un
   barbero RETOMA su registro de Stripe.

   MEDIDO (banco de pruebas con el app.css, el tema y el markup REALES sobre una
   réplica del MainLayout en Chromium, 390×844):
     · visor de la tabla (.table-responsive) ........ 274.0 px
     · ancho MÍNIMO de la tabla ..................... 304.8 px  ⇒ 31 px FUERA
     · min-content por columna: nombre 49.9 · ESTADO 142.5 · acciones 82.8
     · recorte por botón: 22.8 px (Continuar) · 26.8 px (Actualizar / Conectar)

   CAUSA — no es culpa de los botones, es del BADGE: `.badge` de Bootstrap es
   `white-space: nowrap`, así que "⏳ Registro pendiente" es un bloque RÍGIDO de
   142.5 px = 52% del visor. Una tabla de layout AUTOMÁTICO le da al badge todo lo
   que pide y estruja la columna de acciones hasta su mínimo, que ya no cabe. El
   desbordamiento se lo comía el scroll del `.table-responsive` — por eso NO había
   scroll de PÁGINA y el barrido de ancho no lo cazó: el botón quedaba medio
   pintado y con la etiqueta partida a media palabra ("Continu… el registr").
   (Misma familia que el arreglo de `.perm-checks`: dentro de una tabla el ancho
   lo manda el CONTENIDO de la celda, no el contenedor.)

   ARREGLO — en pantalla de teléfono la fila se APILA: nombre / estado / acciones,
   cada bloque con el ancho completo del visor. El umbral es el MISMO 640.98 px que
   ya usa el resto de la app para decir "esto es un teléfono" (.notif-wrap,
   .sidebar-float-wrap, .home-flow). De 641 px en adelante no se toca un píxel.
   MEDIDO DESPUÉS a 390×844: 0 px fuera, los 3 botones ENTEROS a 269.2 px de ancho
   y 31 px de alto (antes 78.8–82.8 px de ancho y 52–73 px de alto por el texto
   partido en 2-3 líneas) y CERO arrastre lateral para tocarlos. ══════════════ */
@media (max-width: 640.98px) {
    .pay-acct-table,
    .pay-acct-table > tbody,
    .pay-acct-table > tbody > tr,
    .pay-acct-table > tbody > tr > td {
        display: block;
        width: 100%;
    }

    .pay-acct-table > tbody > tr {
        border-bottom: 1px solid var(--bs-border-color, #dee2e6);
        padding: .4rem 0;
    }

    .pay-acct-table > tbody > tr:last-child {
        border-bottom: 0;
    }

    .pay-acct-table > tbody > tr > td {
        border: 0;
        padding: .1rem .15rem;
    }

    /* `justify-content-end` de Bootstrap lleva !important ⇒ para que en el celular
       los botones LLENEN la fila hace falta !important también. */
    .pay-acct-actions {
        justify-content: stretch !important;
        margin-top: .25rem;
    }

    .pay-acct-actions > .btn {
        flex: 1 1 auto;
    }
}

/* ══ ÍTEM 664 · LV-17 — LA FOTO DE LA CARD ES EL DISPARADOR DE LA GALERÍA ════
   Con 2+ fotos activas la foto del servicio abre el modal del cliente
   (ServicePhotoModal): badge "N 📸" + pista "toca para ver" (punto 1).
   Dos variantes, una por anatomía de card:
   · .kbs-gal-trigger — cards kbs (wizard/kiosko): la foto es TODA la card
     (absolute inset 0) ⇒ overlay transparente en z1, DEBAJO de la banda del
     nombre (z2) y del badge ×N (z3): escoger y el stepper siguen picando igual.
   · .svc-gal-trigger — cards de landing /b/ /s/ (ítem 93): la foto es un
     rectángulo EN FLUJO (104×78) ⇒ botón-envoltorio con el badge encima.
   R-272: cero nombres reales; R-347: el "control" real es la foto entera
   (≥78px), de sobra táctil. */
.kbs-gal-trigger {
    position: absolute; inset: 0; z-index: 1;
    background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;
}
.kbs-gal-badge {
    position: absolute; top: 6px; left: 6px;
    background: rgba(20, 34, 78, .88); color: #fff;
    font-size: .62rem; font-weight: 700; letter-spacing: .03em;
    /* 🟠 LUPE 664 vuelta-1 (MAYOR-1): line-height EXPLÍCITO — la variante de
       landing (.svc-gal-trigger) declara line-height:0 para pegar la <img> y
       el badge lo HEREDABA: el pill colapsaba a ~4px con el texto desbordado. */
    line-height: 1.2;
    padding: .12rem .34rem; border-radius: 6px; pointer-events: none;
}
/* La pista vive JUSTO ENCIMA de la banda del nombre (la banda ocupa el fondo
   de la card kbs) — un renglocito que invita sin tapar nada. */
.kbs-gal-hint {
    position: absolute; left: 0; right: 0; bottom: 44px;
    color: #fff; font-size: .6rem; line-height: 1.2; text-align: center;
    text-shadow: 0 1px 2px rgba(0, 0, 0, .8); pointer-events: none;
}
.kbs-compact .kbs-gal-hint { bottom: 36px; font-size: .56rem; }
.kbs-gal-trigger:focus-visible { outline: 3px solid #C8102E; outline-offset: -3px; border-radius: 12px; }

/* La variante de LANDING: envuelve la <img class="svc-card-photo"> sin cambiar
   su tamaño (las reglas a11y-font-150/175 le siguen aplicando a la img). */
.svc-gal-trigger {
    position: relative; flex-shrink: 0; display: block;
    background: none; border: 0; padding: 0; margin: 0; cursor: pointer; line-height: 0;
}
/* 🟠 LUPE 664 vuelta-1 (MAYOR-1) — en la foto de 104×78 el badge (bottom:4,
   derecha) y la pista (bottom:0, a todo lo ancho) compartían LA MISMA franja y
   se ENCIMABAN ilegibles. Ahora cada uno tiene su piso: badge ARRIBA-derecha,
   pista ABAJO con su degradado — no se pueden tocar. (El patrón kbs no cambia:
   allí la banda del nombre ya separa los pisos y estaba bien.) */
.svc-gal-trigger .kbs-gal-badge { top: 4px; left: auto; right: 4px; }
.svc-gal-trigger .kbs-gal-hint {
    bottom: 0; border-radius: 0 0 8px 8px; padding: .4rem .2rem .15rem;
    background: linear-gradient(transparent, rgba(0, 0, 0, .55));
}
/* Con las fuentes de accesibilidad 150/175% la foto de landing ENCOGE a 72×54
   (regla del ítem 93/BUG-1) y la pista ya no cabe legible ⇒ se esconde: el
   aria-label del botón ("Ver fotos de X") sigue diciendo lo mismo, y el badge
   arriba conserva la señal visual de que hay galería. */
html.a11y-font-150 .svc-gal-trigger .kbs-gal-hint,
html.a11y-font-175 .svc-gal-trigger .kbs-gal-hint { display: none; }
.svc-gal-trigger:focus-visible { outline: 3px solid #C8102E; outline-offset: 2px; border-radius: 8px; }

/* (El "mismo trato del ítem 410" que vivía aquí para .svrot-scrim se mudó a
   la regla UNIFICADA del ítem 737 — capa (2) del bloque 410, más arriba en
   este archivo: una sola regla esconde TODOS los flotantes con CUALQUIER
   modal de la casa delante.) */

/* ══ ÍTEM 737 (bug 1, Lupe) — R-270: el body JAMÁS scrollea de lado ═════════
   A 150% de fuente a11y, el botón del EMAIL del bloque de contacto de las
   landings públicas (/b/, /s/ y compañía) medía 332px — el email es UNA
   palabra sin espacios, no envuelve — y desbordaba el viewport de 390 por
   ~3px. La fila ya hace flex-wrap (el botón baja a su propia línea), pero el
   botón mismo no tenía techo. Techo + permiso de partir: a 100% NADA cambia
   (manda el ancho natural); solo el botón que NO QUEPA envuelve su texto en
   una segunda línea dentro del botón. */
.pub-contact-actions .btn {
    max-width: 100%;
    overflow-wrap: anywhere;
}

/* ══ ENMIENDA LV-13 (2026-08-17) — EL NÚMERO DE ATH GRANDE Y BOLD ══════════
   Ítem del lote LV-28: "el numero de telefono en esta pantalla debe estar
   mas grande y bold" — porque "usan mucho el numero de telefono en vez de
   el qr code": el número ES la instrucción de pago de muchos usuarios
   reales de ATH, no letra chica. Lo pinta AthPhoneLine (las 3 superficies
   hermanas: pay-ath, modal de cobro ATH, guest booking).

   ══ ENMIENDA #3 · ÍTEM 774 (2026-08-18, captura montada por el dueño) ══════
   "cuando dije que quería grande el número de teléfono debajo del QR Code
   era algo como esto, ¡ASÍ DE GRANDE!" — señalando el $10.00 del tope. De
   `1.35em/700` (texto en negrita) a **PROTAGONISTA**: 2rem/800, el peso
   visual del MONTO (2.2rem en pay-ath, 2.4rem en el modal, 2rem en guest).
   · **rem, ya no em:** el bloque salió del contexto `.small` de la línea
     vieja; en em habría heredado el 0.875 del padre y llegado a ~1.75rem.
   · **nowrap + tamaño que cede:** un número jamás se parte en dos líneas
     (leerlo partido es teclearlo mal). En 390 el `clamp` lo deja bajar
     hasta 1.6rem antes que romperlo — "+52 999 123 4567" (el caso largo,
     código de país + grupos) cabe entero.
   · **Sin color propio (R-315, ver la nota del ⓘ arriba):** hereda
     `--bs-body-color`, el único token que los 8 temas redefinen ⇒ AA en
     claro y en oscuro. El navy del monto sale de `--bs-primary`, que
     NINGÚN tema redefine: cablearlo aquí lo dejaría navy-sobre-navy en los
     6 temas oscuros. Por eso el bloque tampoco vive dentro de un bg-white. */
.ath-phone-block {
    text-align: center;
    margin: .15rem 0 .5rem;
}

.ath-phone-lbl {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .3rem;
    font-size: .8rem;
    line-height: 1.25;
    flex-wrap: wrap;
}

.ath-phone-num {
    font-size: clamp(1.6rem, 7.5vw, 2rem);
    font-weight: 800;
    line-height: 1.15;
    white-space: nowrap;
}

/* ══ ENMIENDA #4 · ÍTEM 775 (el dueño 2026-08-18) — "PARA LOS CEGATOS COMO YO"
   Verbatim: el texto de "qué va a pasar ahora" *"puede aparecer en una letra
   no tan grande como la del teléfono pero sí un poco más grande, para los
   cegatos como yo"*. **No es un chiste:** esa pantalla la lee un cliente DE
   PIE, con el celular, **justo después de mandar dinero** — es el texto que
   le dice qué esperar. Si se lee chiquito, se salta, y de ahí salen las
   llamadas de "¿y ahora qué?" que el producto existe para evitar. Y el
   público de una barbería no es joven-tecnológico: es todo el mundo (R-270).
   La jerarquía que fija el ítem, y que estas dos reglas hacen cumplir:
     1️⃣ monto y teléfono (2–2.4rem, protagonistas — ítem 774)
     2️⃣ **el texto de qué pasa ahora → .ath-next-step, 1.15rem** (este)
     3️⃣ notas y ayudas (el `small` de siempre, sin tocar)
   Un solo escalón: subirlo más lo pondría a competir con el monto, que es
   justo lo que el 774 acaba de coronar.
   ⚠️ **Solo tamaño, cero palabras nuevas** — LV-13 congela el COPY, no la
   presentación (enmienda de presentación #4). */
.ath-next-step {
    font-size: 1.15rem;
    line-height: 1.5;
}

/* ══ 🚨 LV-20 · TARJETAS DE MÉTODO DE PAGO (PaymentMethodModal) ═══════════════
   El defecto que arreglan, medido: la decisión "¿cómo pago?" se dibujaba con
   `form-check` sueltos — un radio de 1rem y una etiqueta de texto normal. En un
   celular de 390 px eso es un blanco de ~16 px para el pulgar y CERO pista visual
   de cuál está escogido. Aquí cada método es una TARJETA COMPLETA que ES el label
   del radio: el área de toque es toda la tarjeta (>44 px de alto, el mínimo de la
   casa), y la escogida se marca con borde + fondo, no solo con el puntito.

   🎨 CONTRASTE (R-315) — los colores salen de tokens que los 8 temas redefinen
   (`--bs-body-color`, `--bs-border-color`, `--bs-secondary-color`). El realce de
   la escogida usa `--bs-primary` SOLO en el borde y en un fondo a 8% de opacidad
   (`color-mix`), nunca como color de texto: navy-sobre-navy en los 6 temas
   oscuros es exactamente la trampa que ya está documentada en el ⓘ y en
   `.ath-phone-num`. Con `color-mix` no soportado, la tarjeta se queda con su
   fondo normal y el borde grueso sigue diciendo cuál está escogida. */
.pm-tile {
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    width: 100%;
    margin-bottom: .5rem;
    padding: .7rem .75rem;
    border: 1px solid var(--bs-border-color, #dee2e6);
    border-radius: .6rem;
    cursor: pointer;
}

.pm-tile:last-child {
    margin-bottom: 0;
}

.pm-tile-on {
    border-color: var(--bs-primary, #14224E);
    box-shadow: inset 0 0 0 1px var(--bs-primary, #14224E);
    background: color-mix(in srgb, var(--bs-primary, #14224E) 8%, transparent);
}

.pm-tile-body {
    display: flex;
    flex-direction: column;
    gap: .15rem;
    flex: 1 1 auto;
    min-width: 0;
}

.pm-tile-title {
    font-weight: 600;
    line-height: 1.3;
}

/* El "qué le pasa a tu dinero". NO es letra de nota al pie: es la mitad de la
   decisión, así que se queda a .9rem — un pelo bajo el título, legible de pie
   con el celular en la mano (misma lección del .ath-next-step de arriba). */
.pm-tile-sub {
    font-size: .9rem;
    line-height: 1.35;
    color: var(--bs-secondary-color, #6c757d);
}

/* El radio no se encoge cuando el título hace wrap en 390 px. */
.pm-tile > .form-check-input {
    flex: 0 0 auto;
    width: 1.25rem;
    height: 1.25rem;
    margin-top: .1rem;
}

/* ══ 🚨 TOUR CHECK-IN MÓVIL · #5 (el dueño 2026-08-23) — LA PISTA DE 50px ═══════
   Verbatim: "el primer card de barbero estaba frizado, no podía coger otro
   barbero porque los cards pasaban detrás del primer card." CUARTA vez que se
   reporta este carrusel — y las tres anteriores atacaron la PERCEPCIÓN (margen,
   sombra, divisor 339, relleno sólido). Esta vez el defecto es FUNCIONAL y la
   causa es de GEOMETRÍA, medida en el navegador a 390x844 con este mismo CSS:

     pista de barberos:  50px de clientWidth  ← tarjetas de 148px (34% visible)
     pista de servicios: 212px de clientWidth ← 143% visible ⇒ POR ESO funcionó

   Dos flechas de 40px + la tarjeta anclada de 148px + la frontera del 339 se
   comen la fila entera. De cada barbero se ve una REBANADA de 50px cortada en
   el canto de la anclada (la ilusión de "pasan por detrás"), y con
   `scroll-snap: mandatory` esa rebanada es además el único blanco táctil.
   El propio ítem 339 lo dejó anotado: "los 42px de pista NO los introduce este
   ítem — son de fábrica y merecen su propio arreglo". Este es ese arreglo.

   🔤 SE LLAMABA `kbs-wrap-pinned` (renombrada por Tito 2026-08-25 con R-431):
   ese nombre pasó a mentir cuando se soltó el anclaje. Lo que esta clase hace
   NO es anclar — es GEOMETRÍA DE TELÉFONO, y con la tarjeta suelta hace MÁS
   falta que antes: los 148px de la anclada se recuperan para la pista, pero
   "Cualquier barbero" ahora ocupa un turno DENTRO de ella.

   ✅ EN TELÉFONO (≤480px), SOLO en el carrusel marcado `kbs-wrap-phone`
   (hoy: el de barberos del check-in móvil):
   ① FUERA LAS FLECHAS — en pantalla táctil el swipe es nativo (touch-action:
     pan-x ya está puesto); las flechas son un remo de escritorio que aquí
     cuesta 96px de pista. El drag de mouse (initCarouselDrag) sigue vivo para
     la ventana angosta de escritorio.
   ② GEOMETRÍA COMPACTA 116x132 — la MISMA variante que la casa ya usa en el
     POS (.kbs-compact, declaraciones calcadas de ahí: cero tamaño nuevo).
   📏 RESULTADO MEDIDO tras el cambio: pista ≈178px para tarjetas de 116px ⇒
   **1.5 tarjetas visibles** — una entera para tocar y media asomada que dice
   "hay más, desliza". La rebanada asomada es la AFFORDANCE que las flechas
   daban; por eso ① sin ② no bastaba (146px = 0.99 tarjetas: ni rebanada ni
   flechas = nadie sabe que hay más barberos).
   ⛔ QUIÉN NO CAMBIA: el kiosco (tablet 1024x600 FIJA — la media query jamás
   aplica; R-384/ítem 800: "no cambiemos el layout de eso please"), el POS y
   los paneles de staff (no llevan esta clase), y el carrusel de SERVICIOS del
   propio móvil (funcionó — el dueño lo confirmó en vivo; no se toca lo que
   funciona). El de barberos del WIZARD de citas comparte el patrón y muy
   probablemente el defecto — anotado para su propio ítem, no se arregla a
   ciegas desde aquí. */
@media (max-width: 480px) {
    .kbs-wrap-phone > .kbs-arrow { display: none; }
    .kbs-wrap-phone .kbs-card { width: 116px; height: 132px; border-radius: 12px; }
    .kbs-wrap-phone .kbs-name-band { padding: 5px 6px 6px; }
    .kbs-wrap-phone .kbs-name-band .nm { font-size: .72rem; line-height: 1.1; }
    .kbs-wrap-phone .kbs-check { width: 20px; height: 20px; min-width: 20px; top: 4px; right: 4px; font-size: .66rem; }
}

/* ══ 📺 LOTE TV · f= — TAMAÑO DE LETRA DE LA FRANJA, POR TELEVISOR ════════════
   El dueño: *"tiene que ser un parámetro porque un cliente tiene 4 televisores
   diferentes"* — el link vive en el stick, así que ?f=1..5 viaja en la URL y
   aterriza aquí como clase en la raíz (`tv-f1`..`tv-f5`). Sin JS, sin re-render:
   un factor `--tvf` multiplica los `clamp()` YA aprobados de la franja.
   🔒 CONTROL DE REGRESIÓN: `tv-f5` ⇒ `--tvf: 1` ⇒ `calc(x * 1) = x` — los links
   de hoy (sin ?f=) pintan PIXEL-IDÉNTICO a antes de este lote. Y como el mínimo,
   el vh y el techo se multiplican LOS TRES por el mismo factor, la disciplina de
   R-305/R-313 (la letra atada al ALTO para que la cuenta de filas escale) queda
   intacta en cada tamaño.
   📏 La paginación del 611 MIDE la altura real de las filas en el navegador ⇒
   letra más chica = más nombres por página, sin ningún conteo cableado.
   ⛔ QUÉ NO ESCALA, a propósito: los QR (un QR más chico deja de escanear de
   lejos), el panel de anuncios, el cartel de llamado (`.tv-turn-call-*`, maqueta
   aprobada), el nombre de la tienda y su foto. La spec dice "los textos de la
   franja: título, lista de espera, pie" — eso y solo eso.
   Escalera: pasos ~13% — f=1 ≈ la mitad del tamaño de hoy (los nombres truncados
   del TV 4K de la evidencia caben completos dos pasos antes). */
.tv-board { --tvf: 1; }
.tv-board.tv-f1 { --tvf: .5; }
.tv-board.tv-f2 { --tvf: .62; }
.tv-board.tv-f3 { --tvf: .75; }
.tv-board.tv-f4 { --tvf: .87; }
.tv-board.tv-f5 { --tvf: 1; }
.tv-board .tv-queue-title { font-size: calc(clamp(.85rem, 1.259vh, 1.7rem) * var(--tvf, 1)); }
.tv-board .tv-queue-list, .tv-board .tv-appt-list { gap: calc(clamp(.859rem, 1.272vh, 1.717rem) * var(--tvf, 1)); }
.tv-board .tv-queue-row {
    gap: calc(clamp(1.249rem, 1.850vh, 2.498rem) * var(--tvf, 1));
    padding: calc(clamp(.859rem, 1.272vh, 1.717rem) * var(--tvf, 1)) calc(clamp(1.249rem, 1.850vh, 2.498rem) * var(--tvf, 1));
}
.tv-board .tv-turn { font-size: calc(clamp(2.498rem, 3.700vh, 4.995rem) * var(--tvf, 1)); }
.tv-board .tv-name { font-size: calc(clamp(1.45rem, 2.891vh, 3.903rem) * var(--tvf, 1)); }
.tv-board .tv-appt-who { font-size: calc(clamp(1.45rem, 2.891vh, 3.903rem) * var(--tvf, 1)); }
.tv-board .tv-appt-rest { font-size: calc(clamp(0.901rem, 1.336vh, 1.803rem) * var(--tvf, 1)); }
.tv-board .tv-empty { font-size: calc(clamp(1.3rem, 1.926vh, 2.6rem) * var(--tvf, 1)); }
.tv-board .tv-footer { font-size: calc(clamp(.8rem, 1.185vh, 1.6rem) * var(--tvf, 1)); }

/* ══ 📺 t= — TAMAÑO DE LA FOTO DE LA TIENDA, POR TELEVISOR (el dueño 2026-08-24)
   ══ 🔴 SEMÁNTICA v3 — LA DEFINITIVA, CON EL MOCKUP DEL DUEÑO ═════════════════
   La v1 (interpolar solo el max-height 38→62vh) fue invisible en fotos anchas
   (*"honestamente no veo que la foto se haga más pequeña o cambie del todo"*).
   La verdad del modelo mental, verbatim: *"para mí esto es la foto de la
   tienda a un 25% del tamaño original, 50% del tamaño original, 75% del
   tamaño original y tamaño original 100%"*.
   ✅ t = FRACCIÓN DEL TAMAÑO NATURAL DE LA FOTO: t=1 ⇒ 25% de sus
   dimensiones originales · t=2 ⇒ 50% · t=3 ⇒ 75% · t=4 ⇒ 100% (natural).
   🔧 CÓMO, EN CSS PURO: `zoom` (--tvz) sobre el <img> con width:auto —
   zoom multiplica el tamaño INTRÍNSECO del elemento y SÍ ocupa layout
   (a diferencia de transform), así que el marco fit-content lo abraza y
   la lista de abajo gana/pierde el espacio real. Estandarizado (CSS
   Viewport) y soportado por el Chromium de los sticks (y Firefox 126+;
   un navegador fósil que lo ignore pinta el tamaño natural capeado —
   degradación honesta, jamás rompe el layout).
   ⛔ TOPE FÍSICO (orden del dueño: la columna sigue FIJA): `max-width: 100%`
   capea la fracción al ancho de la columna — una foto GRANDE se capea al
   ancho en los niveles altos y ACHICA visible en t=1/t=2 (siempre hace
   ALGO); una foto CHICA crece limpia 25→100 sin recorte. `max-height: 62vh`
   queda de seguro anti-fotos-kilométricas (el techo que no tapa
   nombre/lista; la paginación del 611 MIDE y reparte sola las filas).
   🔒 CONTROL DE REGRESIÓN (la lección del f=): `tv-t0` = EL COMPORTAMIENTO
   DE HOY (zoom 1 + max-height 38vh) y es el default — los links de hoy
   (sin ?t= o con basura) pintan PIXEL-IDÉNTICO. El clamp de TvBoardView.Tx
   manda todo lo que no sea 0..4 exacto al 0.
   ⛔ QUÉ NO ESCALA, a propósito (misma doctrina del f=): los QR (un QR más
   chico deja de escanear de lejos), el marco concéntrico (fit-content:
   abraza a la foto A CUALQUIER tamaño; el radio interior se compensa por
   zoom — ver .tv-store-photo), el nombre de la tienda, la lista y el panel
   de anuncios. */
.tv-board { --tvt: 38vh; --tvz: 1; }
.tv-board.tv-t0 { --tvt: 38vh; --tvz: 1; }   /* hoy = default */
.tv-board.tv-t1 { --tvt: 62vh; --tvz: .25; } /* 25% del tamaño original */
.tv-board.tv-t2 { --tvt: 62vh; --tvz: .5; }  /* 50% */
.tv-board.tv-t3 { --tvt: 62vh; --tvz: .75; } /* 75% */
.tv-board.tv-t4 { --tvt: 62vh; --tvz: 1; }   /* 100% = tamaño natural */

/* ══ 📺 c= — ANCHO DE LA COLUMNA IZQUIERDA, POR TELEVISOR (mini-lote #12)
   El dueño, verbatim: *"yo creo que también debemos tener oootro parámetro
   para el ancho de la columna c=0-5"*. Hermano de f= y t= — mismo patrón:
   la clase en la raíz mueve SOLO `--tvcw` (el width de `.tv-queue`). El
   panel de anuncios es `flex: 1` y absorbe el complemento SOLO — cero
   cambios en él. Tope 50% = media pantalla (más mataría los anuncios).
   🔒 CONTROL DE REGRESIÓN: `tv-c0` ⇒ 25% = el ancho de siempre — los links
   de hoy (sin ?c= o con basura) pintan PIXEL-IDÉNTICO; el clamp vive en
   TvBoardView.Cx. `min-width: 260px` se respeta SIEMPRE (está en la regla
   base de .tv-queue, la escalera no lo toca).
   🤝 SINERGIAS REALES, no de adorno: columna más ancha ⇒ el tope físico del
   t= sube (fotos t=4 con más cancha) y los nombres de la lista se truncan
   menos (el paginador del 611 MIDE las filas y se reacomoda solo).
   ⛔ Los QR de los slides NO cambian de tamaño (viven en el panel derecho
   y sus px son fijos — doctrina de la familia). */
.tv-board { --tvcw: 25%; }
.tv-board.tv-c0 { --tvcw: 25%; } /* hoy = default */
.tv-board.tv-c1 { --tvcw: 30%; }
.tv-board.tv-c2 { --tvcw: 35%; }
.tv-board.tv-c3 { --tvcw: 40%; }
.tv-board.tv-c4 { --tvcw: 45%; }
.tv-board.tv-c5 { --tvcw: 50%; }

/* ══ 📺 LOTE TV · R-415 — EL SLIDE DE BIENVENIDA SIN QR SE RECOMPONE ══════════
   Con el módulo de citas apagado el QR y su línea se van; el hueco no se queda
   vacío: la FOTO de la barbería se luce más grande (el dueño: "la foto puede
   lucirse más grande — gratis"). Solo aplica al slide de bienvenida (la clase
   la pone su markup); las tarjetas de anunciantes conservan sus 150px. */
.tv-welcome-noqr .tv-card-photo { width: 260px; height: 260px; }

/* ══ 🚪 LA BARRA DEL ANÓNIMO (mockup v2 de la salida pública, el dueño 2026-08-23)
   Tres zonas: ← Volver (solo pasos intermedios) · nombre de la TIENDA centrado ·
   🏠 (a /s/{slug} o /locations). Sticky: en un formulario largo de teléfono la
   salida no puede irse con el scroll. Tokens del tema (R-315): superficie y
   texto de las variables que los 8 temas redefinen. Blancos táctiles ≥44px. */
.pubnav {
    position: sticky; top: 0; z-index: 5;
    display: flex; align-items: center; gap: .25rem;
    min-height: 48px; padding: .25rem .5rem;
    background: var(--surface-color, #fff);
    border-bottom: 1px solid var(--bs-border-color, #dee2e6);
}
.pubnav-side { flex: 0 0 84px; display: flex; }
.pubnav-side-end { justify-content: flex-end; }
.pubnav-back { text-decoration: none; font-weight: 600; padding: .4rem .5rem; white-space: nowrap; }
.pubnav-title {
    flex: 1 1 auto; min-width: 0; text-align: center; font-weight: 700;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    color: var(--bs-body-color, #1C2333);
}
.pubnav-home {
    font-size: 1.35rem; line-height: 1; text-decoration: none;
    padding: .45rem .55rem; border-radius: .5rem;
}
.pubnav-home:hover { background: var(--icon-chip-bg, #F1F5F9); }

/* ══ 🖼️ LOTE GRANDE · #8 — EL PREVIEW DE LA TARJETA EN /admin/ads ═════════════
   El slide se pinta con SUS clases reales (TvCardSlide = el mismo componente
   del televisor) dentro de una caja de diseño 800×600 encogida con transform —
   el mismo truco del postecito de reconexión: 0 bytes de CSS del slide nuevo.
   0.35 ⇒ 280×210: cabe en la columna del tab Media, y a 390px (columna
   apilada) no empuja el Guardar fuera de vista. */
.tv-card-preview {
    width: 280px; height: 210px; overflow: hidden;
    border: 1px solid var(--bs-border-color, #dee2e6); border-radius: .5rem;
}
.tv-card-preview > .tv-house-ad {
    width: 800px; height: 600px;
    transform: scale(0.35); transform-origin: top left;
}

/* ══ R-423 · LV-24 — EL CHIP DEL MODELO VIRTUAL (editar-servicio) ═════════════
   El botón "🧑‍🎨 Usar Modelo Virtual" y, una vez asignado, el CHIP VERDE con
   Cambiar/Quitar. LV-24 punto 2 (ley de TRANSPARENCIA): *"el dueño SIEMPRE ve
   qué está supliendo sus fotos"* — por eso el estado asignado es una tarjeta
   VISIBLE con mini-foto y nombre, y no un puntito ni un texto gris.
   💚 VERDE porque el chip dice "esto está resuelto"; el rojo de la casa se
   reserva para lo destructivo y para la acción principal del picker.
   📱 Medido a 390px: el chip envuelve y los dos botones caen a una fila propia,
   los dos de altura de dedo.
   ⚠️ Va en app.css y NO en un .razor.css scoped: vive dentro de Services.razor,
   que no tiene hoja propia. */
/* `.vm-row` ya solo lleva el CHIP (el botón se mudó a la fila de la galería,
   MENOR de Lupe 2026-08-25). */
.vm-row { margin-top: .75rem; display: flex; flex-direction: column; gap: .5rem; align-items: flex-start; }
/* 44px = EL MISMO alto que "Subir fotos" y "Vista previa", sus vecinos de fila.
   Estaba en 46 de cuando el botón vivía solo en su propio bloque; en fila, 2px
   de más lo dejaban descuadrado contra los otros dos. */
.vm-btn { min-height: 44px; display: inline-flex; align-items: center; gap: 7px; }

.vm-chip {
    display: flex;
    align-items: center;
    gap: 10px;
    background: #e8f6ee;
    border: 1.5px solid #0f7b45;
    border-radius: 10px;
    padding: 8px 10px;
    max-width: 520px;
    width: 100%;
}

/* R-472 (ítem 1164): el hero del modelo virtual en Setup > Servicios. Cuadrado
   ⇒ sin ancla el recorte empezaba en el 12,65 %, bajo la coronilla de las 153. */
.vm-chip-mini { width: 52px; height: 52px; border-radius: 8px; object-fit: cover; object-position: 50% 0; flex: none; }
.vm-chip-txt { flex: 1 1 auto; min-width: 0; }
.vm-chip-nm { font-weight: 800; font-size: .88rem; color: #16213d; }
.vm-chip-sub { font-size: .74rem; color: #5b6780; }

/* ══ 🔒 LV-26 pieza 2 · R-430 — EL CHIP DORMIDO SE VE DORMIDO ═════════════════
   El verde del chip significa "esto está resuelto y saliendo". Cuando las fotos
   propias mandan, el modelo NO está saliendo — y el chip verde decía lo
   contrario con el color mientras lo desmentía en gris de .74rem. El color pesa
   más que la letra chica: por eso el dueño del producto leyó "activo" dos veces.
   Dormido = superficie neutra, borde gris, mini-foto atenuada y 🔒 candado.
   ⚠️ NO se usa `opacity` sobre el chip entero: eso apagaría también la frase que
   hay que LEER y los botones Cambiar/Quitar, que siguen 100% vivos (el chip
   dormido no es un control deshabilitado — es un estado). */
.vm-chip-asleep {
    background: #f3f4f6;
    border-color: #94a3b8;
}
.vm-chip-asleep .vm-chip-mini { filter: grayscale(1); opacity: .55; }
.vm-chip-asleep .vm-chip-nm { color: #475569; }

/* La frase que hay que poder leer sin buscarla: tamaño de cuerpo (NO .74rem),
   negrita y su propia franja ámbar de "en espera" — el mismo ámbar de la casa
   que ya marca lo pendiente en la galería (#B45309 / #FFF7ED, par R-236c). */
.vm-chip-asleep-msg {
    margin-top: .35rem;
    font-size: .86rem;
    font-weight: 700;
    line-height: 1.3;
    color: #92400E;
    background: #FFF7ED;
    border: 1px solid #B45309;
    border-radius: 8px;
    padding: .35rem .45rem;
}
.vm-chip-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
/* 👆 n-2 de la vuelta 11 (Lupe): medían 40px al lado de los 5 botones de la
   tarjeta de foto, que miden 44. Pre-existente de LV-24, pero con LV-26 estos
   dos botones son LA SALIDA que la frase del chip dormido le manda a usar al
   dueño — el botón que la pantalla te pide tocar no puede ser el más chico. */
.vm-chip-actions .btn { min-height: 44px; }

/* ⏳ m-2 de la vuelta 11 — "ESTO AÚN NO ESTÁ GUARDADO".
   El chip previsualiza el resultado de tu edición (que es justo lo que al dueño
   le faltaba), pero mientras no le des a Guardar el cliente sigue viendo lo de
   antes. Franja NEUTRA a propósito: no compite con el ámbar de "en espera" ni
   con el verde de "esta gana" — no es un estado del modelo, es un estado del
   FORMULARIO. */
.vm-chip-pending {
    margin-top: .35rem;
    font-size: .8rem;
    font-weight: 600;
    line-height: 1.3;
    color: #1F2937;
    background: #F1F5F9;
    border: 1px solid #64748B;
    border-radius: 8px;
    padding: .3rem .45rem;
}

/* ══ 🌑 n-3 DE LA VUELTA 11 — EL SELLO Y EL CHIP EN LOS TEMAS OSCUROS ═════
   Lupe midió en `hc-dark`: la nota gris nueva SÍ se adapta (18.35:1) porque usa
   un tinte TRANSLÚCIDO sobre la superficie del tema; el sello y el chip se
   quedaban CLAROS (colores quemados, herencia de LV-24). Nada quedaba ilegible
   — quedaba fuera de sitio: dos parches de día dentro de una pantalla de noche.
   🔧 EL PATRÓN QUE SE COPIA (el de `.bsp-help-trap-note`): tinte translúcido +
   tinta clara. El tinte compone sobre la superficie del tema, así que un solo
   bloque sirve para los SIETE oscuros sin un color por tema.
   ✅ LOS SIGNIFICADOS NO SE MUEVEN (R-430 depende de ellos): verde = esta gana ·
   gris = dormido · ámbar = en espera. Cambia el tono, no el idioma.
   📏 MEDIDO antes de escribirlo, sobre la superficie de tarjeta de los 7 temas
   (la peor siempre es `jade`, la más clara): sello 8.94:1 · nombre del chip
   10.02:1 · subtítulo 8.32:1 · dormido 9.36:1 · franja ámbar 7.63:1 ·
   "sin guardar" 9.31:1. Todos AAA (≥7:1).
   ⚠️ Los temas CLAROS no se tocan: LV-26 está aprobada con esos colores. */
/* 📌 El SELLO no lleva bloque oscuro, y es una decisión MEDIDA: vive dentro de
   `.svcgal-card`, que trae porcelana clara quemada y no sigue al tema. Teñirlo
   de tema oscuro lo dejó en 1.06:1 (ilegible) contra los 7.15:1 que tiene hoy.
   El porqué completo está junto a su regla, en ServiceGalleryEditor.razor.css. */

.page.theme-hc-dark .vm-chip,
.page.theme-onyx    .vm-chip,
.page.theme-deep    .vm-chip,
.page.theme-jade    .vm-chip,
.page.theme-rose    .vm-chip,
.page.theme-space   .vm-chip,
.page.theme-nebula  .vm-chip {
    background: rgba(16, 185, 129, .12);
    border-color: #34D399;
}

.page.theme-hc-dark .vm-chip-nm,
.page.theme-onyx    .vm-chip-nm,
.page.theme-deep    .vm-chip-nm,
.page.theme-jade    .vm-chip-nm,
.page.theme-rose    .vm-chip-nm,
.page.theme-space   .vm-chip-nm,
.page.theme-nebula  .vm-chip-nm { color: #E2E8F0; }

.page.theme-hc-dark .vm-chip-sub,
.page.theme-onyx    .vm-chip-sub,
.page.theme-deep    .vm-chip-sub,
.page.theme-jade    .vm-chip-sub,
.page.theme-rose    .vm-chip-sub,
.page.theme-space   .vm-chip-sub,
.page.theme-nebula  .vm-chip-sub { color: #CBD5E1; }

.page.theme-hc-dark .vm-chip-asleep,
.page.theme-onyx    .vm-chip-asleep,
.page.theme-deep    .vm-chip-asleep,
.page.theme-jade    .vm-chip-asleep,
.page.theme-rose    .vm-chip-asleep,
.page.theme-space   .vm-chip-asleep,
.page.theme-nebula  .vm-chip-asleep {
    background: rgba(148, 163, 184, .16);
    border-color: #94A3B8;
}
/* El dormido pierde el verde también en oscuro: si el nombre se quedara con la
   tinta del despierto, el chip volvería a verse igual mande o duerma — la
   trampa exacta que mordió al dueño (R-430). */
.page.theme-hc-dark .vm-chip-asleep .vm-chip-nm,
.page.theme-onyx    .vm-chip-asleep .vm-chip-nm,
.page.theme-deep    .vm-chip-asleep .vm-chip-nm,
.page.theme-jade    .vm-chip-asleep .vm-chip-nm,
.page.theme-rose    .vm-chip-asleep .vm-chip-nm,
.page.theme-space   .vm-chip-asleep .vm-chip-nm,
.page.theme-nebula  .vm-chip-asleep .vm-chip-nm { color: #CBD5E1; }

.page.theme-hc-dark .vm-chip-asleep-msg,
.page.theme-onyx    .vm-chip-asleep-msg,
.page.theme-deep    .vm-chip-asleep-msg,
.page.theme-jade    .vm-chip-asleep-msg,
.page.theme-rose    .vm-chip-asleep-msg,
.page.theme-space   .vm-chip-asleep-msg,
.page.theme-nebula  .vm-chip-asleep-msg {
    background: rgba(245, 158, 11, .18);
    border-color: #F59E0B;
    color: #FCD34D;
}

.page.theme-hc-dark .vm-chip-pending,
.page.theme-onyx    .vm-chip-pending,
.page.theme-deep    .vm-chip-pending,
.page.theme-jade    .vm-chip-pending,
.page.theme-rose    .vm-chip-pending,
.page.theme-space   .vm-chip-pending,
.page.theme-nebula  .vm-chip-pending {
    background: rgba(148, 163, 184, .16);
    border-color: #94A3B8;
    color: #E5E7EB;
}

/* ══ R-423 — LA TIRA DE FOTOS DEL ADMIN DE LA CASA (/admin/virtual-models) ════
   Las miniaturas se piden al endpoint cacheable, nunca por el circuito: con 10
   modelos × 8 fotos, traer los bytes serían ~8 MB en cada repintado. */
.vma-strip { display: flex; flex-wrap: wrap; gap: 8px; }

.vma-photo {
    position: relative;
    width: 110px;
    border: 2px solid #d7dce6;
    border-radius: 10px;
    overflow: hidden;
    background: #0d1737;
}

    /* La #1 es la PRINCIPAL — mismo contrato que la ⭐ de la galería del
       servicio (LV-17 punto 7): un solo dato manda el orden Y la principal. */
    .vma-photo.primary { border-color: #C8102E; }
    /* R-472 (ítem 1164): los 8 ángulos del modelo en Admin > Modelos
       virtuales. Medido en el DOM: sin ancla el recorte empezaba en el
       14,47 % ⇒ las 153 fotos de retrato perdían la coronilla, y ésta es
       justo la pantalla donde se aprueba o se rechaza un set. */
    .vma-photo img { width: 110px; height: 140px; object-fit: cover; object-position: 50% 0; display: block; }

.vma-pos {
    position: absolute;
    top: 4px;
    right: 4px;
    background: #14224E;
    color: #fff;
    border-radius: 999px;
    min-width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: .72rem;
    font-weight: 700;
}

.vma-photo-btns { display: flex; gap: 4px; padding: 4px; background: #fff; }
.vma-photo-btns .btn { flex: 1; min-height: 34px; padding: .15rem .3rem; }

/* ══ 🚨 LV-32 · EL PAR DE BOTONES DE PAGO (el dueño 2026-08-31) ════════════════
   "Pagar en línea" y "Pagar en la barbería" lado a lado en escritorio, apilados en
   celular con el de la barbería ABAJO. Lo usan las DOS pantallas de reservar
   (BookAppointment y GuestBooking) — una sola gramática, un solo sitio donde
   arreglarla (R-364).

   📐 MEDIDO por Kahlo con getBoundingClientRect, no estimado:
   escritorio izq 280 px + ⓘ 44 + der 243 px + ⓘ 44 = par de 535 px, que sobresale
   15 px de la columna de avisos (520 px) — invisible en una columna alineada a la
   izquierda, y por eso NO hace falta contenedor nuevo.
   390 px: 366 de ancho útil − 44 del ⓘ − 12 de separación = 310 para el botón; el
   rótulo más largo ("Pagar en línea — $30.00") mide 280 ⇒ cabe con 30 px de sobra.

   🔴 EL `gap: 12px` NO ES ESTÉTICA — ES LA ZONA MUERTA ENTRE EL ⓘ Y SU BOTÓN.
   El botón de la barbería RESERVA LA CITA al primer toque; un pulgar que falla el ⓘ
   y pega en el botón no abre una ayuda: **reserva una cita**. Con el blanco táctil
   de 44 px que el ítem 951 le da a `.bsp-help` (inset: 0 bajo `pointer: coarse`),
   los dos objetivos quedan DISJUNTOS por construcción. Si alguien baja este 12,
   los pega. ⚠️ Va en `px` a propósito, como el 44: el dedo no crece con la letra.

   📱 `flex-wrap` es lo que produce el orden del celular sin una media query: los
   dos pares no caben en 366 px, así que el segundo baja solo. El orden es el del DOM
   — en línea primero — y ese orden lo dictó el dueño y lo respaldó Kahlo. */
.bk-paybtns { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }

.bk-paybtn { display: flex; align-items: center; gap: 12px; min-width: 0; }

/* El rótulo ENVUELVE antes que recortarse: con un total de 3 cifras o la letra al
   150 % el texto no cabe en una línea, y partir una frase es mejor que esconderla. */
.bk-paybtn > .btn { min-width: 0; white-space: normal; }

/* ══ ÍTEM 1118 — SET PROPIO DE ÍCONOS GLOSSY 3D, INFRAESTRUCTURA (`UiIcon.razor`)
   Fuente única para el `Chip` (disco) que cualquier ícono del set puede
   necesitar cuando su arte no tiene suficiente contraste sobre una banda de
   color — 1113/1116 lo miden en 1.01:1 sin chip sobre rojo/magenta. Nada de
   esto se consume todavía desde ninguna pantalla real: es infraestructura,
   ver `/dev/ui-icons` (solo Development) para probarlo. ══ */
.ui-icon { display: inline-block; object-fit: contain; vertical-align: middle; }
.ui-icon-fallback, .ui-icon-chip-fallback { display: none; vertical-align: middle; }
.ui-icon-chip {
  display: inline-flex; align-items: center; justify-content: center;
  aspect-ratio: 1; vertical-align: middle;
}
/* El disco: 92% de la altura del CONTENEDOR (`.ui-icon-chip`, cuya altura la
   fija el `style="height:@Size"` del componente) — el 8% restante es aire
   arriba/abajo del propio disco contra su contenedor. Dentro del disco, 15%
   de padding en cada lado ("aire" alrededor del ícono, para que el borde
   circular nunca toque el arte). */
.ui-icon-chip-disc {
  height: 92%; aspect-ratio: 1; border-radius: 50%; background: #fff;
  box-shadow: 0 .1em .3em rgba(0,0,0,.18);
  display: flex; align-items: center; justify-content: center;
  padding: 15%; box-sizing: border-box; overflow: hidden;
}
.ui-icon-chip-disc img, .ui-icon-chip-disc i {
  width: 100%; height: 100%; object-fit: contain; display: block;
}
.ui-icon-chip-disc i.ui-icon-chip-fallback { display: none; font-size: 100%; color: var(--navy, #14224E); }
/* Variante NAVY (Oller, 1118: "el chip blanco arregla los rojos pero no los
   cromos" — algunos íconos del set quedan deslavados sobre blanco). Mismo
   92%/15% de aire, solo cambia el color del disco; el ícono de respaldo
   pasa a blanco para seguir legible sobre navy. */
.ui-icon-chip-disc-navy { background: #14224E; box-shadow: 0 .1em .3em rgba(0,0,0,.35), inset 0 0 0 .07em rgba(95,170,255,.35); }
.ui-icon-chip-disc-navy i.ui-icon-chip-fallback { color: #fff; }

/* Héctor, 2026-09-04, viendo `/dev/ui-icons` CON chip: "se ven mejor sin el
   disco". `drop-shadow` (no `box-shadow`, que solo sombrea una caja
   rectangular) sigue el CONTORNO real del PNG con fondo transparente — una
   sombra oscura suave que separa el ícono del color de fondo sin un disco
   detrás. Modo por defecto para las bandas cuando llegue la luz verde del
   v8 (LV-46) — hoy sigue sin cablearse en ninguna pantalla real. */
.ui-icon-shadow { filter: drop-shadow(0 .1em .2em rgba(0,0,0,.5)); }



/* ══ ÍTEM 1179a (Jack, 2026-09-05) — R-233b: LA CABECERA REPITE EL PNG DE SU TARJETA DEL HOME ══
   Hasta hoy ninguna de las 29 cabeceras `crud-header-icon` lo hacía (22 `<lord-icon>` planos
   animados + 7 `bi-*` a 25 px — inventario de Kahlo, 1179). Las que tienen tarjeta del Home o
   candidato confirmado en `realistic-icons` (14) pasan a `<img class="crud-header-icon crud-header-img">`.
   32 px es el tamaño que Kahlo midió para cabeceras (COMPARATIVA-1179-TAMANOS: el 3D no falla por
   tamaño sino por contraste; con sombra `drop-shadow` — la MISMA receta de `.ui-icon-shadow` —
   sobre blanco/navy se lee). Sin el chip claro de los temas oscuros (`--icon-chip-bg`): ese chip
   existía para los TRAZOS oscuros del lord-icon (R-236); un PNG con sombra no lo necesita
   (Héctor, LV-46: «se ven mejor sin el disco»). Las 15 cabeceras restantes siguen con lord-icon/bi-*
   hasta que exista su PNG (tanda 2 de LISTA-GENERACION-1179). */
.crud-header-img { width: 32px; height: 32px; padding: 0; background: none; border-radius: 0;
    object-fit: contain; filter: drop-shadow(0 .1em .2em rgba(0,0,0,.5)); }

/* ══ LV-75 / ÍTEM 1179a (Jack, 2026-09-06) — ESTADOS DE LA COLA EN LA LISTA DE ESPERA ══
   Mockup C de Kahlo aprobado por Héctor (LV-75): cada estado en un badge CLARO con su
   ícono del set a 20 px. Los colores son los del mockup (g/n/r/y), a propósito PÁLIDOS:
   a 20 px el 3D midió 1.4–2.3:1 sobre navy/rojo sólido (COMPARATIVA-1179-TAMANOS) —
   sobre fondo claro sí se lee. Lo pinta `QueueStateBadge.razor` (un solo sitio, LEY 14);
   mientras los 4 PNG no existan, `.wl-st-ico` es el glifo plano de Bootstrap en el
   mismo hueco. NO usar `.badge` de Bootstrap debajo: su padding/tamaño pelean con el ícono. */
.wl-st { display: inline-flex; align-items: center; gap: .35em; font-size: .75rem; font-weight: 600;
    line-height: 1.2; padding: .2em .55em; border-radius: 6px; white-space: nowrap; vertical-align: middle; }
.wl-st-g { background: #d1e7dd; color: #0f5132; }
.wl-st-n { background: #e2e6f3; color: #14224E; }
.wl-st-r { background: #f8d7da; color: #842029; }
.wl-st-y { background: #fff3cd; color: #664d03; }
.wl-st-ico { line-height: 1; vertical-align: middle; }
