﻿html, body {
    font-family: '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;
}

#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;
}

.bsp-reconnect-pole {
    width: 72px;
    height: 72px;
}

.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;
}

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;
}

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);
}
.crud-price { color: #B8860B; font-weight: 700; }

/* == 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; }

/* == Í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; }
.svc-card-photo { width: 104px; height: 78px; object-fit: cover;
    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; cursor: not-allowed; }
/* Í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 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; }
}

/* 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); }
/* 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».
*/
.kbs-card-pinned { margin-right: 6px; box-shadow: 0 2px 8px rgba(20,34,78,.20), 3px 0 6px -3px rgba(20,34,78,.35); }
.kbs-card-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.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); }
.kbs-card-any-stripe { position: absolute; inset: 0; background: var(--barber-stripe); opacity: .16; }
.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; }
.kbs-stepper button:hover:not(:disabled) { background: rgba(255,255,255,.28); }
.kbs-stepper .n { min-width: 24px; text-align: center; font-weight: 800; font-size: .82rem; color: #fff; }

/* 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,
.kbs-compact .kbs-qty-badge { width: 20px; height: 20px; min-width: 20px; top: 4px; right: 4px; font-size: .66rem; }
.kbs-compact .kbs-stepper { gap: .35rem; 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. */
.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; }

/* ══ Í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; }
.tv-pick { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; }
.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. */
.tv-queue {
    width: 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 {
    font-size: clamp(1.4rem, 2.074vh, 2.8rem);
    font-weight: 700; 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. */
.tv-store-photo { width: 100%; max-height: clamp(130px, 12.037vh, 260px); object-fit: cover;
    object-position: 50% 18%;
    border-radius: 12px; margin-bottom: clamp(.55rem, .815vh, 1.1rem); border: 2px solid rgba(255, 255, 255, .18); }
.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. */
.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: rgba(255,255,255,.06); 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 `Concha Ve.` (nombre
   realista de 10 car.) 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** (`Concha Ve.` 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 `Maximiliano Or.` (15 car., 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. */
    display: flex; align-items: center; gap: clamp(1.249rem, 1.850vh, 2.498rem);
    background: rgba(255,255,255,.06); border-radius: 18.7px;
    padding: clamp(.859rem, 1.272vh, 1.717rem) clamp(1.249rem, 1.850vh, 2.498rem);
}
.tv-queue-row.next { background: #C8102E; box-shadow: 0 0 18px rgba(200,16,46,.55); }
.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). */
    background: rgba(255,255,255,.12); border-radius: 15.6px; padding: .1rem .3rem;
}
.tv-queue-row.next .tv-turn { background: #fff; color: #C8102E; }
/* 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 `Maximiliano Or.` 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 `Maximiliano Or.` (el nombre 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; }
.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; }

/* 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: 1855.98px`, con su cuenta). 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-pole-w: 132px;         /* la caja del poste (la misma en los 4 escalones) */
    --tvc-air: 14px;             /* aire entre el poste y el bloque del nombre */
    --tvc-edge: 24px;            /* mínimo histórico a la orilla: nunca menos */
    --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 PRESTA AL POSTE, QUE YA RESUELVE ESTO EN ESTA
   MISMA PANTALLA: `.pb-tube` lleva navy sobre este mismo fondo y se ve perfecto
   porque tiene RELIEVE (sombras interiores + el barrido de cristal de `::after`).
   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`. */
.tv-turn-call-pole {
    position: absolute; top: 50%; margin-top: -286px;
    width: var(--tvc-pole-w); height: 573px;
    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 img { width: 132px; height: 573px; display: block; }
/* Escalón 3-4 · poste en CSS puro, 0 bytes. Ni el ángulo ni el ancho de banda ni
   la velocidad son a ojo: Kahlo los MIDIÓ sobre el propio poste del socio
   (33,7° ⇒ 146deg · patrón cada 214 px ⇒ 36 px de banda · 9 px por cuadro de
   50 ms ⇒ vuelta en 1,19 s). Y el bucle es EXACTO: el patrón se repite cada
   144 px PERPENDICULARES, así que 144/|cos 146°| = 173,7 px verticales lo
   devuelven a su sitio ⇒ cero salto en la costura, para siempre. */
.tv-turn-call-pole-b { width: 132px; height: 573px; display: flex; flex-direction: column; align-items: center; }
.pb-knob { width: 48px; height: 48px; border-radius: 50%; flex: none;
    background: radial-gradient(circle at 34% 26%, #FFFFFF 0%, #D9E0E8 26%, #939FAE 62%, #39414C 100%); }
.pb-cap { width: 132px; height: 44px; flex: none; border-radius: 8px;
    background: linear-gradient(90deg, #39414C 0%, #8E99A8 16%, #F2F6FA 38%, #FFFFFF 46%, #AEB8C6 64%, #6B7583 82%, #333B45 100%);
    box-shadow: 0 2px 5px rgba(0,0,0,.45); }
.pb-tube { width: 106px; flex: 1 1 auto; position: relative; overflow: hidden; border-radius: 8px;
    background-image: repeating-linear-gradient(146deg, #C8102E 0 36px, #FFFFFF 36px 72px, #14224E 72px 108px, #FFFFFF 108px 144px);
    animation: pb-spin 1.2s linear infinite;
    box-shadow: inset 0 7px 12px -7px rgba(0,0,0,.85), inset 0 -7px 12px -7px rgba(0,0,0,.85); }
@keyframes pb-spin { from { background-position: 0 0; } to { background-position: 0 calc(-144px / 0.829038); } }
/* El cristal: sin este brillo el tubo se ve plano; con él, cilíndrico. */
.pb-tube::after { content: ""; position: absolute; inset: 0; border-radius: 8px;
    background: linear-gradient(90deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,.20) 13%, rgba(255,255,255,0) 30%,
        rgba(255,255,255,.55) 43%, rgba(255,255,255,.10) 56%, rgba(0,0,0,.20) 78%, rgba(0,0,0,.55) 100%);
    box-shadow: inset 0 0 0 3px rgba(255,255,255,.30); }
/* Escalón 4 · el poste QUIETO. Es el mismo poste de CSS con el giro apagado:
   misma caja, misma silueta, 0 bytes y de verdad quieto. */
.tv-turn-call-pole-still .pb-tube { animation: none; }
/* ══ 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**.
   ⚠️ 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.
   👉 SI ALGUIEN CAMBIA `--tvc-w`, `--tvc-pole-w` O `--tvc-air`, ESTE 1855.98 HAY
   QUE REHACERLO. (Y si se olvida, el `max(--tvc-edge, …)` de la fórmula evita el
   desastre: el poste se queda donde estaba toda la vida, nunca fuera del panel.)
   📌 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. 1855,98 deja
   64 px de colchón. El precio de ese colchón es diminuto y está calculado: entre
   1856 y 1920 px de ventana el poste pisa como mucho **10 px** de la caja del
   bloque (156 − (panel−1100)/2, con panel = 0,75 × ventana) — nunca del texto, que
   va centrado — y ahí no hay ningún televisor: eso es una ventana de escritorio. */
@media (max-width: 1855.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 "Maximilian/o De." 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.
   🔴 LOS CORTES SE RECALIBRARON (9/12 en vez de 11/16 de la receta) PORQUE ESTE
   TV NO CORRE INTER: la línea 2 de este archivo fija Helvetica/Arial, y su peso
   900 sale SINTETIZADO — más ancho. Con los cortes de la receta, "Wilfredo Ma."
   medía 1.201,5 px y "Maximiliano De." 1.151,3 px a 132 px: SEGUÍAN PARTIÉNDOSE.
   Los TRES tamaños aprobados no se tocan; solo se mueve dónde salta el escalón.
   📖 La medición completa: TvBoardView.razor → `NameStep`. */
.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 ⇒ "Wilfredo Ma." (12) 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 "Maximiliano De."
   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. */
.tv-queue-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);
    /* 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 ("Maximiliano De.")
       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).
   ⚠️ HONESTO: con el escalón 1-2 (el WebP animado del socio) esto NO se cumple
   del todo — un WebP animado NO se para desde CSS. Para respetarlo de verdad hay
   que bajar al escalón 4 (`Tv:TurnCallArt = still`), y ahí sí se para todo.
   Es un punto real a favor del poste de CSS, y se dice aunque en la pared de una
   barbería casi nadie tenga esa preferencia puesta. */
@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; }
    .pb-tube { animation: none; }
    .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; }

/* ── Í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 (Héctor 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 a Héctor.
   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 (Hector 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. Hector 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 (Hector 2026-08-20): SE QUITA EL CENTRADO. La tecla NO se toca.
   El defecto que Hector 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. */
.bsp-pad { display: grid; grid-template-columns: repeat(3, 81px); gap: 10px; justify-content: start; }
.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;
    top: 0;
    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; }
.sch-storepick { max-width: 175px; height: 34px; }
.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); }
.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; }
.sch-svc-total { background: var(--bs-body-bg, #EEF1F7); font-weight: 600; }
/* 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));
}
.mkt-sect-label {
    font-size: .8rem; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase; white-space: nowrap;
}

/* 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;
}
.mkt-qrbtn img { display: block; width: min(62vw, 230px); 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)
   · .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.) */
body:has(.modal.show, .modal-backdrop, .sch-cap-back, .svrot-scrim, .svccert-scrim, .modal-backdrop-bsp, .modal-backdrop-bcp, #bsp-cropper-overlay[style*="flex"])
    :is(.sidebar-float-wrap, .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 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;
    /* El botón se ve de 18px pero se TOCA en ~28px (recomendación mínima de
       objetivo táctil) sin empujar la etiqueta: el área crece por fuera. */
    outline-offset: 2px;
    position: relative;
}

.bsp-help-btn::after {
    content: "";
    position: absolute;
    inset: -.3rem;
}

/* 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;
}

/* 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;
}

/* 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 Héctor) ══════
   "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 (Héctor 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;
}
