/*
  Las dos herramientas internas: /admin y /panel.

  Vivía duplicado. 135 de las ~240 líneas de estilo de cada archivo eran
  idénticas palabra por palabra, así que cada arreglo había que hacerlo dos
  veces y el segundo se olvidaba. Ahora es un archivo y dos consumidores.

  Claro por omisión, oscuro como variante elegida con el botón. La preferencia
  del sistema no participa: estos paneles se usan en un salón a media tarde.

  Los colores de estado no son el mismo hex en ambos temas. #22c55e sobre
  blanco da 2.3:1 y no se lee; un token que sobrevive intacto al cambio de
  tema suele ser un token que falla el contraste en uno de los dos.
*/

/*
  Las variables de esta consola ahora APUNTAN a Kabelo, no se inventan.

  Antes eran hex sueltos, y el de acción era rosa — exactamente el "cliché
  rosa de belleza" que el paquete prohíbe por escrito. El color de acción de
  Kabelo es el verde azulado profundo; el cobre es acento y va racionado.

  El nombre `--accent` se queda aunque ya no signifique "acento" sino "color
  de acción". Renombrarlo obligaba a tocar más de cuarenta usos repartidos en
  tres archivos para no cambiar ni un píxel; el paquete pide refactor
  incremental, no reescritura. El cobre entra como `--copper`, aparte, para
  que nadie lo use por descuido donde va el verde.

  Requiere `/assets/kabelo.css` cargado ANTES que este archivo: de ahí salen
  todos los `--k-*` y Montserrat.
*/
:root {
  color-scheme: light;

  --bg: var(--k-bg);
  --panel: var(--k-surface);
  --panel-2: #FCFBF9;
  --field: #F4F3F0;
  --card: var(--panel);
  --border: var(--k-border);
  --border-strong: #CFD3D2;
  --text: var(--k-text);
  --text-soft: #445059;
  --muted: var(--k-text-muted);
  --accent: var(--k-brand);
  --accent-soft: #E8EEF0;
  --accent-ring: rgba(24, 74, 90, .3);
  --on-accent: #ffffff;
  --copper: var(--k-accent);
  --copper-soft: #F6EEDF;
  --ok: var(--k-success);
  --warn: var(--k-warning);
  --danger: var(--k-danger);
  --info: var(--k-info);
  --notice-bg: #FBF4E7;
  --notice-text: #6B4E15;
  --reveal-bg: #E9F4EF;

  /*
    La barra lateral es oscura en LOS DOS temas.

    No es un descuido del modo claro: es la firma de Kabelo. En las pantallas
    del paquete el menú es verde azulado profundo con el fondo marfil al lado,
    y ese contraste es lo que hace que la consola se reconozca de un vistazo.
    Por eso vive en variables propias y no en `--panel`, que sí cambia.
  */
  --nav-bg: #0F3543;
  --nav-text: #DCE6E9;
  --nav-muted: #8FA8B0;
  --nav-active-bg: #1B4C5C;
  --nav-active-text: #FFFFFF;
  --nav-border: #1D4757;

  --shadow-sm: var(--k-shadow-sm);
  --shadow: var(--k-shadow-md);
  --shadow-lg: var(--k-shadow-lg);

  /* 14px la tarjeta, 10px el control: los radios del paquete, no los de antes. */
  --radius: var(--k-radius-lg);
  --radius-sm: var(--k-radius-md);
  --sidebar-w: var(--k-sidebar);
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: var(--k-bg);
  --panel: var(--k-surface);
  --panel-2: var(--k-surface-raised);
  /* El campo va por debajo de la superficie que lo contiene: hundido, no
     levantado. Es lo que hace que se lea como un hueco donde escribir. */
  --field: #0F2128;
  --border: var(--k-border);
  --border-strong: #37545D;
  --text: var(--k-text);
  --text-soft: #CBD8DC;
  --muted: var(--k-text-muted);
  /*
    El color de acción en oscuro: el MISMO de marca, como manda la lámina.

    Lo inventé dos veces. Primero un azul claro con tinta oscura, después un
    verde azulado medio. Las dos veces porque el archivo de tokens del paquete
    no redefine la marca en oscuro y leí ese silencio como un hueco que había
    que llenar.

    No era un hueco. La lámina Foundations en modo oscuro trae su propia tabla
    de colores y ahí está escrito: Brand Primary #184A5A, uso "Botón primario".
    El mismo verde en los dos temas, con tinta blanca encima. Lo que cambia con
    el tema son las superficies, no la marca.

    La lección: cuando el sistema de diseño calla, la respuesta no es inventar.
    Es buscar dónde lo dice.
  */
  --accent: #184A5A;
  --accent-soft: #14313D;
  --accent-ring: rgba(24, 74, 90, .55);
  --on-accent: #FFFFFF;
  --copper: var(--k-accent);
  --copper-soft: #2C2416;
  --ok: var(--k-success);
  --warn: var(--k-warning);
  --danger: var(--k-danger);
  --info: var(--k-info);
  --notice-bg: #241E12;
  --notice-text: #EBCF95;
  --reveal-bg: #0E2C22;

  /*
    La barra es la superficie elevada de la lámina, ni más ni menos.

    #19323A es "Surface Raised", el nivel que la tabla reserva para lo que está
    por encima del contenido. Antes la tenía más OSCURA que la página, que es
    leer la pantalla al revés de como está construida.
  */
  --nav-bg: #19323A;
  --nav-text: #D6E2E6;
  --nav-muted: #7E959C;
  --nav-active-bg: #23414A;
  --nav-active-text: #FFFFFF;
  --nav-border: #29434B;

  --shadow-sm: var(--k-shadow-sm);
  --shadow: var(--k-shadow-md);
  --shadow-lg: var(--k-shadow-lg);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* Body de la lámina: 14/22. Estaba en 15/1.6, que es medio píxel más grande
     y un interlineado más suelto — pequeño, pero suficiente para que ninguna
     tarjeta midiera lo que mide en el diseño. */
  font: 14px/22px var(--k-font-sans);
  -webkit-font-smoothing: antialiased;
}

.hidden { display: none !important; }

/* La vista que la barra lateral no está mostrando. Separada de .hidden a
   propósito: los permisos usan .hidden, y si compartieran clase, cambiar de
   vista volvería a mostrar algo que un rol no puede ver. */
/*
  Con `!important`, y por la misma razón que `[hidden]` de abajo.

  Sin él sólo gana por orden de aparición: cualquier regla posterior con la
  misma especificidad que fije `display` deja la sección encendida. Pasó en
  cuanto Inicio dejó de ser una tarjeta y se convirtió en dos retículas —
  `.kpi-grid { display: grid }` está escrita más abajo, así que le ganaba, y
  el tablero no se quitaba nunca: todas las demás pantallas se abrían debajo
  de él.

  No es un caso raro que valga la pena vigilar a mano en cada sección nueva.
  Es una trampa, y se cierra una vez.
*/
.view-off { display: none !important; }

/* Visible sólo para lectores de pantalla: no basta con quitarlo de la vista si
   el objetivo es que un lector lo siga anunciando. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* El navegador oculta [hidden] con `display:none`, pero cualquier regla de
   autor que fije `display` le gana por especificidad. Eso ya escondió un fallo
   real: el selector de forma de pago se mostraba cuando debía estar oculto, y
   parecía roto porque no cambiaba nada. Se declara una vez, para todos. */
[hidden] { display: none !important; }

/* --- Tipografía ---------------------------------------------------------- */

/*
  Los tamaños son los de la lámina Foundations, en píxeles y con su altura de
  línea. En píxeles y no en rem porque la lámina los da así: un título de 32
  con interlineado de 40 es una decisión de diseño, no una proporción que se
  pueda derivar del tamaño base del navegador.

  H1 baja a 24 dentro del panel: la lámina reserva 32 para el título de una
  pantalla completa, y aquí el título convive con una cabecera de 64.
*/
h1 { font: 600 24px/32px var(--k-font-sans); margin: 0; letter-spacing: -.02em; }
h2 { font: 600 20px/28px var(--k-font-sans); margin: 0 0 .25rem; letter-spacing: -.01em; }
h3 { font: 600 16px/24px var(--k-font-sans); margin: 0; }
/* La tarjeta ocupa todo el ancho, pero un párrafo de 1900px no se puede leer:
   el ojo pierde el renglón al volver. Sólo la prosa lleva medida; las
   retículas de tarjetas siguen usando la pantalla completa. */
p.sub { color: var(--muted); margin: 0 0 1.25rem; font-size: .85rem; max-width: 78ch; }

/* --- Controles ----------------------------------------------------------- */

/* Label de la lámina: 13/18, peso medio. */
label {
  display: block; font: 500 13px/18px var(--k-font-sans);
  color: var(--text-soft); margin-bottom: .3rem;
}

input, select, textarea {
  background: var(--field);
  border: 1px solid var(--border-strong);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: .55rem .7rem;
  font: inherit;
  width: 100%;
  /* 44px es la altura de control de Kabelo Y el objetivo táctil mínimo. Un
     barbero toca esto con las manos mojadas entre cliente y cliente. */
  min-height: var(--k-control);
}

/*
  Salvo las casillas y los redondeles.

  El navegador los dibuja de trece píxeles y la altura mínima sólo les infla
  la caja: quedaba un control de 13 dentro de un hueco de 44, y por eso las
  casillas de permisos aparecían hundidas respecto a su rótulo. Su objetivo
  táctil no sale de la caja del control sino de la etiqueta que lo envuelve,
  que es donde se toca.
*/
input[type="checkbox"], input[type="radio"] {
  min-height: 0;
  width: auto;
}

/*
  Y la etiqueta que la envuelve SÍ mide 44.

  Es donde se toca de verdad: la casilla mide trece píxeles y nadie acierta
  trece píxeles con el pulgar. Medida en un iPhone SE, la etiqueta de "Incluir
  desactivados" daba veinte de alto — se podía tocar, pero apenas.

  `:has()` para no tener que marcar a mano cada etiqueta de casilla del panel
  y de /admin, que es como se consigue que la número once nazca sin esto.
*/
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
  min-height: var(--k-control);
  padding-block: .25rem;
  transition: border-color var(--k-fast), box-shadow var(--k-fast);
}

/*
  Una casilla con su texto al lado, en un renglón.

  Estaba resuelto con estilos sueltos repetidos en cada sitio —`display:flex`,
  el hueco, el peso de la letra— y cada copia se desviaba un poco. Aquí una vez,
  con nombre, y la regla de arriba le sigue dando sus 44px de zona tocable.
*/
label.inline {
  display: flex; align-items: center; gap: .45rem;
  font-weight: 400; margin: 0;
  cursor: pointer;
}

/*
  Listas de casillas para elegir varios: artículos, formas de cobro, días.

  Se reparten en columnas según quepan y no en una lista vertical. Un catálogo
  de treinta servicios en una sola columna convierte el cajón en un scroll de
  dos pantallas antes de llegar al botón de guardar.
*/
.pr-combo {
  display: grid; gap: .1rem .9rem;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  max-height: 220px; overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--border); border-radius: var(--k-radius-md);
  padding: .5rem .7rem;
}

/* Los siete días caben en fila; en un teléfono se parten solos. */
.pr-days { display: flex; flex-wrap: wrap; gap: .1rem .9rem; }

input::placeholder, textarea::placeholder { color: var(--muted); opacity: .8; }

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

input:disabled, select:disabled, textarea:disabled { opacity: .55; cursor: not-allowed; }

button {
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0 1rem;
  /* Button de la lámina: 14/20 semibold. */
  font: 600 14px/20px var(--k-font-sans);
  cursor: pointer;
  /*
    40px es la altura Medium de la lámina, que es la que se usa por omisión.
    En un teléfono sube a 44, que es el objetivo táctil mínimo: 40 se puede
    apuntar con un ratón y no con el pulgar de alguien que tiene las manos
    mojadas entre cliente y cliente.
  */
  min-height: var(--k-btn-md);
  transition: filter var(--k-fast), background-color var(--k-fast), border-color var(--k-fast);
}

button:hover:not(:disabled) { filter: brightness(1.07); }
/*
  El aro de foco se dibuja contra el FONDO de la página, no contra el botón:
  `outline-offset` lo saca dos píxeles afuera. Por eso usa `--text` y no
  `--accent` — el aro anterior era del mismo color que el botón que rodeaba,
  así que en el botón principal no se veía. Era invisible cuando era rosa
  sobre rosa y lo seguiría siendo en verde sobre verde.
*/
button:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* El cobre, racionado: acciones premium y momentos de Kabelo AI. Nunca la
   acción principal de una pantalla — para eso está el verde. */
button.accent { background: var(--copper); color: #1F1503; }

button.secondary {
  background: var(--panel);
  color: var(--text);
  border-color: var(--border-strong);
}
button.secondary:hover:not(:disabled) { background: var(--field); filter: none; }

button.ghost { background: transparent; color: var(--muted); border-color: transparent; }
button.ghost:hover:not(:disabled) { background: var(--field); color: var(--text); filter: none; }

button:disabled { opacity: .5; cursor: not-allowed; }

.row { display: flex; gap: .75rem; flex-wrap: wrap; align-items: flex-end; }
.row > div { flex: 1 1 170px; }

/*
  En un teléfono la fila se apila.

  A 320 píxeles los campos ya caían uno por línea de todas formas, así que
  esto no cambia nada... salvo los nueve grupos de botones que llevan
  `flex:0 0 auto` en un atributo `style`. Ese `0` de encogimiento hace que el
  grupo mida lo que midan sus botones juntos y se niegue a ceder: en Clientes,
  "Agregar cliente" y "Registrar sin teléfono" sumaban 361 y sacaban la página
  entera fuera de la pantalla de un iPhone SE.

  Lleva `!important` porque un estilo en línea no se gana de otra forma, y
  cambiarlo en los nueve sitios habría dejado el décimo esperando a que
  alguien lo escribiera mañana.
*/
@media (max-width: 520px) {
  .row { flex-direction: column; align-items: stretch; }
  .row > div { flex: 1 1 auto !important; width: 100%; min-width: 0; }
}

/* --- El armazón: barra lateral y contenido ------------------------------- */

/*
  El armazón mide la ventana exacta, y quien se desplaza es el contenido.

  Antes era `min-height: 100vh` y la página entera se desplazaba. Para que el
  punto de venta no lo hiciera le puse un alto calculado a mano —restando el
  encabezado, la cabecera de página y los rellenos— y ese número mágico se
  equivoca en cuanto algo de arriba mide distinto: un subtítulo que se parte en
  dos líneas, un aviso de créditos, una barra del navegador que aparece. Es
  exactamente el defecto que Héctor reportó tres veces.

  Ahora no hay número que adivinar. El armazón mide `100dvh`, el contenido toma
  lo que sobre con `flex: 1` y se desplaza por dentro. El encabezado se queda
  arriba porque está fuera de la zona que se desplaza, no porque esté pegado.
*/
.shell { display: flex; height: 100dvh; overflow: hidden; }

.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--nav-bg);
  color: var(--nav-text);
  border-right: 1px solid var(--nav-border);
  padding: 1.1rem .75rem;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  /* Ya no se pega a nada: el armazón mide la ventana y la barra lateral es una
     de sus dos columnas, así que ocupa el alto entero sin pedirlo. Lo único
     suyo es que la lista larga se desplace por dentro. */
  overflow-y: auto;
  /* Al llegar al final de la lista, la rueda deja de arrastrar la página que
     hay detrás. */
  overscroll-behavior: contain;
}

/* El logo arriba y grande, el nombre debajo. Es la marca del negocio: quien
   abre su panel tiene que reconocerlo de un vistazo, no leer una fila de
   datos. */
.sidebar-brand {
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  padding: .5rem .5rem 1rem;
  min-width: 0; text-align: center;
}
.sidebar-brand img {
  width: 76px; height: 76px; border-radius: .9rem; object-fit: contain;
  background: #fff; border: 1px solid var(--border); padding: 5px; flex-shrink: 0;
}
.sidebar-brand .mark {
  width: 76px; height: 76px; border-radius: .9rem; flex-shrink: 0;
  /* Cobre y no verde: el verde de acción sobre la barra verde oscura da un
     cuadro que casi no se despega del fondo. Es la inicial del negocio para
     quien todavía no subió logo, y tiene que verse. */
  background: var(--copper); color: #1F1503;
  display: grid; place-items: center; font-weight: 700; font-size: 1.8rem;
}
/* min-width:0 y el truncado: los nombres los escriben los negocios, y uno
   largo sin esto empuja la barra entera. */
.sidebar-brand strong {
  display: block; max-width: 100%; font-size: 1rem; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Los rótulos de grupo son cobre. Es de los pocos lugares donde el acento
   aparece por sí solo, y ahí gana: separan bloques del menú sin necesitar una
   línea ni un espacio extra. */
.nav-label {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--copper); font-weight: 700; padding: .9rem .6rem .3rem;
}

.nav-link {
  display: flex; align-items: center; gap: .6rem;
  padding: .55rem .6rem;
  border-radius: var(--radius-sm);
  color: var(--nav-text);
  text-decoration: none;
  font-size: .875rem;
  font-weight: 500;
  border: 0;
  background: transparent;
  width: 100%;
  text-align: left;
  cursor: pointer;
  min-height: var(--k-control);
}
/*
  `:not(:disabled)` no sobra: iguala la especificidad de la regla genérica.

  `button:hover:not(:disabled)` puntúa (0,2,1) y `.nav-link:hover` sólo (0,2,0),
  así que el `filter: none` de aquí NUNCA se aplicaba y el brillo de los botones
  se colaba en el menú. Y un `filter` no es sólo cosmético: crea contexto de
  contención, y eso vuelve el `position: fixed` del tooltip relativo AL PROPIO
  BOTÓN. De ahí las dos cosas que Héctor reportó juntas — la etiqueta recortada
  a 34px, ilegible, y una barra de desplazamiento horizontal de 69px dentro del
  menú.

  Dos reglas que se contradicen y ganó la que no debía. Medido en navegador, no
  deducido: `getComputedStyle(link, '::after')` es lo único que lo dice.
*/
.nav-link:hover:not(:disabled) {
  background: var(--nav-active-bg);
  color: var(--nav-active-text);
  filter: none;
}
.nav-link.on {
  background: var(--nav-active-bg); color: var(--nav-active-text); font-weight: 600;
}
/*
  El foco dentro de la barra se dibuja contra el fondo de la barra, no contra
  el de la página, así que `--text` no sirve: es casi negro en tema claro.

  Usa `currentColor` y no `--nav-text` porque la barra puede estar teñida con
  el color del negocio, y entonces la tinta la calcula el panel por contraste.
  Un aro fijo se volvería invisible en cuanto alguien eligiera un color claro.
*/
.nav-link:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
/*
  Los iconos del menú son cobre, no del color del texto.

  Es la marca puesta donde más se mira, y era lo que le faltaba a la barra para
  parecerse a la del diseño: con el icono del mismo blanco que la palabra, los
  dos pesan igual y la lista se lee como un bloque gris. El cobre los separa
  sin gritar.

  El activo sube a cobre pleno; el resto va apagado, para que el activo siga
  siendo el que salta a la vista.
*/
/*
  Los iconos son cobre SIEMPRE, y se ponen blancos al pasar por encima.

  La primera versión los devolvía a la tinta calculada cuando el negocio teñía
  su barra, para no arriesgar el contraste. El efecto fue que el único negocio
  con color —el que estaba mirando la pantalla— seguía viendo los iconos
  blancos. La regla se cayó por el caso que quería proteger.

  El cobre es de la marca de la plataforma y no del negocio: la barra la tiñe
  quien paga, los iconos son de Kabelo. El blanco al pasar encima da la
  respuesta al toque sin cambiar de idea sobre eso.
*/
.nav-link svg {
  width: 18px; height: 18px; flex-shrink: 0;
  color: var(--k-accent);
  transition: color var(--k-fast);
}
.nav-link:hover svg,
.nav-link.on svg,
.nav-link:focus-visible svg { color: #FFFFFF; }
.nav-link .count {
  margin-left: auto; font-size: .7rem; font-weight: 700;
  background: var(--warn); color: #fff; border-radius: 999px; padding: .05rem .4rem;
}

/* El correo vive aquí, pegado a Salir: es donde importa saber con qué cuenta
   estás antes de cerrar sesión, y arriba sólo competía con el nombre. */
.sidebar-foot {
  margin-top: auto; padding-top: .75rem;
  display: grid; gap: .45rem;
}
.sidebar-foot .whoami {
  font-size: .72rem; color: var(--nav-muted); padding: 0 .25rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/*
  Los botones de la barra se visten con la paleta de la barra. `.secondary` por
  sí solo es una pastilla blanca, y sobre el verde oscuro pesa más que
  cualquier cosa del menú — Salir terminaba siendo lo más llamativo de la
  pantalla.

  Va en `:not([data-tinted])` a propósito. Cuando el negocio eligió color, esa
  barra ya tiene sus propias reglas más abajo, y si esto se colara encima el
  botón volvería al verde de Kabelo sobre un fondo que el negocio pintó de
  otro color.
*/
.sidebar-foot button { width: 100%; }
.sidebar:not([data-tinted]) .sidebar-foot button.secondary {
  background: var(--nav-active-bg);
  color: var(--nav-text);
  border-color: var(--nav-border);
}
.sidebar:not([data-tinted]) .sidebar-foot button.secondary:hover:not(:disabled) {
  background: var(--nav-bg); color: var(--nav-active-text);
}

/* --- Barra lateral colapsada ---------------------------------------------- */

/* 72 y no 78: es la medida plegada que declara el paquete, y estaba escrita a
   mano seis píxeles más ancha. */
.shell.rail { --sidebar-w: var(--k-sidebar-collapsed); }
.shell.rail .sidebar { padding-left: .5rem; padding-right: .5rem; }
.shell.rail .sidebar-brand img,
.shell.rail .sidebar-brand .mark { width: 46px; height: 46px; font-size: 1.1rem; }
.shell.rail .sidebar-brand strong,
.shell.rail .nav-label,
.shell.rail .nav-text,
.shell.rail .sidebar-foot .whoami { display: none; }
.shell.rail .nav-link { justify-content: center; padding-left: 0; padding-right: 0; }
.shell.rail .nav-link .count {
  position: absolute; top: .2rem; right: .55rem; margin-left: 0;
}
.shell.rail .sidebar-foot button { font-size: .75rem; padding-left: 0; padding-right: 0; }

/*
  La etiqueta al pasar el ratón. Sin ella, una barra de sólo iconos obliga a
  abrirla para recordar cuál era cuál — y entonces colapsarla no sirve.

  Va en `fixed`, no en `absolute`, y esa es la parte que importa: la barra
  tiene `overflow-y: auto`, y en cuanto un eje deja de ser `visible` el otro
  también se vuelve desplazable. Con posición absoluta la etiqueta quedaba
  recortada por la propia barra y, peor, aparecía una barra de desplazamiento
  horizontal dentro del menú. En `fixed` la etiqueta sale del recorte y no
  cuenta como contenido desbordado.
*/
.nav-link { position: relative; }
.shell.rail .nav-link::after {
  content: attr(data-label);
  position: fixed;
  left: calc(var(--sidebar-w) + .4rem);
  margin-top: -1.75rem;
  background: var(--text); color: var(--panel);
  padding: .35rem .6rem; border-radius: var(--radius-sm);
  font-size: .78rem; font-weight: 500; white-space: nowrap;
  box-shadow: var(--shadow);
  opacity: 0; pointer-events: none; z-index: 60;
  transition: opacity .12s ease;
}
.shell.rail .nav-link:hover::after,
.shell.rail .nav-link:focus-visible::after { opacity: 1; }

/* --- La barra con los colores del negocio --------------------------------- */

/*
  El negocio elige su color y la barra lo toma. El color del texto no se
  elige: se calcula contra el fondo, porque un rosa claro y un azul marino
  necesitan tintas opuestas y dejar una fija hace ilegible la mitad de los
  casos. `--rail-ink` lo fija el panel al cargar la marca.
*/
.sidebar[data-tinted] {
  background: var(--rail-bg);
  border-right-color: var(--rail-line);
}
.sidebar[data-tinted] .sidebar-brand .mark {
  background: var(--rail-mark-bg);
  color: var(--rail-mark-ink);
}
.sidebar[data-tinted] .sidebar-brand strong,
.sidebar[data-tinted] .nav-link { color: var(--rail-ink); }
/*
  Y el icono y el rotulillo de cada bloque, que se quedaban fuera.

  Era el bug: `.nav-link svg` fija el cobre de la plataforma, y la regla de
  arriba sólo alcanza al TEXTO del enlace — un `svg` con su propio `color` no
  hereda. Así que al teñir la barra el texto se adaptaba y el icono no. Con un
  color de marca claro, once iconos dorados desaparecían.

  `--rail-icon` lo calcula el panel midiendo: deja el cobre donde contrasta y lo
  cambia por la tinta legible donde no. Ver `tintSidebar`.
*/
.sidebar[data-tinted] .nav-link svg { color: var(--rail-icon); }
.sidebar[data-tinted] .nav-label { color: var(--rail-icon); }
/*
  Y al pasar por encima, la tinta entera.

  Estaba en `#FFFFFF` fijo, que sobre una barra clara es el peor momento para
  desaparecer: justo cuando alguien señala lo que va a pulsar.
*/
.sidebar[data-tinted] .nav-link:hover svg,
.sidebar[data-tinted] .nav-link.on svg,
.sidebar[data-tinted] .nav-link:focus-visible svg { color: var(--rail-ink); }
.sidebar[data-tinted] .nav-label,
.sidebar[data-tinted] .sidebar-foot .whoami { color: var(--rail-ink-soft); }
.sidebar[data-tinted] .nav-link:hover { background: var(--rail-hover); color: var(--rail-ink); }
.sidebar[data-tinted] .nav-link.on {
  background: var(--rail-active); color: var(--rail-ink); font-weight: 600;
}
.sidebar[data-tinted] .sidebar-foot button {
  background: var(--rail-hover); color: var(--rail-ink); border-color: var(--rail-line);
}

.main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }

/*
  El encabezado de 64px del sistema.

  Antes era una franja con el título de la vista y el botón de tema. El título
  se fue al contenido, donde encabeza lo que encabeza, y en su lugar quedan las
  tres cosas que sirven desde cualquier pantalla: buscar, cobrar, y tu cuenta.

  La altura es fija y no depende del relleno: con relleno, un control de 44px
  adentro empujaba la barra a 62 u 68 según el navegador, y la barra lateral,
  que sí mide, dejaba de alinear con ella.
*/
.topbar {
  display: flex; align-items: center; gap: .75rem;
  height: var(--k-header);
  padding: 0 1.25rem;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  /* Sin `sticky`: está fuera de la zona que se desplaza, así que no tiene de
     qué despegarse. Un `sticky` aquí sólo añadía un contexto de apilamiento. */
  flex-shrink: 0;
  z-index: 20;
}
/*
  El hueco lo hace ESTO, no el buscador.

  Antes el empujón venía de un `margin-right: auto` en el buscador, y el
  buscador se esconde entero cuando la cuenta no ve clientes ni catálogo. Sin
  él, los controles se amontonaban contra el menú hamburguesa —a un tercio de
  la pantalla— y el menú de cuenta, que se ancla a su derecha, se salía por el
  borde izquierdo. En un iPhone quedaba el correo cortado por el canto de la
  pantalla. Lo reportó un tester y se reproduce con cualquier cuenta sin punto
  de venta.

  El hueco pertenece a las acciones, que son las que quieren estar a la
  derecha. Que el buscador exista o no deja de importar.
*/
.topbar-actions { display: flex; align-items: center; gap: .5rem; flex-shrink: 0; margin-left: auto; }
/* /admin todavía tiene su título dentro del encabezado y se apoya en esto para
   empujar sus acciones a la derecha. El panel ya no lo usa: allí el título se
   mudó al contenido y el hueco lo hace el buscador. */
.topbar .grow { flex: 1; min-width: 0; }

/* --- El buscador del encabezado ------------------------------------------ */

.topbar-search {
  position: relative;
  flex: 1 1 auto;
  max-width: 420px;
  display: flex; align-items: center;
}
.topbar-search > svg {
  position: absolute; left: .7rem; width: 17px; height: 17px;
  color: var(--muted); pointer-events: none;
}
.topbar-search input {
  padding-left: 2.2rem;
  background: var(--field);
  border-color: transparent;
}
.topbar-search input:focus { border-color: var(--accent); background: var(--panel); }
/* La cruz que Chrome mete en los `search` cae encima del texto y no se puede
   estilar; el buscador ya se limpia con Escape y al elegir un resultado. */
.topbar-search input::-webkit-search-cancel-button { display: none; }

.search-results {
  position: absolute; top: calc(100% + .35rem); left: 0; right: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: .3rem;
  z-index: 40;
  max-height: 60vh; overflow-y: auto;
}
.search-hit {
  display: flex; flex-direction: column; align-items: flex-start; gap: .1rem;
  width: 100%; text-align: left;
  background: transparent; border: 0; color: var(--text);
  padding: .45rem .6rem; border-radius: var(--radius-sm);
}
.search-hit:hover:not(:disabled) { background: var(--field); filter: none; }
.hit-name { font-weight: 600; font-size: .875rem; }
.hit-meta { font-size: .72rem; color: var(--muted); font-weight: 400; }
.search-empty { margin: 0; padding: .6rem; font-size: .82rem; color: var(--muted); }

/* --- La acción principal y la cuenta -------------------------------------- */

.topbar-cta { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; }
.topbar-cta svg { width: 17px; height: 17px; }

.account { position: relative; }
.account-btn {
  display: inline-flex; align-items: center; gap: .25rem;
  background: transparent; border: 1px solid var(--border); color: var(--muted);
  padding: .2rem .35rem .2rem .2rem;
  border-radius: var(--radius-pill, 999px);
}
.account-btn:hover:not(:disabled) { background: var(--field); filter: none; }
.account-btn svg { width: 15px; height: 15px; }
.avatar {
  width: 30px; height: 30px; border-radius: 999px; flex-shrink: 0;
  background: var(--accent); color: var(--on-accent);
  display: grid; place-items: center; font-weight: 700; font-size: .82rem;
}
/*
  Y un cinturón, además del tirante.

  Que las acciones vivan a la derecha ya evita el desbordamiento, pero este
  menú crece con el correo que tenga adentro y basta una cuenta con un correo
  largo, o una pantalla más angosta, para que vuelva a salirse. El tope lo
  amarra a la ventana pase lo que pase; el correo ya se corta solo con puntos
  suspensivos.
*/
.account-menu {
  position: absolute; top: calc(100% + .4rem); right: 0;
  min-width: 210px;
  max-width: calc(100vw - 1.5rem);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: .5rem;
  z-index: 40;
  display: grid; gap: .4rem;
}
.account-who { display: grid; gap: .1rem; padding: .3rem .4rem .5rem; border-bottom: 1px solid var(--border); }
.account-who strong { font-size: .85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-who span { font-size: .72rem; color: var(--muted); }
.account-menu button { width: 100%; justify-content: center; }

/* --- La cabecera de página ------------------------------------------------ */

.page-head {
  margin-bottom: 1.25rem;
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
}
.page-head-text { min-width: 0; }
/* La acción no se encoge ni se parte en dos renglones; si no cabe al lado del
   título, baja entera. */
#view-action { flex: 0 0 auto; }
#view-action:empty { display: none; }
/* El título de pantalla ES el H1 de la lámina: 32/40. Lo tenía en 1.6rem
   —25.6px— que era mi aproximación a ojo de un número que estaba escrito. */
.page-head h1 { font: 600 32px/40px var(--k-font-sans); letter-spacing: -.02em; }
.page-head p { margin: .3rem 0 0; color: var(--muted); font-size: .9rem; max-width: 78ch; }
/* Una vista sin línea de apoyo no debe dejar un hueco donde iría. */
.page-head p:empty { display: none; }

@media (max-width: 720px) {
  /* En un teléfono no caben las tres cosas. Se queda el ícono de cobrar, que
     es la acción, y el buscador cede el ancho que le sobra. */
  .topbar-cta span { display: none; }
  .topbar-cta { padding-left: .7rem; padding-right: .7rem; }
  /* H2 de la lámina en pantalla angosta: 32 se come tres renglones con un
     nombre de pantalla largo. */
  .page-head h1 { font: 600 24px/32px var(--k-font-sans); }
}

/* Sin tope de ancho: en un monitor grande el panel usa la pantalla completa.
   Las tarjetas ya se reparten solas con la retícula, así que el espacio de más
   se convierte en más columnas, no en líneas de texto imposibles de seguir. */
/* `min-height: 0` es lo que permite que un hijo de flex se encoja por debajo
   de su contenido. Sin él, `overflow` no tiene nada que recortar. */
/*
  El contenido es una columna flexible.

  Era un bloque, y por eso una pantalla que quisiera llegar al fondo no tenía
  contra qué medirse: `height: 100%` dentro de un contenedor con relleno se
  pasa por abajo justo ese relleno, y `flex: 1` sin contenedor flexible no
  hace nada. Así, la agenda se estira hasta el fondo y las demás pantallas
  siguen midiendo lo que miden.
*/
.content {
  padding: 1.5rem; width: 100%;
  flex: 1; min-height: 0; overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
}
/* Los hijos no se encogen por debajo de su contenido salvo que se les diga:
   una tabla larga tiene que empujar el desplazamiento, no aplastarse. */
.content > * { flex: 0 0 auto; }

/* El botón que abre la barra en pantalla angosta. Oculto donde la barra ya
   se ve, porque un control que no hace nada visible se lee como roto. */
.nav-toggle { display: none; }

@media (max-width: 900px) {
  .sidebar {
    /*
      Fija a la ventana, y su propio desplazamiento adentro.

      `height: 100vh` de la regla base la ata al alto de la PÁGINA, no al de la
      ventana, así que con contenido largo el menú se iba hacia arriba al
      bajar: quedaba la mitad de la lista fuera de la vista y había que subir
      del todo para volver a Inicio. `100dvh` mide lo que de verdad se ve, y la
      barra del navegador entrando y saliendo no la descuadra.
    */
    position: fixed; inset: 0 auto 0 0; z-index: 40;
    height: 100dvh; max-height: 100dvh;
    overscroll-behavior: contain;
    transform: translateX(-100%);
    transition: transform .22s ease;
    box-shadow: var(--shadow-lg);
  }
  .sidebar.open { transform: none; }
  .nav-toggle { display: inline-flex; }
  .content { padding: 1rem; }
  /* Sólo los costados: la altura de 64px la fija el encabezado y meterle
     relleno vertical aquí aplastaría los controles dentro de esa altura. */
  .topbar { padding: 0 1rem; }
  /* Toca fuera y se cierra: en un teléfono no hay dónde poner una X que no
     estorbe, y buscarla con el pulgar es peor que tocar el resto. */
  .scrim {
    position: fixed; inset: 0; z-index: 30;
    background: rgba(2, 6, 23, .45);
  }
}

@media (min-width: 901px) { .scrim { display: none; } }

/* --- Tarjetas ------------------------------------------------------------ */

.card, section.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.15rem;
  margin-bottom: 1.15rem;
  box-shadow: var(--shadow-sm);
}

.card > h2 { color: var(--text); }
.card-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; margin-bottom: 1rem;
}
.card-head h2 { margin: 0; }
.card-head p.sub { margin: .15rem 0 0; }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: .75rem; }
.stat {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .9rem;
  font-size: .78rem;
  color: var(--muted);
}
.stat b { display: block; font-size: 1.75rem; margin: .1rem 0 .15rem; color: var(--text); letter-spacing: -.02em; }
.stat.low b { color: var(--warn); }
.stat.empty b { color: var(--danger); }

/* --- Inicio: las tarjetas de cabecera ------------------------------------- */

/*
  `auto-fit` con mínimo de 220px y no un número fijo de columnas: la retícula
  no sabe cuántas tarjetas va a recibir, porque cada una depende de un permiso.
  Quien sólo cobra ve tres y quien es dueño ve cuatro, y las dos veces quedan
  repartidas en todo el ancho.
*/
.kpi-grid {
  display: grid; gap: .85rem; margin-bottom: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.kpi {
  position: relative;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 1rem 1.1rem 1.1rem;
  display: grid; gap: .1rem;
  /* `start` y no el estirado por omisión: la retícula iguala la altura de las
     cuatro tarjetas, y sin esto el sobrante se repartía entre las filas de
     cada una. El resultado era que la etiqueta de una tarjeta quedaba siete
     píxeles más arriba que la de al lado. El hueco va abajo, no en medio. */
  align-content: start;
  overflow: hidden;
}
.kpi-head { display: flex; align-items: center; gap: .55rem; margin-bottom: .45rem; }
.kpi-icon {
  width: 34px; height: 34px; border-radius: 999px; flex-shrink: 0;
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent);
}
.kpi-icon svg { width: 17px; height: 17px; }
.kpi-label {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); font-weight: 600;
}
.kpi-value { font-size: 1.85rem; line-height: 1.1; letter-spacing: -.02em; }
.kpi-note { font-size: .78rem; color: var(--muted); }

/* La flecha nunca va sola: el porcentaje la acompaña siempre y la palabra
   "vs ayer" dice contra qué, porque un color y un triángulo no son un dato. */
.delta { font-weight: 700; }
.delta.up { color: var(--ok); }
.delta.down { color: var(--danger); }

/*
  La curva ocupa su propia franja al final de la tarjeta.

  El primer intento la puso en posición absoluta al fondo, para no cambiar la
  altura. Se cruzaba por debajo de "61 ventas" y el texto quedaba tachado por
  una línea. Ahora es una fila más de la retícula: la tarjeta que la lleva
  crece, y las otras crecen con ella porque la retícula las estira a la más
  alta. Espacio en blanco antes que texto atravesado.
*/
.kpi-spark {
  width: 100%; height: 32px;
  margin-top: .6rem;
  color: var(--accent);
  opacity: .55;
  overflow: visible;
}

/* La tarjeta de saldo se tiñe SÓLO cuando urge: un tablero con algo siempre en
   ámbar deja de avisar de nada. */
.kpi.low .kpi-value { color: var(--warn); }
.kpi.low .kpi-icon { background: var(--notice-bg); color: var(--warn); }
.kpi.empty .kpi-value { color: var(--danger); }
.kpi.empty .kpi-icon { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); }

/* --- Inicio: los dos paneles de abajo ------------------------------------- */

.dash-grid { display: grid; gap: 1rem; grid-template-columns: 1.4fr 1fr; align-items: start; }
@media (max-width: 900px) { .dash-grid { grid-template-columns: 1fr; } }

.top-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .7rem; }
.top-list li {
  display: grid; align-items: center; gap: .25rem .75rem;
  grid-template-columns: 1fr auto;
  grid-template-areas: 'nombre cash' 'barra num';
}
.top-name { grid-area: nombre; font-weight: 600; font-size: .875rem; }
.top-cash { grid-area: cash; font-weight: 700; font-size: .875rem; }
.top-num { grid-area: num; font-size: .72rem; color: var(--muted); white-space: nowrap; }
.top-bar {
  grid-area: barra; height: 6px; border-radius: 999px;
  background: var(--field); overflow: hidden;
}
.top-bar span { display: block; height: 100%; background: var(--accent); border-radius: 999px; }

/* --- Para empezar a trabajar -------------------------------------------------
   La lista de lo que le falta a un negocio nuevo. Vive arriba de Inicio y se va
   sola cuando no queda nada. */

.arranque {
  /* Con el color de la marca al canto, no de fondo. Es una GUÍA, no un aviso:
     un recuadro entero de color pone urgencia donde no la hay, y el negocio
     lleva su primer minuto aquí. */
  border-left: 3px solid var(--accent);
}
.arranque-pasos {
  list-style: none;
  margin: .6rem 0 0;
  padding: 0;
  display: grid;
  gap: .1rem;
}
.arranque-pasos li {
  display: grid;
  /* Marca, texto, botón. La marca mide fijo para que los títulos queden
     alineados entre sí, tachados o no: con `auto` cada renglón empezaría en un
     sitio distinto y la lista se vería desordenada por dentro. */
  grid-template-columns: 22px 1fr auto;
  align-items: center;
  gap: .1rem .6rem;
  padding: .55rem 0;
  border-bottom: 1px solid var(--border);
}
.arranque-pasos li:last-child { border-bottom: 0; }
.arranque-pasos strong { display: block; font-size: .9rem; }
.arranque-pasos span:not(.arranque-marca) {
  display: block;
  font-size: .8rem;
  color: var(--muted);
}
.arranque-marca {
  display: grid;
  place-items: center;
  width: 20px; height: 20px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: .7rem; font-weight: 700;
}
/*
  Los hechos se quedan a la vista, tachados. No se borran: una lista que encoge
  sin explicar por qué se lee como que algo se perdió, no como que se avanzó.

  Y se apagan — no desaparecen, pero dejan de pedir atención, que es lo que
  hace un pendiente resuelto.
*/
.arranque-pasos li.listo strong { color: var(--muted); text-decoration: line-through; }
.arranque-pasos li.listo .arranque-marca {
  background: var(--ok);
  border-color: transparent;
  color: #fff;
}

@media (max-width: 520px) {
  /* En un teléfono el botón baja a su propio renglón: apretado en la misma
     línea deja el título en dos renglones y el botón en 30px de ancho. */
  .arranque-pasos li { grid-template-columns: 22px 1fr; }
  .arranque-pasos li button { grid-column: 2; justify-self: start; margin-top: .3rem; }
}

.quick-actions { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
.quick {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .4rem; min-height: 84px; padding: .8rem .5rem;
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: .8rem; font-weight: 600; text-align: center;
}
.quick:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--accent); filter: none; }
.quick svg { width: 22px; height: 22px; color: var(--accent); }

/* --- Tablas -------------------------------------------------------------- */

table { width: 100%; border-collapse: collapse; font-size: .875rem; }
th, td { text-align: left; padding: .6rem .5rem; border-bottom: 1px solid var(--border); }
th {
  color: var(--muted); font-weight: 600; font-size: .72rem;
  text-transform: uppercase; letter-spacing: .04em;
}
tbody tr:last-child td { border-bottom: 0; }
/*
  El desplazamiento lateral sólo donde hace falta, y por una razón concreta.

  `overflow-x: auto` obliga al navegador a calcular `overflow-y: auto` también,
  y eso convierte la envoltura en un contenedor de desplazamiento. Una celda
  con `position: sticky` adentro se pega a ESE contenedor, no a la ventana — y
  como la envoltura crece con su contenido, nunca hay nada contra qué pegarse.
  El encabezado fijo era decoración: medido, se iba con la página igual que
  antes.

  Debajo de 720px la tabla ya es una lista de tarjetas y no necesita
  desplazarse. De 1024 para arriba el ancho sobra —la tabla más ancha de la
  consola mide 623px— así que la envoltura deja de ser contenedor y el
  encabezado se pega de verdad. Queda la franja de en medio, la tableta, donde
  sí puede faltar ancho y el desplazamiento se gana el sitio.
*/
.table-wrap { overflow-x: auto; }

@media (min-width: 1024px) {
  .table-wrap { overflow-x: visible; }

  /* El encabezado se queda arriba al recorrer una lista larga. Sin esto, a la
     fila veinte ya no se sabe qué columna es cuál. */
  .table-wrap thead th {
    /* `top: 0` y no el alto del encabezado: ahora quien se desplaza es
       `.content`, y su borde de arriba ya empieza DEBAJO del encabezado.
       Descontarlo otra vez dejaba la cabecera flotando 64px más abajo. */
    position: sticky; top: 0; z-index: 1;
    background: var(--panel);
    /* `border-bottom` no acompaña a una celda pegada en todos los navegadores;
       la sombra sí, y separa mejor de la fila que pasa por debajo. */
    box-shadow: inset 0 -1px 0 var(--border);
  }

  /* Sin desplazamiento lateral, un correo largo sin espacios sería lo único
     capaz de empujar la tabla fuera de la tarjeta. */
  .table-wrap td { overflow-wrap: anywhere; }
}

/*
  En un teléfono, una tabla deja de ser una tabla.

  El catálogo mide 623 píxeles de ancho dentro de una pantalla de 319. El
  desplazamiento horizontal existía, pero nadie lo usa: Estado, Editar y
  Desactivar quedaban fuera de la vista y quien abría el panel desde el
  mostrador no sabía siquiera que esos botones estaban ahí.

  Cada fila pasa a ser una tarjeta y cada celda dice su columna. El texto de la
  columna no está escrito dos veces en ninguna parte: lo copia del <thead> el
  observador de panel.html, así que una tabla nueva lo hereda sin que nadie se
  acuerde de nada.
*/
@media (max-width: 720px) {
  .table-wrap { overflow-x: visible; }

  /* Oculto a la vista pero presente para quien lee la página con un lector, y
     —lo que importa aquí— presente para el observador que copia sus textos. */
  .table-wrap thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }

  .table-wrap tbody tr {
    display: block;
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .3rem .75rem;
    margin-bottom: .6rem;
  }
  .table-wrap tbody td {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 1rem;
    padding: .4rem 0;
    border-bottom: 1px solid var(--border);
    text-align: right;
  }
  .table-wrap tbody tr td:last-child { border-bottom: 0; }

  .table-wrap tbody td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    color: var(--muted); font-weight: 600;
    font-size: .7rem; text-transform: uppercase; letter-spacing: .04em;
    text-align: left;
  }
  /* La columna de acciones no tiene título, así que sus botones ocupan la
     fila entera en vez de apretarse contra un rótulo que no existe. */
  .table-wrap tbody td:not([data-label]) {
    display: flex; gap: .5rem; flex-wrap: wrap; justify-content: flex-start;
    text-align: left;
  }
  /* Al ancho de su texto, no estirados: con `flex:1` el botón de una palabra
     quedaba tan ancho como el de tres y el tercero se caía solo a otra fila. */
  .table-wrap tbody td:not([data-label]) button { flex: 0 1 auto; }
}
.mono { font-family: ui-monospace, monospace; }

/* --- Etiquetas de estado ------------------------------------------------- */

/*
  El estado nunca se dice sólo con color.

  Es regla del sistema de diseño y no una preferencia: uno de cada doce
  hombres no distingue el verde del rojo, y una tabla donde "activo" y
  "desactivado" sólo se diferencian por el tono de un borde es ilegible para
  él. La pastilla ya lleva su palabra escrita; el signo la refuerza de un
  vistazo, para quien recorre veinte filas sin leer ninguna.

  El signo va en `::before` y no en el marcado porque son veinticinco lugares
  entre las dos consolas, y meterle un SVG a cada uno para no cambiar ni una
  palabra habría sido veinticinco oportunidades de olvidarse de uno.
*/
.pill {
  display: inline-flex; align-items: center; gap: .28rem;
  font-size: .7rem; font-weight: 600;
  padding: .15rem .5rem; border-radius: 999px;
  border: 1px solid var(--border-strong); color: var(--muted);
}
.pill::before { font-size: .82em; line-height: 1; }
.pill.on { color: var(--ok); border-color: var(--ok); background: var(--reveal-bg); }
.pill.on::before { content: '✓'; }
.pill.off { color: var(--muted); }
.pill.off::before { content: '○'; }
.pill.miss { color: var(--warn); border-color: var(--warn); background: var(--notice-bg); }
.pill.miss::before { content: '!'; font-weight: 800; }

/* --- El punto de venta ---------------------------------------------------- */

/*
  Dos columnas: lo que se vende y la venta que se está armando.

  Antes eran dos cajas de una fila flexible, y la de la derecha se iba abajo
  del todo en cuanto la lista de artículos crecía. Aquí es una retícula con
  medidas: el mostrador se queda donde está mientras se toca el catálogo, que
  es la única manera de ver crecer el total mientras se arma la venta.
*/
.pos-grid {
  display: grid; gap: 1rem;
  grid-template-columns: minmax(0, 1fr) 380px;
  align-items: stretch;
  /* Llena lo que le den. Quién se lo da es la retícula de arriba, que ya sabe
     cuánto sobra: aquí no se calcula nada. */
  flex: 1; min-height: 0;
}

/*
  La pantalla del punto de venta ocupa el contenido entero y no se desplaza.
  Se desplazan sus columnas, que es como funciona un mostrador: el total y el
  botón de cobrar no pueden irse mientras se busca el tercer artículo.
*/
.pos-view { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }

/*
  Sólo cuando el punto de venta está en pantalla, el contenido REPARTE su alto
  en vez de apilar.

  `height: 100%` a secas no servía: medía el contenido entero e ignoraba la
  cabecera de página que va encima, así que el mostrador sobresalía justo lo
  que mide el título. Ese hueco es el que antes tapaba un `calc()` con números
  a mano, y es el que se equivocaba en cuanto el subtítulo se partía en dos
  líneas.

  Con `:has()` la regla se enciende sola: la cabecera ocupa lo suyo, el
  mostrador se queda con lo que sobra, y nadie tiene que saber cuánto es. Las
  demás pantallas siguen en flujo normal —sin flex— para que sus márgenes se
  junten como siempre.
*/
.content:has(> .pos-view:not(.view-off)) {
  display: flex; flex-direction: column;
}
.content:has(> .pos-view:not(.view-off)) > .pos-view {
  flex: 1; min-height: 0;
}
/*
  El catálogo se desplaza por dentro; la página no se mueve.

  Es como funciona un mostrador de verdad: la lista de lo que se vende puede
  ser larga, pero el total y el botón de cobrar no pueden irse de la pantalla
  mientras se busca el tercer artículo. Con la página desplazándose, el
  mostrador quedaba arriba y había que subir para ver cuánto llevaba.

  Y de paso resuelve el hueco: las dos columnas miden lo mismo, así que ya no
  queda media pantalla de fondo debajo del panel de la venta.
*/
.pos-grid > .card { display: flex; flex-direction: column; min-height: 0; }
.pos-grid > .card > .pos-items {
  flex: 1; align-content: start;
  overflow-y: auto; overscroll-behavior: contain;
  /* Un pelo de aire para que la última fila no quede pegada al borde. */
  padding-bottom: .25rem;
}

/*
  El botón de cobrar NO se desplaza. Se desplazan las líneas de la venta.

  Antes se desplazaba la tarjeta entera, así que con seis artículos el total,
  la forma de cobro y el botón de Cobrar se iban por debajo del borde: había
  que desplazar para cobrar, y en un mostrador con gente esperando eso es
  exactamente lo que no puede pasar.

  Ahora la tarjeta reparte: el encabezado y el cliente arriba, el pie —total,
  forma de cobro, cambio, Cobrar— abajo, y en medio la lista, que es lo único
  que crece. Medido a 1440x900 con nueve artículos: el botón de Cobrar sigue
  dentro de la pantalla.
*/
.pos-ticket { display: flex; flex-direction: column; }
.pos-ticket > .card {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  /* Red de seguridad: si el pie por sí solo no cabe —una pantalla muy baja—,
     la lista se queda en cero y se desplaza la tarjeta, que es peor pero deja
     todo alcanzable. Sin esto quedaría recortado y sin manera de llegar. */
  overflow-y: auto; overscroll-behavior: contain;
}

/*
  La lista se queda con lo que sobre y se desplaza ella.

  `min-height: 0` es lo que le permite encogerse por debajo de su contenido:
  sin él un hijo de flex nunca baja de su tamaño natural y el pie sale
  empujado hacia abajo igual que antes.
*/
.pos-ticket > .card > #sale-lines {
  flex: 1; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
}

/*
  Y si aun así no cabe, el botón de Cobrar se queda pegado abajo.

  Hay pantallas donde el ticket entero no entra ni con la lista en cero: una
  laptop de 640 de alto con el navegador ocupando su parte. Ahí la tarjeta se
  desplaza —es la red de seguridad de arriba— y sin esto el botón se iría con
  ella. Pegado, el cajero siempre lo tiene debajo del pulgar, y lo que se
  desplaza por detrás es el detalle, que es lo que se puede consultar.
*/
.pos-ticket > .card > #sale-charge {
  position: sticky; bottom: 0;
  /* El fondo es obligatorio: sin él, las líneas de la venta se verían pasar
     por debajo del texto del botón. */
  z-index: 1;
}
@media (max-width: 1100px) {
  /* En una columna el alto lo pone el contenido: forzarlo dejaría el
     mostrador empujado fuera de la pantalla debajo de un catálogo estirado. */
  /* En una columna manda el contenido: forzar el alto dejaría el mostrador
     empujado fuera de la pantalla debajo de un catálogo estirado. */
  /* En una columna manda el contenido y se desplaza la página, como el resto
     de las pantallas: forzar el alto dejaría el mostrador debajo de un catálogo
     estirado y fuera de la vista. */
  .pos-grid { grid-template-columns: minmax(0, 1fr); flex: 0 0 auto; }
  /* Apiladas, el alto lo pone el contenido y quien se desplaza es la página.
     Una lista con su propio desplazamiento dentro de otra que ya se desplaza
     es una trampa: el dedo nunca sabe cuál va a moverse. */
  .pos-ticket > .card,
  .pos-ticket > .card > #sale-lines { overflow-y: visible; min-height: 0; flex: 0 0 auto; }
  .pos-view { display: block; height: auto; overflow: visible; }
  /* Y deja de repartirse el alto: en una columna manda el contenido. Con
     `flex: 1` el mostrador se quedaba con un alto de ventana y las dos
     columnas apiladas le salían por debajo, encimándose con lo que hubiera. */
  .content:has(> .pos-view:not(.view-off)) > .pos-view { flex: 0 0 auto; }
}

/* Ya no hace falta pegarlo: la página no se desplaza en esta pantalla, se
   desplaza el catálogo por dentro. Un elemento pegado dentro de una retícula
   que ya mide la ventana entera no tiene contra qué pegarse. */
.pos-ticket { min-height: 0; }

.pos-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .6rem 1rem; margin-bottom: .75rem;
  /* Sin esto, en una pantalla de 320 el buscador no cabía junto al filtro y
     empujaba la página tres píxeles fuera. Tres píxeles bastan para que el
     teléfono deje toda la página corriéndose de lado. */
  flex-wrap: wrap;
}
/*
  El buscador se lleva lo que sobre, y las categorías lo que necesiten.

  Era `> div:last-child`, que ahora sería la fila de categorías: apuntar por
  posición se rompe en cuanto se mete algo entre medias, y se rompió. Con nombre
  no se rompe.
*/
.pos-search { flex: 1 1 180px; min-width: 0; max-width: 280px; }

/* Quién atiende: lo justo para un nombre y ni un píxel más. Cede espacio antes
   que el buscador, que es el que salva un catálogo grande. */
.pos-performer { flex: 0 1 190px; min-width: 0; }
.pos-performer select { min-height: var(--k-btn-sm); font-size: 13px; }

@media (pointer: coarse) {
  .pos-performer select { min-height: var(--k-control); }
}

/* Las categorías van después del tipo y antes del buscador, y ceden espacio
   antes que él: sin buscador, un catálogo de cuarenta artículos no se navega. */
.pos-head .chips { flex: 0 1 auto; }
.pos-head h3 {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); font-weight: 700;
}

/* El total y el botón de cobrar son lo que se mira, así que pesan como en el
   diseño y no como un renglón más de la columna. */
/*
  El desglose de la venta: subtotal, descuentos, y el total aparte.

  Los descuentos van en el acento de la marca —el cobre— porque es lo que hace
  la lámina y porque es lo único de este bloque que le interesa mirar a alguien:
  el resto son números que se leen una vez. El acento está racionado en el
  sistema, y éste es de los sitios donde se gana el sitio.
*/
.pos-breakdown { display: grid; gap: .3rem; margin-top: .6rem; }

.pos-breakdown-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem;
  font: 400 14px/22px var(--k-font-sans);
  color: var(--muted);
}
.pos-breakdown-row.menos { color: var(--copper); font-weight: 600; }

.pos-total {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; margin: .9rem 0 .25rem; padding-top: .9rem;
  border-top: 2px solid var(--border);
}
.pos-total strong:first-child {
  font-size: .78rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted);
}
.pos-total strong:last-child { font-size: 1.6rem; letter-spacing: -.02em; }
#sale-charge { min-height: 52px; font-size: 1rem; }

/* El catálogo, en tarjetas y no en botones de una línea. */
.pos-items {
  display: grid; gap: .85rem;
  /* 170 y no 150: en el diseño la tarjeta respira, y con el botón adentro una
     de 150 aprieta el nombre de un servicio de tres palabras. */
  grid-template-columns: repeat(auto-fill, minmax(min(170px, 100%), 1fr));
}

/*
  Las categorías como pastillas de filtro, en la misma fila que el tipo.

  La lámina de componentes no tiene una "pastilla de filtro" aparte: tiene el
  CONTROL SEGMENTADO y los botones SMALL de 32px. Esto es lo segundo con el
  estado seleccionado del primero — botón secundario en reposo, primario cuando
  está elegido— y así no se inventa un componente que el sistema no tiene.

  La primera versión usaba `var(--brand)`, que NO EXISTE en esta hoja: el token
  es `--accent`. Un color inválido deja el fondo transparente, y con el texto en
  blanco la categoría elegida se volvía invisible sobre el panel. Es exactamente
  el error de no mirar el sistema de diseño antes de escribir.

  Se envuelven en varias líneas y no se desplazan de lado: un carrusel
  horizontal esconde las categorías del final, y esconder un filtro es peor que
  ocupar un renglón más.
*/
.chips { display: flex; flex-wrap: wrap; gap: .4rem; }

.chips button {
  /* Secundario de la lámina, en la medida Small (32px). */
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--k-radius-pill);
  padding: 0 .85rem; min-height: var(--k-btn-sm);
  font: 600 13px/18px var(--k-font-sans);
  transition: background var(--k-fast), color var(--k-fast), border-color var(--k-fast);
}
.chips button:hover:not([aria-pressed="true"]) { background: var(--field); }

/* Elegida: primario. Es el mismo par que usa el control segmentado. */
.chips button[aria-pressed="true"] {
  background: var(--accent); color: var(--on-accent); border-color: var(--accent);
}

@media (pointer: coarse) {
  .chips button { min-height: var(--k-control); }
}

.pos-item {
  display: grid; gap: 0; justify-items: center; text-align: center;
  align-content: start;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem .75rem .9rem;
  color: var(--text);
  min-height: 0;
  transition: border-color var(--k-fast), box-shadow var(--k-fast);
}
/* Se levanta, no se pinta: con el fondo teñido, el círculo de color del icono
   se perdía contra él y las tarjetas dejaban de distinguirse entre sí. */
.pos-item:hover:not(:disabled) {
  background: var(--card); border-color: var(--accent);
  box-shadow: var(--shadow); filter: none;
}
.pos-item .ico {
  width: 52px; height: 52px; border-radius: 999px; margin-bottom: .5rem;
  display: grid; place-items: center;
}
/* Dos tonos y no uno: en un catálogo mezclado, distinguir de un vistazo un
   servicio de un producto ahorra un error de cobro. El diseño pinta un
   círculo de color por artículo; el negocio no va a elegir uno para cada
   corte, así que el color lo pone el tipo. */
.pos-item .ico.servicio { background: var(--accent-soft); color: var(--accent); }
.pos-item .ico.producto { background: var(--copper-soft); color: var(--copper); }
.pos-item .ico svg { width: 24px; height: 24px; }
.pos-item .nom {
  font-weight: 600; font-size: .875rem; line-height: 1.3;
  /* Dos renglones y luego puntos: sin tope, un nombre largo estira su tarjeta
     y desalinea la fila entera de la retícula. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.pos-item .dur {
  display: inline-flex; align-items: center; gap: .2rem;
  font-size: .72rem; color: var(--muted); font-weight: 400; margin-top: .15rem;
}
.pos-item .dur svg { width: 12px; height: 12px; color: inherit; }
.pos-item .pre { font-weight: 700; font-size: 1.05rem; margin-top: .3rem; }
/* Las tres medidas de la lámina, por si una pantalla necesita nombrarlas. */
button.sm { min-height: var(--k-btn-sm); padding: 0 .75rem; font-size: 13px; }
button.lg { min-height: var(--k-btn-lg); padding: 0 1.25rem; font-size: 15px; }

@media (max-width: 720px) {
  /* Con el pulgar, 40 no alcanza. */
  button:not(.sm) { min-height: var(--k-control); }
}

/*
  Y con el dedo, 32 tampoco.

  `button.sm` mide 32 porque en un escritorio es un botón secundario que no
  debe pesar. En una pantalla táctil eso es la mitad del objetivo mínimo, y son
  botones de verdad: dividir el pago, quitar un descuento, aplicar el del
  cartel. Se estira sólo donde se toca con el dedo, así que el escritorio se
  queda como estaba.

  `pointer: coarse` y no un ancho: una tableta grande se toca con el dedo, y un
  monitor chico no.
*/
@media (pointer: coarse) {
  button.sm { min-height: var(--k-control); }
}

.pos-item .add {
  display: block; width: 100%; margin-top: .75rem;
  background: var(--accent); color: var(--on-accent);
  border-radius: var(--radius-sm); padding: .5rem;
  font-size: .8rem; font-weight: 600;
}
.pos-item:hover .add { filter: brightness(1.08); }
/* Cuántas veces va en la venta. Sin esto hay que leer la columna de al lado
   para saber si el toque contó. */
.pos-item .cuenta {
  position: absolute; top: .4rem; right: .4rem;
  min-width: 20px; height: 20px; padding: 0 .3rem;
  border-radius: 999px; background: var(--accent); color: var(--on-accent);
  font-size: .68rem; font-weight: 700; display: grid; place-items: center;
}
.pos-item { position: relative; }

/* --- El control segmentado ------------------------------------------------ */

.segmented {
  display: inline-flex; padding: 3px; gap: 2px;
  background: var(--field);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.segmented button {
  background: transparent; color: var(--muted);
  border: 0; border-radius: calc(var(--radius-sm) - 3px);
  padding: 0 .8rem; min-height: 34px;
  font-size: .8rem; font-weight: 600;
}
/* En el teléfono los tres botones se reparten el ancho y suben a la altura
   táctil. En el escritorio se apunta con el ratón y 34 basta; con el pulgar
   no. */
@media (max-width: 720px) {
  .segmented { display: flex; width: 100%; }
  .segmented button { flex: 1 1 0; min-height: var(--k-control); }
}
.segmented button:hover:not(:disabled) { color: var(--text); filter: none; }
.segmented button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }

/* Desplegables y menús: el nivel "Dropdown" de la lámina, que es un escalón
   por debajo del modal. */
.search-results, .account-menu { box-shadow: var(--k-shadow-drop); }

/* --- El cajón lateral ----------------------------------------------------- */

/*
  Modal para decidir, cajón para editar. Es la regla del sistema de diseño y
  tiene su lógica: una confirmación interrumpe y se va, una edición convive con
  lo que estabas mirando y conviene que se abra al lado, no encima.

  Sobre `<dialog>` y no sobre un `div` con posición fija: se lleva el foco, lo
  devuelve al cerrarse, atrapa el tabulador adentro y responde a Escape sin que
  haya que escribir nada de eso. Escribir a mano un atrapador de foco correcto
  es más difícil de lo que parece, y equivocarse deja a quien navega con
  teclado tabulando por detrás de una capa que no puede ver.
*/
.drawer {
  /* `margin` sin `left:auto` no lo pega a la derecha: un dialog modal se
     centra con márgenes automáticos, así que el de la izquierda se queda con
     todo el sobrante. */
  margin: 0 0 0 auto;
  width: min(420px, 100vw);
  max-width: 100vw;
  height: 100dvh; max-height: 100dvh;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  flex-direction: column;
}
.drawer::backdrop { background: rgba(13, 27, 32, .45); }

/*
  `display` SÓLO cuando está abierto.

  El navegador esconde un `<dialog>` cerrado con su propia regla `display:
  none`, y cualquier `display` que escriba el autor se la gana. Puesto en la
  regla base, el cajón se dibujaba dentro de la página —con su formulario
  entero— debajo de las tarjetas que venía a reemplazar.

  Es el mismo error que `view-off` en este mismo archivo. Dos veces en un día:
  un `display` propio siempre le gana al del navegador, y el navegador es el
  que sabe cuándo esconder un diálogo.
*/
.drawer[open] { display: flex; animation: drawer-in var(--k-base) ease-out; }
@keyframes drawer-in { from { transform: translateX(24px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .drawer[open] { animation: none; } }

.drawer-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0 1rem 0 1.15rem;
  min-height: var(--k-header);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.drawer-head h2 { font-size: 1rem; }

/* Sólo el cuerpo se desplaza: el título y el guardar se quedan donde están. */
.drawer-body { flex: 1; overflow-y: auto; padding: 1.15rem; }

.drawer-foot {
  display: flex; gap: .6rem; justify-content: flex-end;
  padding: .85rem 1.15rem;
  border-top: 1px solid var(--border);
  background: var(--panel-2);
  flex-shrink: 0;
}

/* En un teléfono ocupa la pantalla entera: 420px de cajón sobre 390px de
   ventana dejaría el contenido de atrás asomando por una rendija de nada. */
@media (max-width: 520px) {
  .drawer { width: 100vw; border-left: 0; }
}

/* --- Tarjetas de resumen con su acción ------------------------------------ */

/* Columnas iguales: son tarjetas del mismo rango, y una más ancha que otra
   sugiere que una importa más. */
.summary-grid {
  display: grid; gap: 1rem; align-items: start;
  /*
    `min(320px, 100%)` y no `320px` a secas.

    El mínimo de un `minmax` es un mínimo de verdad: en un iPhone SE la
    ventana mide 320 y el contenido, con su relleno, 288 — así que la columna
    se quedaba en 320 y empujaba la página 32 píxeles fuera de la pantalla.
    Con `min()` la columna nunca pide más de lo que hay.
  */
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
}

.summary-card { display: grid; gap: .1rem; }
.summary-card .card-head { margin-bottom: .75rem; }
.summary-rows { display: grid; gap: .1rem; }
.summary-rows > div {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; padding: .5rem 0;
  border-bottom: 1px solid var(--border);
}
.summary-rows > div:last-child { border-bottom: 0; }
.summary-rows dt, .summary-rows .k {
  color: var(--muted); font-size: .78rem; flex: 0 0 auto;
}
.summary-rows .v { font-weight: 600; font-size: .875rem; text-align: right; overflow-wrap: anywhere; }
.summary-rows .v.empty { font-weight: 400; color: var(--muted); }

/* La muestra de color: un cuadro no dice cuál es, y el código sin el cuadro
   tampoco. Van juntos. */
.swatch {
  display: inline-block; width: 16px; height: 16px; border-radius: 4px;
  border: 1px solid var(--border-strong); vertical-align: -3px; margin-right: .35rem;
}

.brand-shot {
  display: flex; align-items: center; gap: .75rem;
  padding: .25rem 0 .75rem;
}
.brand-shot img {
  width: 54px; height: 54px; border-radius: var(--radius-sm); object-fit: contain;
  background: #fff; border: 1px solid var(--border); padding: 4px; flex-shrink: 0;
}
.brand-shot .mark {
  width: 54px; height: 54px; border-radius: var(--radius-sm); flex-shrink: 0;
  background: var(--copper); color: #1F1503;
  display: grid; place-items: center; font-weight: 700; font-size: 1.3rem;
}

/* --- Esqueletos de carga -------------------------------------------------- */

/*
  Lo que se ve mientras llegan los datos.

  No es adorno. El panel hace nueve peticiones al abrirse, y sin esto la
  pantalla está en blanco hasta que vuelve la más lenta: en el wifi de un
  salón a media tarde eso son varios segundos en los que no se sabe si la
  aplicación está cargando o rota. Un bloque gris con la forma de lo que viene
  contesta esa pregunta sin decir nada.

  La forma importa más que la animación: lo que se dibuja tiene el tamaño de
  lo que va a llegar, así que el contenido no salta cuando aparece.
*/
.skeleton {
  display: block;
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg,
    var(--field) 25%,
    color-mix(in srgb, var(--border) 55%, var(--field)) 37%,
    var(--field) 63%);
  background-size: 400% 100%;
  animation: skeleton-sweep 1.4s ease-in-out infinite;
}
@keyframes skeleton-sweep {
  from { background-position: 100% 0; }
  to { background-position: 0 0; }
}

/* Quien pidió menos movimiento no pierde la información: el bloque sigue ahí,
   sólo deja de barrer. */
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
}

.skeleton-line { height: .7rem; margin: .35rem 0; }
.skeleton-line.wide { width: 100%; }
.skeleton-line.half { width: 55%; }
.skeleton-line.short { width: 30%; }
.skeleton-title { height: 1.5rem; width: 45%; margin: .2rem 0 .5rem; }
.skeleton-pill { height: 34px; width: 34px; border-radius: 999px; }

/* --- Estado vacío --------------------------------------------------------- */

/*
  Una lista vacía no es una advertencia.

  Los cuatro vacíos del panel se dibujaban dentro de `.notice`, que es la caja
  ámbar con la barra al costado que usamos para avisar de un problema. Abrir
  Clientes el primer día y encontrarse una alerta de color hace pensar que
  algo salió mal, cuando lo único que pasa es que todavía no has empezado.

  Neutro, centrado, con la acción al lado del texto que la explica.
*/
.empty-state {
  display: grid; justify-items: center; gap: .5rem;
  padding: 2.2rem 1.25rem;
  text-align: center;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel-2);
}
.empty-state svg {
  width: 34px; height: 34px; color: var(--muted); opacity: .75;
  margin-bottom: .15rem;
}
.empty-state strong { font-size: .95rem; }
/* La medida importa: centrado y a todo el ancho de la tarjeta, el ojo pierde
   el renglón al volver. */
.empty-state p { margin: 0; color: var(--muted); font-size: .85rem; max-width: 46ch; }
.empty-state button { margin-top: .4rem; }

/* --- Avisos -------------------------------------------------------------- */

.notice {
  border: 1px solid var(--border);
  border-left: 3px solid var(--warn);
  background: var(--notice-bg);
  padding: .75rem .9rem;
  border-radius: var(--radius-sm);
  font-size: .82rem;
  color: var(--notice-text);
  margin-bottom: 1rem;
}

/* El borde de estado es lo único que cambia: el aviso ya se leyó como aviso,
   el color solo dice si es bueno o si pide atención. */
.notice.ok { border-left-color: var(--ok); }
.notice.warn { border-left-color: var(--warn); }

/* --- La zona de peligro ------------------------------------------------------
   Eliminar un negocio: lo único de esta consola que no se puede deshacer. De
   `tenants` cuelgan cuarenta y tres tablas en cascada. */

.peligro {
  margin-top: 1.5rem;
  /* Separada por arriba con una línea, no metida en una tarjeta de color.
     Un recuadro rojo grande se vuelve paisaje: se ve cada vez que se abre una
     ficha, y a la quinta ya no se lee. La línea dice "esto es otra cosa" sin
     gritarlo, y el rojo se guarda para el botón, que es lo que de verdad
     hace daño. */
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}
.peligro h3 {
  margin: 0 0 .35rem;
  font-size: .9rem;
  color: var(--danger);
}
.peligro p {
  margin: 0;
  max-width: 62ch;
  font-size: .82rem;
  color: var(--muted);
}
.peligro-boton {
  margin-top: .75rem;
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}
/*
  Y no se ilumina al pasar por encima: se OSCURECE.

  Todos los demás botones de la consola se aclaran, que es la señal de "llégame".
  Éste hace lo contrario a propósito — es la única pista, antes de pulsarlo, de
  que no se parece a los otros.
*/
.peligro-boton:hover:not(:disabled) {
  filter: brightness(.88);
  background: var(--danger);
  border-color: var(--danger);
}

/* --- Elegir cómo pagar ---------------------------------------------------- */

.mode-switch {
  display: inline-flex;
  gap: .25rem;
  padding: .25rem;
  margin-bottom: .5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--field);
  flex-wrap: wrap;
}

.mode-switch button {
  background: none;
  border: 0;
  color: var(--muted);
  font: inherit;
  font-size: .82rem;
  font-weight: 600;
  padding: .45rem .8rem;
  border-radius: calc(var(--radius-sm) - 2px);
  cursor: pointer;
  /* 44px de alto real: en el celular esto se toca con el pulgar. */
  min-height: 44px;
}

.mode-switch button:hover { color: var(--text); }

.mode-switch button.on {
  background: var(--panel);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}

.mode-switch .save {
  display: inline-block;
  margin-left: .35rem;
  padding: .1rem .4rem;
  border-radius: 999px;
  background: var(--reveal-bg);
  color: var(--ok);
  font-size: .65rem;
  font-weight: 700;
  text-transform: none;
}

/* Un selector de color heredaba la regla general de los campos, que le mete
   relleno y un fondo propio: la muestra quedaba reducida a una rayita y el
   color elegido sólo se veía al abrir el selector. Aquí el campo ES la
   muestra. */
input[type="color"] {
  width: 100%;
  height: 44px;
  padding: 3px;
  cursor: pointer;
  background: var(--panel);
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch {
  border: 1px solid var(--border);
  border-radius: calc(var(--radius-sm) - 3px);
}
input[type="color"]::-moz-color-swatch {
  border: 1px solid var(--border);
  border-radius: calc(var(--radius-sm) - 3px);
}

/* --- Aviso con salida ----------------------------------------------------- */

/* Qué pasa, qué significa y qué hacer, en ese orden y en una sola pieza. La
   versión anterior era una línea de texto rojo con la que no se podía hacer
   nada desde donde estaba. */
.alert {
  display: flex; align-items: flex-start; gap: .85rem;
  padding: 1rem 1.1rem;
  margin-bottom: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
}
.alert-icon { flex-shrink: 0; display: grid; place-items: center; }
.alert-icon svg { width: 26px; height: 26px; }
.alert.danger .alert-icon { color: var(--danger); }
.alert.warn .alert-icon { color: var(--warn); }

.alert-body { flex: 1; min-width: 0; }
.alert-body strong { display: block; font-size: .98rem; margin-bottom: .15rem; }
.alert-body p { margin: 0; color: var(--muted); font-size: .84rem; line-height: 1.5; }

.alert button { flex-shrink: 0; align-self: center; min-height: 44px; }

@media (max-width: 620px) {
  .alert { flex-wrap: wrap; }
  .alert button { width: 100%; }
}

/* --- Paginación ---------------------------------------------------------- */

.pager {
  display: flex; align-items: center; justify-content: flex-end;
  gap: .75rem; margin-top: .85rem;
  color: var(--muted); font-size: .8rem;
}
.pager button { min-height: 40px; }

/* Un par clave/valor del registro. La clave en negrita y pegada al valor se
   lee de un vistazo; una línea de JSON hay que descifrarla. */
.kv {
  display: inline-flex; align-items: baseline; gap: .3rem;
  margin-right: .5rem; font-size: .78rem;
}
.kv b { color: var(--muted); font-weight: 600; }

/* --- Elegir forma de pago ------------------------------------------------ */

.pay-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  padding: 1.25rem;
  width: min(420px, calc(100vw - 2rem));
  box-shadow: var(--shadow-lg);
}

.pay-dialog::backdrop { background: rgba(15, 23, 42, .45); }

.pay-dialog h3 { font-size: 1.05rem; margin-bottom: .35rem; }
.pay-dialog .sub { margin-bottom: 1rem; }

/*
  El inventario de lo que una acción se lleva por delante.

  En renglón propio y no dentro del párrafo: "1.240 ventas, 380 clientes, 95
  citas" en una frase corrida no se lee, se ojea — y el número grande, que es lo
  que tiene que hacer parar, se pierde entre comas.
*/
.confirm-detalle {
  display: grid;
  gap: .3rem;
  margin-bottom: 1rem;
  padding: .6rem .75rem;
  border-left: 3px solid var(--danger);
  background: var(--panel-2);
  border-radius: var(--radius-sm);
}
.confirm-detalle span { font-size: .84rem; font-weight: 600; }

/* El campo de escribir para confirmar. Ancho completo: lo que se teclea es un
   nombre corto, y un campo estrecho invita a abreviarlo. */
#confirm-campo input { width: 100%; }
#confirm-campo label { display: block; margin-bottom: .3rem; }

.pay-methods { display: grid; gap: .6rem; margin-bottom: .75rem; }

/* Cada opción se lee como una decisión, no como una etiqueta: arriba qué es,
   abajo qué pasa si la eliges. Sin eso, "tarjeta" y "transferencia" obligan a
   adivinar cuál se acredita solo. */
.pay-methods button {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .15rem;
  text-align: left;
  padding: .8rem .9rem;
  min-height: 44px;
}

.pay-methods button strong { font-size: .9rem; font-weight: 600; }
.pay-methods button span { font-size: .75rem; font-weight: 400; opacity: .85; }

.plan-card .price-aside {
  font-size: .72rem;
  color: var(--muted);
  margin-top: -.25rem;
}

.code-reveal {
  border: 1px solid var(--border);
  border-left: 3px solid var(--ok);
  background: var(--reveal-bg);
  padding: 1rem; border-radius: var(--radius-sm); margin-top: .75rem;
}

/* --- Vista previa de imagen subida --------------------------------------- */

/* Un logo subido se guarda dentro del propio campo, así que el cuadro de texto
   mostraría una pared de base64. La vista previa lo reemplaza. */
.upload-preview { display: none; align-items: center; gap: .5rem; margin-top: .5rem; }
.upload-preview.on { display: flex; }
.upload-preview img {
  max-height: 64px; max-width: 180px; background: #fff;
  border: 1px solid var(--border); border-radius: 8px; padding: 4px;
}
.upload-preview span { font-size: .75rem; color: var(--muted); }
.upload-preview button { padding: .2rem .5rem; font-size: .7rem; min-height: 0; }

/* --- Interruptor de tema ------------------------------------------------- */

/* La forma la comparten varios botones de sólo icono; `theme-toggle` es
   además el gancho que el script usa para pintarles sol o luna. Separadas a
   propósito: el botón del menú tiene esta forma y no es un interruptor de
   tema, y con una sola clase el script le sobrescribía el icono. */
.icon-btn, .theme-toggle {
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--muted);
  border-radius: var(--radius-sm);
  width: 40px; height: 40px; min-height: 0;
  display: inline-grid; place-items: center;
  padding: 0;
}
.icon-btn:hover, .theme-toggle:hover { color: var(--text); border-color: var(--accent); filter: none; }
.icon-btn svg, .theme-toggle svg { width: 18px; height: 18px; }

/* --- Ingreso: la marca a un lado, el formulario al otro ------------------ */

.auth {
  /* 100vh mide la ventana con la barra de direcciones retraída, así que en un
     teléfono siempre sobra alto y aparece una barra de desplazamiento en una
     pantalla que cabe. 100dvh mide lo que de verdad se ve; el 100vh queda
     como respaldo para quien no lo entienda. */
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 1.05fr 1fr;
}

/* El lado de la marca. Se va en pantalla angosta: en un teléfono, un panel
   decorativo empuja el formulario fuera de la vista, y quien entra viene a
   entrar. */
.auth-brand {
  position: relative;
  overflow: hidden;
  padding: 2.5rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--panel-2);
  border-right: 1px solid var(--border);
}

/* Un halo detrás de la marca, dibujado con un gradiente: sin peticiones, sin
   imagen que pueda faltar, y escala a cualquier tamaño.

   Aquí había además una retícula de cuadros de 46 px. Se quitó: no era un
   plano, ni un lienzo, ni una superficie que se mida —era papel cuadriculado
   detrás de un formulario—, y es de las texturas que delatan una pantalla
   hecha con plantilla. La superficie limpia deja que el ojo vaya a la marca. */
.auth-brand::after {
  content: '';
  position: absolute;
  width: 520px; height: 520px;
  top: 12%; left: 50%;
  transform: translateX(-50%);
  background: radial-gradient(circle, var(--accent-ring), transparent 62%);
  opacity: .5;
  pointer-events: none;
}

.auth-brand > * { position: relative; z-index: 1; }

.auth-logo { display: flex; align-items: center; gap: .6rem; font-weight: 700; letter-spacing: -.02em; }
.auth-logo .mark {
  width: 38px; height: 38px; border-radius: .55rem;
  background: var(--accent); color: var(--on-accent);
  display: grid; place-items: center;
}
.auth-logo img {
  width: 38px; height: 38px; border-radius: .55rem; object-fit: contain;
  background: #fff; border: 1px solid var(--border); padding: 3px;
}

.auth-art { display: grid; place-items: center; flex: 1; padding: 1.5rem 0; }
.auth-art svg { width: min(330px, 80%); height: auto; }

.auth-pitch h2 { font-size: 1.45rem; line-height: 1.3; margin: 0 0 .9rem; max-width: 22ch; color: var(--text); }
.auth-pitch ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.auth-pitch li { display: flex; align-items: center; gap: .6rem; font-size: .875rem; color: var(--muted); }
.auth-pitch li svg { width: 17px; height: 17px; color: var(--accent); flex-shrink: 0; }

.auth-form-side {
  display: flex; flex-direction: column;
  padding: 1.25rem 2rem 2.5rem;
  background: var(--bg);
}
.auth-form-top { display: flex; justify-content: space-between; align-items: center; gap: .75rem; }

/* La marca del lado del formulario sólo existe para el teléfono: en pantalla
   ancha ya está la grande a la izquierda, y repetirla sería ruido. */
.auth-logo-compact { display: none; font-size: .95rem; min-width: 0; }
.auth-logo-compact span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.auth-logo-compact .mark { font-size: .95rem; }

.auth-form-wrap { flex: 1; display: grid; place-items: center; }
.auth-form { width: 100%; max-width: 370px; }
.auth-form h1 { font-size: 1.6rem; margin: 0 0 .35rem; }
.auth-form > p { color: var(--muted); margin: 0 0 1.6rem; font-size: .875rem; }
.auth-form .field { margin-bottom: .9rem; }
.auth-form button[type="submit"], .auth-form .primary { width: 100%; min-height: 44px; margin-top: .5rem; }

/* El ojo que muestra la contraseña. Sin esto, alguien con una contraseña larga
   la escribe a ciegas y la falla dos veces. */
.field-with-action { position: relative; }
.field-with-action input { padding-right: 2.6rem; }
.field-with-action .peek {
  position: absolute; right: .3rem; top: 50%; transform: translateY(-50%);
  background: transparent; border: 0; color: var(--muted);
  width: 2rem; height: 2rem; min-height: 0; padding: 0;
  display: grid; place-items: center; cursor: pointer;
}
.field-with-action .peek:hover { color: var(--text); filter: none; }
.field-with-action .peek svg { width: 17px; height: 17px; }

.auth-error {
  color: var(--danger); font-size: .82rem; min-height: 1.25rem; margin-top: .6rem;
}

@media (max-width: 860px) {
  .auth { grid-template-columns: 1fr; }
  .auth-brand { display: none; }
  .auth-form-side { padding: 1rem 1.25rem 2rem; }
  .auth-logo-compact { display: flex; }
}

/* --- Planes y recargas --------------------------------------------------- */

/* auto-fill con un mínimo, no un número fijo de columnas: el catálogo cambia y
   una rejilla de tres se ve rota con dos. */
.plan-grid {
  display: grid; gap: .75rem; margin-top: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
}
.plan-card {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: .95rem; background: var(--panel-2);
  display: flex; flex-direction: column; gap: .35rem;
}
.plan-card.current { border-color: var(--accent); }
.plan-card h3 { margin: 0; font-size: .95rem; }
.plan-card .price { font-size: 1.35rem; font-weight: 700; color: var(--text); letter-spacing: -.02em; }
.plan-card .price small { font-size: .7rem; font-weight: 500; color: var(--muted); }
.plan-card ul { margin: .25rem 0 0; padding-left: 1.1rem; font-size: .75rem; color: var(--muted); }
.plan-card button { margin-top: auto; width: 100%; }
.plan-card .pill { align-self: flex-start; }

/* --- Compartir el estudio ------------------------------------------------ */

.share { display: flex; gap: 1.5rem; flex-wrap: wrap; align-items: flex-start; }
.qr-frame {
  background: #fff; padding: .75rem; border-radius: var(--radius);
  width: 220px; height: 220px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border);
}
.qr-frame svg, .qr-frame img { width: 100%; height: 100%; display: block; }
/* Mientras se genera, o cuando no se puede: texto oscuro sobre el marco blanco
   —que es blanco siempre, para que el codigo se lea— y no el color del tema. */
.qr-frame .qr-cargando {
  color: #020617;
  font-size: .75rem;
  text-align: center;
  line-height: 1.4;
}
.share-side { flex: 1 1 280px; min-width: 260px; }
.link-box { display: flex; gap: .5rem; margin: .5rem 0 1rem; }
.link-box input { font-family: ui-monospace, monospace; font-size: .8rem; }

/* --- Avisos flotantes ---------------------------------------------------- */

/*
  Antes el único canal era una línea de texto al tope de la página: guardando
  un formulario abajo, el error aparecía fuera de la pantalla y la acción
  parecía no haber hecho nada. Eso fue exactamente lo que pasó al subir un
  logo demasiado pesado.
*/
.toasts {
  position: fixed; right: 1rem; bottom: 1rem; z-index: 60;
  display: flex; flex-direction: column; gap: .5rem;
  max-width: min(380px, calc(100vw - 2rem));
  pointer-events: none;
}
.toast {
  display: flex; align-items: flex-start; gap: .6rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-left: 3px solid var(--muted);
  border-radius: var(--radius-sm);
  padding: .7rem .9rem; font-size: .85rem; line-height: 1.45;
  box-shadow: var(--shadow);
  pointer-events: auto; cursor: pointer;
  animation: toast-in .18s ease-out;
}
.toast.ok { border-left-color: var(--ok); }
.toast.error { border-left-color: var(--danger); }
/* Ni bien ni mal: "se acabó tu cuota de esta foto" no es un fallo, es un tope
   haciendo su trabajo. En rojo parecía que algo se rompió. */
.toast.warn { border-left-color: var(--warn); }
.toast svg { width: 17px; height: 17px; flex-shrink: 0; margin-top: .1rem; }
.toast.ok svg { color: var(--ok); }
.toast.error svg { color: var(--danger); }
.toast.warn svg { color: var(--warn); }
.toast.leaving { animation: toast-out .18s ease-in forwards; }

@keyframes toast-in  { from { opacity: 0; transform: translateY(8px); } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(8px); } }

/* --- Mensajes en línea --------------------------------------------------- */

#error, .error-line { color: var(--danger); font-size: .875rem; min-height: 1.25rem; }
.ok-msg { color: var(--ok); font-size: .875rem; }
.whoami { font-size: .8rem; color: var(--muted); }

.code-reveal strong {
  font-family: ui-monospace, monospace; font-size: 1.6rem; letter-spacing: .08em;
  display: block; margin: .25rem 0;
}

/*
  Reducir movimiento no es apagarlo: un cambio de estado que ocurre sin ninguna
  señal se lee como que no pasó nada. Se acorta a un umbral que no marea y se
  conserva la transición de color, que informa sin desplazar nada.
*/
@media (prefers-reduced-motion: reduce) {
  .toast, .toast.leaving { animation: none; }
  .sidebar { transition: none; }
}


/* --- El ticket ------------------------------------------------------------
   Pensado para una impresora térmica de 58 mm, que es lo que hay en el
   mostrador de una barbería. Ancho en milímetros y no en píxeles: el papel
   mide lo que mide, y una medida en píxeles depende de a cuántos puntos por
   pulgada imprima cada aparato.

   Monoespaciada porque los importes tienen que alinearse a la derecha unos
   debajo de otros para poder sumarlos de un vistazo.

   Todo cuelga de `.printing-ticket`, que se pone justo antes de imprimir y se
   quita después. Esta hoja la comparten el panel y /admin, y unas reglas que
   escondieran el cuerpo en cada impresión romperían cualquier otra cosa que
   alguien quisiera imprimir desde cualquiera de las dos. */
/*
  La FORMA del ticket vive fuera de @media print, y eso es a propósito.

  Para saber qué alto de papel pedir hay que medir el ticket ya dibujado, y un
  elemento con `display: none` mide cero. Si la tipografía viviera dentro del
  bloque de impresión, medirlo en pantalla daría el alto de otra cosa.

  Sólo `display` y el ocultar el resto del cuerpo dependen de imprimir. Todo lo
  de abajo cuelga de `#ticket`, que normalmente no se muestra, así que no le
  cuesta nada a la pantalla.
*/
#ticket {
  display: none;
  padding: 0 2mm;
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 9pt;
  line-height: 1.35;
  color: #000;
  background: #fff;
}
#ticket .t-center { text-align: center; }
#ticket .t-rule { border-top: 1px dashed #000; margin: 2.5mm 0; }
#ticket .t-row { display: flex; justify-content: space-between; gap: 2mm; }
#ticket .t-row span:last-child { white-space: nowrap; }
#ticket .t-total { font-size: 11pt; font-weight: 700; margin: 2mm 0; }
#ticket .t-small { font-size: 7.5pt; }
/* El logo, acotado. Una impresora térmica no tiene grises: sale a puntos, y
   algo alto se come el rollo antes de llegar a los precios. */
#ticket .t-logo { max-width: 40mm; max-height: 15mm; filter: grayscale(1) contrast(1.6); }

/*
  Las clases del CORTE DE CAJA, que no estaban definidas.

  `renderCorte` usa `t-line`, `t-sub`, `t-head`, `t-note` y `t-foot` desde
  siempre, y ninguna existía en esta hoja: salían como divs planos, sin un
  milímetro de separación entre secciones. Es el "está todo junto" que reportó
  Héctor, y no era del ticket de venta sino de este papel.
*/
/* El nombre del negocio en el corte. Tampoco estaba definida: salía del
   tamaño del texto normal y alineada a la izquierda, como una línea más. */
#ticket .t-shop {
  text-align: center;
  font-weight: 700;
  font-size: 11pt;
  margin-bottom: 1mm;
}
#ticket .t-line { border-top: 1px solid #000; margin: 2.5mm 0; }
#ticket .t-sub { font-size: 7.5pt; }
/* Los títulos de sección respiran ARRIBA, no abajo: lo que separa dos bloques
   es el hueco antes del título, no el de después. */
#ticket .t-sec {
  margin: 3mm 0 1mm;
  font-weight: 700;
  font-size: 8pt;
  letter-spacing: .03em;
}
#ticket .t-note {
  margin: 2mm 0;
  padding: 1.5mm 0;
  border-top: 1px dotted #000;
  border-bottom: 1px dotted #000;
  font-size: 7.5pt;
}

/*
  La firma de la plataforma, centrada y separada.

  Iba pegada al final del papel y alineada a la izquierda, en el corte y en el
  ticket. Es una firma: va centrada y con aire, o parece una línea más del
  detalle — que es exactamente lo que Héctor vio.
*/
#ticket .t-foot {
  margin-top: 3mm;
  padding-top: 2mm;
  border-top: 1px dashed #000;
  text-align: center;
  font-size: 7.5pt;
}

/* El encabezado del papel: el logo y el nombre necesitan aire antes de la
   primera línea de contenido. */
#ticket .t-marca { margin-bottom: 2.5mm; }
#ticket .t-marca .t-logo { margin-bottom: 1.5mm; }

/*
  Los puntos que generó la visita.

  Se enmarcan porque es lo que hace que alguien vuelva, y en un papel de 58 mm
  todo tiene el mismo peso salvo lo que se enmarque.
*/
#ticket .t-puntos {
  margin: 2.5mm 0;
  padding: 1.5mm 2mm;
  border: 1px solid #000;
  text-align: center;
  font-size: 8pt;
}
#ticket .t-puntos strong { font-size: 9.5pt; }

/* Sólo para medirlo: se dibuja fuera de la vista, con su forma de papel. */
.measuring-ticket #ticket {
  display: block;
  position: absolute;
  left: -9999px;
  top: 0;
  visibility: hidden;
}

@media print {
  /* La hoja la comparten el panel y /admin; sin acotar a esta clase,
     cualquier otra impresión desde cualquiera de las dos saldría en blanco. */
  /* El armazón recorta lo que sobra en pantalla; al imprimir no hay ventana
     que llenar y ese recorte cortaría un ticket largo por la mitad. */
  .printing-ticket .shell,
  .printing-ticket .content { height: auto !important; overflow: visible !important; }
  .printing-ticket body > *:not(#ticket) { display: none !important; }
  .printing-ticket #ticket { display: block !important; margin: 0; }
  .printing-ticket { background: #fff; }

  /*
    El reporte impreso, que es el "exportar a PDF".

    No se genera un PDF a mano: sin librerías eso significa escribir fuentes y
    tablas byte a byte, y saldría peor que lo que cualquier navegador hace con
    "Guardar como PDF". Lo que hace falta es que la HOJA valga la pena, y eso
    es esto: se va la barra lateral, se van los controles, y queda el reporte.
  */
  .printing-report .shell,
  .printing-report .content { height: auto !important; overflow: visible !important; }
  .printing-report .sidebar,
  .printing-report .topbar,
  .printing-report #report-filters,
  .printing-report #ticket,
  /* Los avisos flotantes: en pantalla desaparecen solos, en papel se quedan
     impresos encima del reporte para siempre. */
  .printing-report #toasts,
  /* Y los botones de la tabla. "Ticket" en una hoja de papel es un botón que
     nadie puede pulsar ocupando una columna entera. */
  .printing-report #sale-history button,
  .printing-report .segmented,
  /* Y el encabezado de la PANTALLA. En papel sale justo encima del encabezado
     del reporte, así que el papel empezaba diciendo dos veces de qué va: primero
     "Reportes · Lo que se cobró…" y debajo el nombre del negocio con su fecha.
     El segundo es el que sirve para archivar; el primero es un rótulo de menú. */
  .printing-report .page-head,
  /* Y la instruccion de la grafica: "pasa el mouse" es imposible en papel. */
  .printing-report #report-chart-card .sub { display: none !important; }

  /*
    El encabezado que sólo existe en el papel.

    Sale del flujo normal —está `hidden` en pantalla— y aquí se enseña: un
    reporte que circula sin el nombre del negocio y sin la fecha en que se sacó
    no se puede archivar ni comparar con otro.
  */
  .printing-report #report-print-head { display: block !important; margin-bottom: 1rem; }
  .printing-report #report-print-head h1 { margin: 0; font-size: 1.4rem; }
  .printing-report #report-print-head p { margin: .25rem 0 0; font-size: .8rem; color: #555; }

  /*
    La firma de la plataforma al pie. En chico, centrada y gris.

    Centrada y no a la derecha: a la derecha compite con las cifras, que es la
    columna donde el ojo ya está. En el centro se lee como lo que es — el final
    de la hoja.

    `break-before: avoid` para que no se vaya sola a una hoja nueva. Una página
    entera en blanco con "by Kabelo" en medio no firma nada: hace pensar que la
    impresión se rompió.
  */
  .printing-report #report-print-foot {
    display: block !important;
    margin-top: 8pt;
    text-align: center;
    font-size: 7.5pt;
    color: #777;
    break-before: avoid;
  }
  /* Todas las vistas menos Reportes: en el panel están todas en el DOM y sólo
     una se ve, pero `@media print` no hereda esa decisión. */
  .printing-report [data-view]:not([data-view="reportes"]) { display: none !important; }
  .printing-report { background: #fff; }
  /*
    Las tarjetas, en dos columnas y sin sombra.

    El papel no tiene modo oscuro ni relieve: una sombra impresa es una mancha
    gris que gasta tinta y no dice nada.
  */
  .printing-report .card {
    box-shadow: none !important;
    border: 1px solid #ccc;
  }
  /*
    Que una tarjeta no se parta entre dos hojas — pero sólo las CHICAS.

    Estaba en `.card`, todas, y ahí hacía justo lo contrario de lo que quería:
    una tarjeta con cien ventas dentro no cabe en ninguna hoja, así que el
    navegador saltaba a la siguiente y la partía igual, dejando media hoja en
    blanco. Dos hojas enteras se iban en esos saltos.

    Un desglose de cinco renglones cortado a la mitad sí es un problema: obliga
    a ir y volver de página. Una tabla de cien no: se lee de corrido.
  */
  .printing-report .summary-card { break-inside: avoid; }
  /* Y en la tabla, lo que no se parte es la FILA. Un renglón cortado por la
     mitad deja el nombre en una hoja y el importe en la siguiente. */
  .printing-report tr { break-inside: avoid; }
  .printing-report .summary-grid { grid-template-columns: 1fr 1fr; }
  .printing-report .table-wrap { overflow: visible !important; }
  /* El encabezado de la tabla se repite en cada hoja: sin él, la segunda
     página son números sin columnas. */
  .printing-report thead { display: table-header-group; }

  /*
    --- Y que quepa ----------------------------------------------------------

    Un mes de un salón que va bien —cien ventas— salía en SIETE hojas, y seis
    eran la tabla del detalle: 101 renglones a 16mm cada uno, que es la altura
    de una fila pensada para tocarse con el dedo. En papel no hay dedos.

    Lo mide `scripts/pesar-reporte-pdf.mjs`, contra el mismo `page.pdf()` de
    Chromium que usa el botón, y con los datos que devuelve el motor de verdad.
  */

  /*
    Los márgenes del papel. El navegador pone 1cm por lado y luego cada tarjeta
    añade el suyo: acaba habiendo más borde que reporte.

    `@page` no se puede acotar a una clase, así que esto vale también para el
    TICKET. Por eso baja y no sube: un ticket sale en papel angosto, y darle dos
    milímetros más de ancho útil no le hace daño a nadie — subirlo sí se los
    habría quitado.
  */
  @page { margin: 8mm; }

  /*
    La letra baja a 9pt. No más: por debajo de 8pt un reporte deja de leerse
    impreso, y un papel que no se lee no ahorra nada — se vuelve a imprimir.
  */
  .printing-report { font-size: 9pt; }
  .printing-report table { font-size: 8.5pt; }
  /* La fila de tabla, de 16mm a unos 6mm. Aquí está el 86% del problema. */
  .printing-report th,
  .printing-report td { padding: 2pt 4pt; }

  /* Las tarjetas: menos aire y menos borde. En pantalla el aire separa; en una
     hoja, con todo junto y quieto, sólo gasta papel. */
  .printing-report .card { padding: 6pt 8pt; margin-top: 6pt !important; }
  .printing-report .card-head { margin-bottom: 4pt; }
  .printing-report .summary-card .card-head { margin-bottom: 4pt; }
  .printing-report h2 { font-size: 10pt; margin: 0; }
  .printing-report .summary-rows > div { padding: 2pt 0; }

  /*
    Los resúmenes, tantos por fila como quepan sin apretar.

    Puse `repeat(4, 1fr)` para ahorrar media hoja y salió el peor papel de todo
    esto: con sólo dos tarjetas, cada una se quedó en un cuarto del ancho, y
    `$14,850.00` no cabe en esa columna. Como `.summary-rows .v` lleva
    `overflow-wrap: anywhere` —que en pantalla evita que un nombre largo
    desborde— el importe se partió LETRA POR LETRA en vertical: un dígito por
    renglón, diez renglones por cifra. Una tarjeta de seis líneas ocupaba media
    hoja ella sola.

    Con `auto-fit` y un mínimo de 62mm, entran las que quepan de verdad y ni una
    más. Lo caza `scripts/pesar-reporte-pdf.mjs`, que ahora comprueba que ningún
    importe se parta.
  */
  .printing-report .summary-grid {
    grid-template-columns: repeat(auto-fit, minmax(62mm, 1fr));
    gap: 6pt;
  }
  /* Y por si acaso: en papel una cifra NUNCA se parte. Cortar un importe es
     cambiarlo — "$1,150.00" partido se lee como "$1" y "150.00". */
  .printing-report .summary-rows .v { overflow-wrap: normal; white-space: nowrap; }

  /*
    La gráfica, a un tercio de hoja.

    Ocupaba 80mm porque en pantalla se estira a lo que haya. En papel es una
    ilustración de apoyo —"¿qué día vendí más?"— y para eso le sobra con 55mm.
  */
  .printing-report #report-chart svg { max-height: 55mm; width: 100%; }

  /*
    Y el detalle de las ventas, PLEGADO salvo que lo pidan.

    Es la decisión que de verdad quita hojas: el resumen entero cabe en una y el
    detalle son seis más. Y ese detalle ya se va completo —con más columnas— en
    el archivo de Excel, que es donde se trabaja con él; en papel nadie suma
    cien renglones a mano.

    Quien lo quiera lo enciende con la casilla de al lado del botón. No se
    decide por él: se elige un valor por defecto.
  */
  .printing-report:not(.print-detalle) #sale-history tbody tr:nth-child(n + 16) {
    display: none;
  }
  .printing-report.print-detalle #sale-history-mas { display: none; }
  .printing-report #sale-history-mas {
    margin: 4pt 0 0;
    font-size: 8pt;
    color: #555;
  }
}

/* --- Los reportes ---------------------------------------------------------- */

/*
  La fila de filtros: se envuelve sola.

  Seis controles no caben en un panel de 380px y no tienen por qué: `wrap` los
  baja de renglón antes que encogerlos hasta que un `<select>` con nombres de
  personas muestre "Ma…". Un filtro que no se puede leer no se usa.
*/
.filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .75rem;
  margin-top: 1rem;
}
.filter-row > label {
  display: grid;
  gap: .25rem;
  /* Crece si hay lugar, pero nunca baja de donde una fecha deja de leerse. */
  flex: 1 1 10rem;
  min-width: 9rem;
  margin: 0;
}
.filter-row > label > span {
  color: var(--muted);
  font-size: .78rem;
}
.filter-row > label.inline {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex: 0 0 auto;
}
.filter-row > label.inline > span { font-size: .82rem; }
.filter-row > button { flex: 0 0 auto; }
/* La fila de acciones va a la derecha: son el final del formulario, no otro
   filtro más. */
.filter-row-end { justify-content: flex-end; }

/*
  La gráfica, en SVG y sin librería.

  Ninguna página de esta plataforma le pide nada a otro dominio, así que una
  librería de gráficas por CDN no es una opción. Barras y un eje bastan para
  contestar "¿qué día vendí más?", que es la única pregunta que se le hace.
*/
.chart-scroll {
  overflow-x: auto;
  /* Noventa días son 3060px de barras. Sin este scroll propio, la PÁGINA
     entera se movería de lado, que es el error que ya salió una vez con el
     menú colapsado. */
  padding-bottom: .25rem;
}
.chart { display: block; }
.chart .bar { fill: var(--accent); }
/* Al pasar el mouse, la barra se aclara y el `<title>` de dentro enseña el
   dato exacto: en 22px de ancho no cabe un importe. */
.chart .bar:hover { fill: var(--accent-strong, var(--accent)); opacity: .85; }
.chart .chart-axis { stroke: var(--border); stroke-width: 1; }
.chart .bar-label {
  fill: var(--muted);
  font-size: 9px;
  font-family: inherit;
}

/* --- Las líneas de la venta ------------------------------------------------ */

/*
  Nombre e importe en la primera línea, los controles debajo.

  Antes era una sola fila con menos, cantidad, más, nombre y precio, y en un
  panel de 380px el nombre se quedaba con cuarenta píxeles: "Balayage Prem…".
  Lo que se cobra tiene que poder leerse entero.
*/
.pos-line {
  display: grid; gap: .1rem .75rem;
  grid-template-columns: 1fr auto;
  grid-template-areas: 'nom tot' 'ctl ctl';
  padding: .6rem 0;
  border-bottom: 1px solid var(--border);
}
.pos-line:last-child { border-bottom: 0; }
.pos-line .nom { grid-area: nom; font-weight: 600; font-size: .875rem; }
.pos-line .tot { grid-area: tot; font-weight: 700; font-size: .875rem; }
.pos-line .ctl { grid-area: ctl; display: flex; align-items: center; gap: .35rem; }
.pos-line .ctl button {
  min-height: 30px; min-width: 30px; padding: 0;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  color: var(--text); font-size: 1rem; line-height: 1;
}
.pos-line .ctl button:hover:not(:disabled) { background: var(--field); filter: none; }
.pos-line .num { min-width: 1.4rem; text-align: center; font-weight: 700; font-size: .85rem; }
.pos-line .uni { color: var(--muted); font-size: .74rem; margin-left: .25rem; }

/* Quién atendió una línea. Va debajo de los controles y sólo en servicios. */
.pos-line { grid-template-areas: 'nom tot' 'ctl ctl' 'quien quien'; }
.pos-line .quien { grid-area: quien; margin-top: .3rem; }
.pos-line .quien select { font-size: .78rem; min-height: 34px; padding: .2rem .5rem; }

/* El plegado de las últimas ventas. */
#sale-history-box > summary {
  cursor: pointer; font-weight: 600; font-size: .875rem;
  padding: .6rem .75rem; border-radius: var(--radius-sm);
  background: var(--panel-2); border: 1px solid var(--border);
  list-style-position: inside;
}
#sale-history-box > summary:hover { background: var(--field); }
#sale-history-box > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* El botón de una cabecera de tarjeta no se parte en dos renglones: si no
   cabe al lado del título, la retícula ya lo baja entero. */
.card-head > button { white-space: nowrap; }

/* --- Los puntos en el mostrador ------------------------------------------- */

.sale-loyalty {
  background: var(--copper-soft);
  border: 1px solid color-mix(in srgb, var(--copper) 35%, transparent);
  border-radius: var(--radius-sm);
  padding: .7rem .8rem;
  margin: .75rem 0 0;
}
.sale-loyalty label { color: var(--text); }

/* El descuento en rojo y con su signo, como en la lámina: es lo único de la
   columna que RESTA, y leerlo como un cargo más sería leer mal el ticket. */
.sale-discount {
  display: flex; justify-content: space-between; gap: 1rem;
  margin-top: .6rem; font-size: .875rem; font-weight: 600;
  color: var(--danger);
}

/* Entra y sale. El color acompaña al signo, nunca lo sustituye: el importe ya
   lleva su + o su − escrito. */
.mov-in { color: var(--ok); }
.mov-out { color: var(--danger); }

/* El precio de una línea, editable en sitio. Angosto a propósito: es un
   importe, no un campo de texto, y darle todo el ancho invita a escribir
   cualquier cosa. */
.pos-line .uni-edit {
  width: 5.5rem; min-height: 30px; padding: .1rem .4rem;
  font-size: .78rem; text-align: right; margin-left: .25rem;
}

/* --- La agenda del día -----------------------------------------------------
   Una columna por persona y el reloj a la izquierda, como la lámina.

   La posición de una tarjeta ES el dato: dónde empieza y cuánto dura se leen
   de un vistazo porque el alto se calcula en minutos, no en filas de tabla.
   Por eso todo cuelga de `--min`, los píxeles que mide un minuto: cambiarlo
   acerca o separa el día entero sin tocar una sola cuenta en el JavaScript.
   -------------------------------------------------------------------------- */
.agenda-sheet {
  --min: 1.15px;
  --hora: calc(var(--min) * 60);
  --rail: 62px;
  --cols: var(--rail);
  min-width: fit-content;
}

/* Las cabeceras se quedan arriba al bajar por el día: una columna sin nombre
   a las seis de la tarde es una columna de nadie. */
.agenda-heads,
.agenda-body {
  display: grid;
  grid-template-columns: var(--cols);
}
.agenda-heads {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.agenda-body { position: relative; }

/*
  La agenda ocupa el alto que hay, no un porcentaje.

  Estaba en `min(72vh, 780px)`: en una pantalla grande sobraba un palmo debajo
  de la última hora y en una chica se quedaba corta. Ahora la vista es una
  columna que llena el contenido y la tarjeta del día se estira hasta el
  fondo — la agenda es la única pantalla donde ver una hora más es ver más
  trabajo.

  `min-height: 0` en la cadena es lo que hace que funcione: sin él, un hijo
  flexible se niega a encoger por debajo de su contenido y el desplazamiento se
  va a la página entera en vez de quedarse dentro de la retícula.
*/
/* `.content >` y no `[data-view="agenda"]` a secas: el BOTÓN del menú lleva el
   mismo atributo, y la regla lo convertía en una columna con el icono encima
   del texto. Es la misma trampa que `showView` ya esquiva con `:not(.nav-link)`. */
.content > [data-view="agenda"] {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.content > [data-view="agenda"] > .card:last-child {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  /* La tarjeta ya no pinta marco: el marco lo pone `.agenda-scroll`, y dos
     bordes concéntricos separados por el relleno se ven como un error. */
  padding: 0;
  border: 0;
  background: none;
  /* Y sin el margen de abajo, que en la última tarjeta de una columna que
     llega al fondo es un hueco y no una separación. */
  margin-bottom: 0;
}

.agenda-scroll {
  overflow: auto;
  flex: 1;
  min-height: 18rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}

.agenda-pro {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .7rem .8rem;
  border-left: 1px solid var(--border);
  min-width: 0;
}
/* La inicial y no una foto: el producto no guarda fotos de quien trabaja aquí,
   y un avatar genérico para todos no distingue a nadie de nadie. */
.agenda-pro .inicial {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700;
  font-size: .9rem;
}
.agenda-pro b {
  display: block;
  font-size: .85rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.agenda-pro span {
  display: block;
  font-size: .74rem;
  color: var(--muted);
}

/* El riel de horas. La etiqueta se sienta sobre la línea que nombra, no en
   medio de la franja: la línea es la que marca las nueve en punto. */
.agenda-rail { position: relative; }
.agenda-rail div {
  height: var(--hora);
  padding-right: .5rem;
  text-align: right;
  font-size: .74rem;
  color: var(--muted);
  transform: translateY(-.5em);
}

.agenda-col {
  position: relative;
  border-left: 1px solid var(--border);
  /* Las rayas van al fondo y no en elementos: son sesenta líneas por jornada y
     cada una sería un nodo que no dice nada.

     Dos capas: la hora en punto marcada, y la media más tenue. Sin la media
     hay que medir a ojo dónde cae "y treinta" en una franja de 69 píxeles, y
     ése es el error que pone una cita media hora corrida. */
  background-image:
    linear-gradient(to bottom, var(--border) 1px, transparent 1px),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--border) 45%, transparent) 1px, transparent 1px);
  background-size: 100% var(--hora), 100% calc(var(--hora) / 2);
}

/* La columna donde se va a soltar, encendida: dice a qué agenda va a parar la
   cita cuando hay cuatro columnas y el cursor está entre dos. */
.agenda-col.recibiendo { background-color: color-mix(in srgb, var(--accent) 6%, transparent); }

/*
  Una cita.

  El texto se corta con puntos suspensivos en vez de encoger la letra: una cita
  de quince minutos mide dieciocho píxeles, y ahí no cabe nada que se pueda
  leer. Lo que sí cabe —la hora y el nombre— tiene que seguir legible.
*/
.cita {
  --tono: var(--muted);
  --pv: .3rem;
  /* La tarjeta se mide a sí misma: una cita de quince minutos y una de dos horas
     son el mismo marcado con alturas distintas, y lo que cabe dentro lo decide
     el alto, no el JavaScript. Ver las consultas de contenedor más abajo. */
  container-type: size;
  position: absolute;
  left: 4px; right: 4px;
  overflow: hidden;
  /* Un <button> centra su contenido en vertical: en una cita de dos horas eso
     deja la hora flotando a la mitad, lejos del renglón que le corresponde en
     el riel. Se pega arriba, que es donde empieza la cita. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  width: auto;
  text-align: left;
  padding: var(--pv) .45rem;
  line-height: 1.25;
  border: 1px solid color-mix(in srgb, var(--tono) 40%, transparent);
  border-left: 3px solid var(--tono);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--tono) 9%, var(--panel));
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
.cita:hover { background: color-mix(in srgb, var(--tono) 16%, var(--panel)); }
.cita.elegida {
  outline: 2px solid var(--tono);
  outline-offset: -1px;
}
/*
  La hora no se encoge y el nombre sí.

  La hora mide siempre lo mismo —cinco caracteres— y recortarla no ahorra nada
  útil: "11:0…" no es una hora. El nombre es lo que puede medir tres letras o
  veinte, así que es el que cede, y cede con puntos suspensivos en vez de
  desbordar la tarjeta.
*/
.cita .reloj {
  flex: none;
  font-size: .72rem;
  color: var(--text-soft);
  white-space: nowrap;
}
.cita strong {
  /* Se lleva el hueco que quedaba libre a la derecha de la hora, que es de
     donde salió esta idea. */
  flex: 1 1 auto;
  min-width: 0;
  font-size: .82rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cita .servicios {
  display: block;
  font-size: .72rem;
  color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Una cancelada y un plantón ya no ocupan la silla: se ven como lo que son,
   un hueco con historia, y no compiten con las citas que sí van a pasar. */
.cita.libera { border-style: dashed; background: var(--panel); }
.cita.libera strong { text-decoration: line-through; }

.tone-neutral { --tono: var(--border-strong); }
.tone-info { --tono: var(--info); }
.tone-warning { --tono: var(--warn); }
.tone-brand { --tono: var(--accent); }
.tone-success { --tono: var(--ok); }
.tone-danger { --tono: var(--danger); }
.tone-danger-muted { --tono: color-mix(in srgb, var(--danger) 50%, var(--muted)); }

/* --- La semana ------------------------------------------------------------ */

/*
  Siete columnas con la lista de cada día, y no la retícula de horas.

  En una columna de ciento cincuenta píxeles, una cita de quince minutos mide
  diecisiete de alto: no cabe ni la hora. La lista dice lo mismo —qué hay y
  cuándo— y deja ver de un vistazo el jueves vacío, que es lo que se busca al
  mirar una semana.
*/
.agenda-semana {
  display: grid;
  grid-template-columns: var(--cols);
  gap: 1px;
  background: var(--border);
  min-width: fit-content;
}
.agenda-semana .dia-col {
  display: flex;
  flex-direction: column;
  background: var(--panel);
  min-height: 12rem;
}
.dia-cabeza {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  padding: .5rem .6rem;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
  text-align: left;
  font: inherit;
}
.dia-cabeza:hover { background: var(--field); }
.dia-cabeza .nombre { font-size: .75rem; color: var(--muted); text-transform: uppercase; }
.dia-cabeza .numero { font-size: 1.1rem; font-weight: 700; }
/* Hoy, marcado en la cabecera y no con un fondo en toda la columna: el fondo
   compite con las tarjetas, que es lo que hay que leer. */
.dia-col.es-hoy .dia-cabeza { background: var(--accent); color: var(--on-accent); }
.dia-col.es-hoy .dia-cabeza .nombre { color: inherit; opacity: .85; }

.dia-citas { display: flex; flex-direction: column; gap: .3rem; padding: .4rem; }
.dia-citas .vacio { margin: .4rem 0 0; text-align: center; }

/* Una cita en una lista: la hora arriba y el nombre debajo, con la franja del
   estado a la izquierda igual que en el día. */
.cita-lista {
  --tono: var(--border-strong);
  display: block;
  width: 100%;
  text-align: left;
  padding: .35rem .45rem;
  border: 1px solid color-mix(in srgb, var(--tono) 40%, transparent);
  border-left: 3px solid var(--tono);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--tono) 9%, var(--panel));
  color: var(--text);
  font: inherit;
  cursor: pointer;
  line-height: 1.3;
}
.cita-lista:hover { background: color-mix(in srgb, var(--tono) 16%, var(--panel)); }
.cita-lista .reloj { display: block; font-size: .72rem; color: var(--text-soft); }
.cita-lista strong {
  display: block; font-size: .82rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cita-lista .quien {
  display: block; font-size: .72rem; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cita-lista.libera { border-style: dashed; background: var(--panel); }
.cita-lista.libera strong { text-decoration: line-through; }

/* --- El mes --------------------------------------------------------------- */

/*
  La rejilla de siempre. No caben las citas enteras, así que caben las primeras
  y el resto se cuenta: lo que se pregunta mirando un mes no es a qué hora
  viene nadie, es qué días están llenos y cuáles vacíos.
*/
.agenda-mes {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  background: var(--border);
}
.mes-cabeza {
  padding: .4rem;
  background: var(--panel-2);
  text-align: center;
  font-size: .75rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
}
.mes-dia {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  align-items: stretch;
  min-height: 6.5rem;
  padding: .35rem;
  border: 0;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.mes-dia:hover { background: var(--field); }
.mes-dia .numero { font-size: .8rem; font-weight: 700; color: var(--text-soft); }
/* Los días de los meses vecinos existen para no romper las esquinas de la
   rejilla, no para leerse: apagados y sin invitar a tocarlos. */
.mes-dia.de-otro-mes { background: var(--panel-2); }
.mes-dia.de-otro-mes .numero { color: var(--muted); opacity: .6; }
.mes-dia.es-hoy .numero {
  align-self: flex-start;
  padding: 0 .35rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
}

.mes-cita {
  --tono: var(--border-strong);
  display: block;
  padding: 0 .25rem;
  border-left: 3px solid var(--tono);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--tono) 12%, var(--panel));
  font-size: .7rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mes-cita.libera { text-decoration: line-through; opacity: .65; }
.mes-mas { font-size: .68rem; color: var(--muted); }

@media (max-width: 700px) {
  /* En un teléfono el mes no cabe con texto dentro: se queda la rejilla con el
     número y el color, que sigue diciendo qué días están llenos. */
  .mes-dia { min-height: 3.2rem; }
  .mes-cita, .mes-mas { display: none; }
}

/* Fuera del horario del negocio: apagado, sin rayas. Las rayas son del tiempo
   bloqueado —comida, descanso— y significan otra cosa: eso es tiempo que el
   salón decidió no vender, esto es tiempo en que el salón no existe. */
.agenda-cerrado {
  position: absolute;
  left: 0; right: 0;
  /* Un gris que se distingue de la columna en claro Y en oscuro. Con
     `opacity` sobre `--field` la diferencia era de dos o tres por ciento de
     luminancia: estaba, pero había que buscarla. */
  background: color-mix(in srgb, var(--border) 38%, var(--panel));
  pointer-events: none;
}

/*
  Arrastrando.

  La tarjeta se queda a media tinta en su sitio de origen mientras viaja: sin
  eso no se sabe de dónde salió, y si el movimiento se rechaza no hay a dónde
  volver la vista.
*/
.cita.movible { cursor: grab; }
.cita.arrastrando { opacity: .4; cursor: grabbing; }

/*
  Y la guía, con la hora escrita.

  Soltar a ciegas es mover una cita a las 11:45 cuando se quería a las 12:00.
  La hora va en la línea, no en el cursor, porque es donde va a caer.
*/
.agenda-guia {
  position: absolute;
  left: 0; right: 0;
  height: 0;
  border-top: 2px dashed var(--accent);
  z-index: 3;
  pointer-events: none;
}
/* La hora, igual que la de la raya de ahora: pegada a la línea y con fondo
   propio para que se lea encima de una cita. */
.agenda-guia span {
  position: absolute;
  top: -.7em; left: .3rem;
  padding: 0 .25rem;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--on-accent);
  font-size: .68rem;
  font-weight: 700;
}

/* El estado, escrito. Nunca sólo el color: el sistema de diseño lo prohíbe por
   escrito y con razón — uno de cada doce hombres no distingue estos verdes. */
.estado {
  display: inline-block;
  border: 1px solid color-mix(in srgb, var(--tono) 45%, transparent);
  background: color-mix(in srgb, var(--tono) 14%, var(--panel));
  color: var(--text);
  border-radius: 999px;
  padding: .05rem .45rem;
  font-size: .68rem;
  font-weight: 600;
  white-space: nowrap;
}
/* La hora y el estado comparten renglón: el estado a la derecha, sin flotar —
   un elemento flotado dentro de una tarjeta de dieciocho píxeles se sale. */
.cita .cabeza {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .3rem;
}
/*
  Los tres en un renglón: cuándo, quién y en qué va.

  Alineados por la base y no al centro: la hora es más pequeña que el nombre, y
  centradas verticalmente se ven descolgadas una de otra en vez de leerse como
  una sola línea.
*/
.cita .cabeza {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  min-width: 0;
}
.cita .estado { flex: none; }

/*
  Pagada: la única pastilla que cambia de color en la agenda.

  Lo que se busca al cerrar el día no es quién vino — eso ya lo dice el color
  del borde de la cita — es quién PAGÓ. Y en una tarjeta de treinta minutos no
  caben dos pastillas, así que ésta sustituye al estado: una cita cobrada está
  completada por definición, porque el servidor la cierra al unirla con su
  venta.

  Verde y no el color de la marca: es un hecho contable, no decoración, y tiene
  que leerse igual en el salón que use turquesa y en el que use vino.
*/
.cita .estado.pagada {
  padding: 1px 6px;
  border-radius: var(--radius-pill, 999px);
  background: color-mix(in srgb, #2E7D52 22%, var(--surface, #fff));
  color: #1B5E3A;
  font-weight: 700;
  letter-spacing: .01em;
}
[data-theme="dark"] .cita .estado.pagada {
  background: color-mix(in srgb, #2E7D52 34%, #0D1B20);
  /* Sobre fondo oscuro, el verde profundo desaparece: sube a uno que sí se lee
     sin dejar de ser el mismo verde. */
  color: #8FE3B4;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .cita .estado.pagada {
    background: color-mix(in srgb, #2E7D52 34%, #0D1B20);
    color: #8FE3B4;
  }
}

/*
  Qué se sacrifica cuando la cita es corta.

  El orden de importancia es: a quién atiendes, en qué va, y al final qué se le
  hace. Sin esto una cita de treinta minutos enseñaba la hora recortada y la
  pastilla, y el nombre —el único dato que no está ya en el riel ni en el
  color— quedaba fuera de la tarjeta.
*/
/* Los altos que se consultan son los de la caja de contenido, sin relleno ni
   borde: una cita de una hora mide 69 px pero por dentro son 57. */
@container (max-height: 48px) {   /* 45 min */
  .cita .servicios { display: none; }
}
/*
  En una tarjeta corta, lo primero que se va es la HORA.

  El riel de la izquierda y la posición de la tarjeta ya dicen a qué hora es;
  repetirla aquí cuesta letras del nombre, que es el único dato que no está en
  ningún otro sitio de la pantalla.
*/
@container (max-height: 30px) {   /* 30 min */
  .cita .reloj { display: none; }
}
/*
  Y en carril estrecho se va también el estado, mida lo que mida de alto.

  Esto miraba además la altura, y por eso una cita de UNA HORA en carril se
  quedaba con los tres: la pastilla se llevaba el renglón y del nombre quedaban
  veintiún píxeles — una letra y unos puntos suspensivos. El problema nunca fue
  el alto: es que en ciento cincuenta píxeles de ancho no caben tres cosas.

  Entre el estado y el nombre gana el nombre. El estado sigue estando en el
  color del borde, en el rayado del plantón y en la ficha que se abre al
  tocarla; el nombre no está en ningún otro sitio de la pantalla.
*/
@container (max-width: 170px) {
  .cita .estado { display: none; }
}
/* Por debajo de media hora ya no cabe una línea con este cuerpo de letra: se
   encoge el nombre antes que recortarlo. La regla vive en el hijo porque una
   consulta de contenedor nunca alcanza al contenedor mismo. */
@container (max-height: 14px) {   /* 15 min */
  .cita strong { font-size: .74rem; margin-top: -.15rem; }
}

/* El tiempo bloqueado: rayado, sin borde de color y con su nombre. No es una
   cita fallida, es tiempo que no existe para la agenda. */
.agenda-bloqueo {
  position: absolute;
  left: 1px; right: 1px;
  display: grid;
  place-content: center;
  text-align: center;
  font-size: .72rem;
  color: var(--text-soft);
  background: repeating-linear-gradient(
    45deg,
    var(--field) 0 6px,
    color-mix(in srgb, var(--border) 60%, var(--field)) 6px 12px
  );
}
.agenda-bloqueo b { display: block; letter-spacing: .04em; text-transform: uppercase; }

/*
  La hora que es.

  Sólo se dibuja en el día de hoy. En cualquier otro día sería una raya roja
  cruzando una jornada que ya pasó o que no ha llegado, y lo único que diría es
  qué hora es en el reloj de quien mira.
*/
.agenda-ahora {
  position: absolute;
  left: 0; right: 0;
  height: 0;
  border-top: 2px solid var(--danger);
  z-index: 2;
  pointer-events: none;
}
/* La etiqueta ocupa el riel entero y alineada a la derecha, igual que las horas
   que hay debajo: si sólo tapara parte de "15:00" quedaría un "00" suelto al
   lado de las 14:57, que se lee como otra hora. */
.agenda-ahora span {
  position: absolute;
  top: -.65em; left: 0;
  width: var(--rail);
  text-align: right;
  padding-right: .5rem;
  font-size: .68rem;
  font-weight: 700;
  color: var(--danger);
  background: var(--panel);
}

/* La barra del día: mover el día es lo que más se hace en esta pantalla, así
   que va primero y del tamaño de un dedo. */
.agenda-bar {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
}
.agenda-bar .hoy { text-transform: none; }
.agenda-bar input[type="date"] { width: auto; min-width: 10rem; }
.agenda-bar .cuenta { margin-left: auto; color: var(--muted); font-size: .82rem; }

/* La ficha de la cita elegida, dentro del cajón. */
.cita-ficha { display: grid; gap: .5rem; margin-bottom: 1rem; }
.cita-ficha .dato { display: flex; gap: .6rem; align-items: baseline; }
.cita-ficha .dato span { flex: 0 0 7.5rem; color: var(--muted); font-size: .8rem; }
.cita-ficha .dato b { font-weight: 600; }

/*
  El cajón de apartar.

  Los servicios como casillas y no como un desplegable múltiple: una cita
  lleva dos o tres, y un `select multiple` obliga a saber que hay que dejar la
  tecla puesta para elegir el segundo. Con la duración de cada uno a la vista,
  porque es lo que decide si la cita cabe en el hueco que se está mirando.
*/
.nc-servicios {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem 1.25rem;
  margin-bottom: .2rem;
}
/* Cada una es un `label.inline`, que ya trae el renglón y los 44 px de zona
   tocable: aquí sólo se le dice cuánto ocupa en la fila. */
.nc-servicios label { flex: 1 1 200px; }
.nc-servicios .rato { color: var(--muted); font-size: .8rem; }

/* Los huecos son pastillas elegibles: se ofrecen, no se teclean. Con muchas
   horas libres la lista es larga, así que se desplaza sola en vez de estirar
   el cajón hasta que el botón de apartar queda fuera de la pantalla. */
.huecos {
  max-height: 11rem;
  overflow-y: auto;
  padding: .15rem;
  margin-bottom: .6rem;
}
.huecos:empty { display: none; }

/* Mañana, tarde y noche. El rótulo es más chico y más apagado que las horas:
   ordena la lista sin competir con lo que hay que elegir. */
/* Un día de la jornada: la casilla, y sus dos horas en la misma línea. En
   columnas fijas para que los siete días se lean como una tabla y no como
   siete formularios distintos. */
/*
  Un día de la jornada, con su tramo y —si hace falta— el segundo.

  El turno partido es lo normal en el oficio: entrar a las nueve, comer de dos
  a cuatro, salir a las ocho. La casilla "parte el día" revela el segundo
  tramo; escondido mientras no se use, porque la mayoría de los días son de
  corrido y dos parejas de desplegables en cada renglón convierten siete líneas
  en un formulario que da pereza abrir.
*/
/*
  Un día de la jornada, en dos renglones.

  Estuvo en una sola fila de cinco columnas —día, desde, a, hasta, "parte el
  día"— con una media query que la partía en pantallas estrechas. No servía: el
  cajón mide 420px SIEMPRE, esté en un teléfono o en un monitor de treinta
  pulgadas, y la media query mira el ancho de la VENTANA. En el monitor la fila
  no se partía nunca y "Parte el día" se salía por la derecha, con la lista
  entera rodando de lado.

  Así que no hay fila de cinco: hay dos renglones, y caben en cualquier sitio.
  Arriba el día y sus horas, que es lo que se mira; debajo y sangrado, la
  casilla de partir y —cuando está marcada— el segundo tramo. El sangrado es lo
  que hace que el segundo renglón se lea como continuación del día de arriba y
  no como otro día.
*/
.jornada-dia {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  align-items: center;
  gap: .4rem .5rem;
  padding-bottom: .5rem;
}
.jornada-dia label { margin: 0; }
.jornada-dia select { width: auto; min-width: 5.6rem; }
.jornada-dia .a { color: var(--muted); font-size: .8rem; }

.jornada-partir {
  grid-column: 1 / -1;
  padding-left: 1.6rem;
  font-size: .8rem;
  color: var(--muted);
}
.jornada-tarde {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: auto auto auto auto;
  justify-content: start;
  align-items: center;
  gap: .5rem;
  padding-left: 1.6rem;
}

/* Las pestañas de Configuración: separadas del contenido, para que se lean
   como "dónde estoy" y no como una fila de botones que hacen algo. */
.config-tabs { margin-bottom: 1rem; }
/* La acción de una sección va arriba y a la derecha, donde estaba la de la
   cabecera de página: no cambia de sitio, sólo de dueño. */
.config-accion { justify-content: flex-end; margin-bottom: .75rem; }

/* Un botón que abre un formulario dentro del cajón. En `ghost` se leía como
   una etiqueta gris y nadie lo tocaba: es una acción, y las acciones se ven. */
button.ancho { width: 100%; margin: .2rem 0 .1rem; }

/* El alta rápida: se despliega dentro del mismo cajón, hundida un poco para
   que se lea como un aparte y no como dos campos más del formulario. */
.alta-rapida {
  margin-top: .5rem;
  padding: .6rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}

.huecos .franja + .franja { margin-top: .5rem; }
.huecos .franja .titulo {
  display: block;
  margin-bottom: .3rem;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- La galería de trabajos, en Configuración --------------------------------
   Cuadrícula de miniaturas cuadradas: la foto se sube desde un teléfono y viene
   de cualquier forma, así que se recorta al centro igual que en la página
   pública. Lo que se ve aquí tiene que ser lo que se ve allá. */

.galeria-panel {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--k-space-4);
}
.foto-panel {
  margin: 0;
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-lg);
  background: var(--k-surface);
  overflow: hidden;
}
.foto-panel img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
}

/*
  La foto quitada se ve APAGADA, no desaparece.

  Quitarla de la página no es borrarla, y una que se esfuma al tocar "Quitar"
  hace creer que se perdió. Atenuada y con su botón diciendo "Poner", queda
  claro que sigue ahí y cómo devolverla. El texto del botón es lo que lo dice:
  la opacidad sola no comunica nada a quien no distingue el gris.
*/
.foto-panel.apagada img { opacity: .35; filter: grayscale(1); }

.foto-panel figcaption {
  display: grid;
  gap: var(--k-space-2);
  padding: var(--k-space-3);
}
.foto-panel figcaption input { width: 100%; }
.foto-acciones { display: flex; gap: var(--k-space-2); justify-content: flex-end; }
.foto-acciones button { min-width: 40px; }

/* --- Las opiniones, en Configuración -----------------------------------------
   Una lista, no una cuadrícula: se leen de arriba abajo y cada una lleva su
   respuesta debajo. La calificación NO se puede tocar desde aquí, y por eso no
   hay ningún control junto a las estrellas. */

.opinion-panel {
  padding: var(--k-space-4) 0;
  border-bottom: 1px solid var(--k-border);
}
.opinion-panel:last-child { border-bottom: 0; }

/*
  La escondida se ve atenuada, pero su nota NO.

  Es la regla entera del producto dicha en CSS: el texto se apaga porque ya no
  sale en la página; las estrellas siguen a plena luz porque siguen contando en
  la nota. Y lo dice además una etiqueta, no sólo el gris.
*/
.opinion-panel.escondida .opinion-dice { opacity: .5; }

.opinion-cabeza {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--k-space-3);
}
.opinion-cabeza .estrellas { color: var(--k-warning); letter-spacing: .1em; }
.opinion-cabeza .sub { font: var(--k-small) var(--k-font-sans); color: var(--k-text-muted); }
.opinion-cabeza button { margin-left: auto; }

.chip-off {
  padding: 2px var(--k-space-3);
  border-radius: var(--k-radius-pill);
  background: color-mix(in srgb, var(--k-text-muted) 15%, var(--k-surface));
  color: var(--k-text-muted);
  font: var(--k-small) var(--k-font-sans);
  font-weight: 600;
}

.opinion-dice { margin: var(--k-space-3) 0; }
.opinion-panel input { width: 100%; }

/* --- El mapa para poner el pin -----------------------------------------------
   Escrito a mano, sin librería: son tres cosas —arrastrar, acercar y saber
   dónde quedó el centro— y no valen 140 KB de código de otros ni un dominio
   ajeno en la consola de un negocio. Las teselas son de OpenStreetMap, y su
   licencia pide que la atribución se vea: por eso `.mapa-credito` no se puede
   quitar. */

.mapa-caja {
  position: relative;
  height: 320px;
  margin-top: var(--k-space-2);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-lg);
  overflow: hidden;
  background: var(--k-surface);
  /* Sin esto, arrastrar el mapa en un teléfono rueda la página debajo y el
     pin nunca llega a su sitio. */
  touch-action: none;
  cursor: grab;
  user-select: none;
}
.mapa-caja.agarrando { cursor: grabbing; }
.mapa-caja:focus-visible { outline: 2px solid var(--k-brand); outline-offset: 2px; }

.mapa-lienzo { position: absolute; inset: 0; }
.mapa-lienzo img {
  position: absolute;
  top: 0; left: 0;
  width: 256px; height: 256px;
  /* Las teselas son fotos de un mundo con luz de día: en modo oscuro no se
     invierten, porque un mapa en negativo no lo reconoce nadie. */
  pointer-events: none;
}

/*
  El pin, clavado en el centro exacto.

  `translate(-50%, -100%)` porque la punta de la gota es lo que señala, no su
  centro: sin eso el pin apunta medio centímetro más arriba de donde dice.
*/
.mapa-pin {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -100%);
  width: 34px; height: 34px;
  color: var(--k-brand);
  pointer-events: none;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .35));
}
.mapa-pin svg { width: 100%; height: 100%; }

.mapa-zoom {
  position: absolute;
  top: var(--k-space-3); right: var(--k-space-3);
  display: grid;
  gap: 1px;
}
.mapa-zoom button {
  width: 34px; height: 34px;
  padding: 0;
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-sm);
  background: var(--k-surface);
  color: var(--k-text);
  font-size: 1.1rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}

.mapa-credito {
  position: absolute;
  bottom: 0; right: 0;
  margin: 0;
  padding: 2px var(--k-space-2);
  background: color-mix(in srgb, var(--k-surface) 85%, transparent);
  border-top-left-radius: var(--k-radius-sm);
  font: var(--k-small) var(--k-font-sans);
  color: var(--k-text-muted);
}
.mapa-credito a { color: inherit; }

/* Los resultados de la búsqueda: una lista corta debajo del campo, no un menú
   flotante. Flotando taparía el mapa, que es justo lo que hay que mirar. */
.mapa-resultados {
  display: grid;
  margin-top: var(--k-space-2);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-md);
  overflow: hidden;
}
.mapa-resultados button {
  padding: var(--k-space-3) var(--k-space-4);
  border: 0;
  border-bottom: 1px solid var(--k-border);
  background: var(--k-surface);
  color: var(--k-text);
  text-align: left;
  font: var(--k-body);
  cursor: pointer;
}
.mapa-resultados button:last-child { border-bottom: 0; }
.mapa-resultados button:hover { background: var(--k-surface-raised); }

/* --- El saldo en la ficha del cliente ----------------------------------------
   Un número grande y solo: se mira una vez y se entiende. Es lo que contesta la
   pregunta que se hace por teléfono con la persona esperando. */

/* Rejilla y no flex envolvente. Con `flex-wrap`, tres tarjetas en un teléfono
   salían dos arriba y una sola abajo estirada a todo el ancho, que se lee como
   maquetación rota y no como una fila incompleta. `auto-fit` reparte lo que
   hay entre las columnas que caben, y las tres caben. */
.saldos-ficha {
  display: grid;
  /* DOS columnas fijas, no `auto-fit`.

     Con `auto-fit` y cuatro tarjetas —gastado, por visita, puntos, sellos— en
     un cajón de 420px entraban tres arriba y una sola abajo, y a ese ancho
     "$12,450.00" se partía en dos renglones: "$12,45" y "0.00", que no es una
     cifra pequeña sino una cifra ilegible. Dos columnas dan el ancho que un
     importe necesita y reparten cuatro tarjetas en un 2x2 limpio. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--k-space-3);
}
.saldo-ficha {
  padding: var(--k-space-4);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-lg);
  background: var(--k-surface);
  text-align: center;
}
.saldo-ficha .cifra {
  display: block;
  font-weight: 700;
  line-height: 1.1;
  /* Encoge para un importe y se queda grande para "120 puntos". A 1.9rem fijos,
     "$12,450.00" no cabía en su caja y se cortaba a la mitad — "$12,450.0",
     que es peor que una cifra pequeña: es una cifra equivocada. */
  font-size: clamp(1.15rem, .7rem + 2vw, 1.7rem);
  font-variant-numeric: tabular-nums;
  /* Y si aun así no cabe, se encoge la letra antes que recortarse. */
  overflow-wrap: anywhere;
}
.saldo-ficha .que { color: var(--k-text-muted); font: var(--k-small) var(--k-font-sans); }

/* --- Lo que se ha probado ----------------------------------------------------
   Rejilla de caras, no lista: lo que se busca aquí es reconocer un look de un
   vistazo, y para eso hace falta verlo, no leer su fecha. */

/* Cada prueba es un PAR: el antes y el después, juntos. Sin el antes, el
   después es la foto de una persona rubia y no la prueba de un cambio. */
.ficha-pruebas {
  display: grid;
  gap: var(--k-space-4);
  margin-top: var(--k-space-3);
}
.prueba-par {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--k-space-2);
  align-items: start;
}
/* Con una sola foto —las hechas antes de que se guardara el origen— ocupa el
   ancho entero en vez de dejar media rejilla vacía al lado. */
.prueba-par:has(figure:only-of-type) { grid-template-columns: 1fr; }

.prueba-par figure { margin: 0; }
.prueba-par img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: var(--k-radius-md);
  border: 1px solid var(--border);
  /* Un fondo bajo la foto: mientras carga, el hueco es una superficie y no un
     rectángulo transparente con el borde flotando. */
  background: var(--field);
}
.prueba-par figcaption {
  margin-top: var(--k-space-1);
  color: var(--muted);
  font: var(--k-small) var(--k-font-sans);
  text-align: center;
}
/* El pie del par: la fecha una sola vez —repetida bajo cada foto parecen dos
   pruebas del mismo día— y el botón de marcar cuál se hizo. */
.prueba-pie {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--k-space-2);
}
.prueba-fecha { color: var(--muted); font: var(--k-small) var(--k-font-sans); }

.prueba-elegir {
  min-height: 32px;
  padding: 0 var(--k-space-3);
  border: 1px solid var(--border);
  border-radius: var(--k-radius-pill);
  background: var(--panel);
  color: var(--muted);
  font: 600 12px/1 var(--k-font-sans);
}
.prueba-elegir[aria-pressed="true"] {
  border-color: var(--ok);
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  color: var(--ok);
}

/* La elegida se marca en la foto, no sólo en su botón: quien mira el historial
   está mirando las CARAS, y una diferencia que sólo vive en un botón de doce
   píxeles no se ve al pasar. */
.prueba-par.elegida img { border-color: var(--ok); border-width: 2px; }

/* --- Las pestañas de la ficha ------------------------------------------------
   Era un solo desplazamiento con todo apilado: para llegar a lo que se ha
   probado había que recorrer cuatro tablas, y quien abre la ficha con la
   clienta delante no tiene tiempo de recorrer nada. */

.ficha-tabs {
  margin: var(--k-space-5) 0 var(--k-space-4);
  /* En un cajón de 420px las cuatro no caben en fila: se envuelven, y con el
     borde de abajo se leen como pestañas y no como botones sueltos. */
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--k-space-3);
}
.ficha-panel { min-height: 80px; }

/* --- Las notas de la ficha ---------------------------------------------------
   Con fecha y firma. Sin ellas, "alérgica al amoníaco" es un dato que nadie se
   atreve a usar ni a borrar. */

.nota-nueva {
  display: flex;
  gap: var(--k-space-2);
  align-items: flex-start;
  margin-top: var(--k-space-3);
}
.nota-nueva textarea { flex: 1 1 auto; resize: vertical; }
/* El botón a la altura de la primera línea del campo, no estirado a su alto:
   un botón de 60px de alto al lado de un área de texto parece un error. */
.nota-nueva button { flex: none; }

.notas { list-style: none; margin: var(--k-space-3) 0 0; padding: 0; }
.notas li {
  position: relative;
  padding: var(--k-space-3) var(--k-space-8) var(--k-space-3) var(--k-space-3);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-md);
  background: var(--k-surface);
}
.notas li + li { margin-top: var(--k-space-2); }
.notas li p { margin: 0 0 var(--k-space-1); white-space: pre-wrap; }
/* La equis arriba a la derecha y sin fondo: es la salida de emergencia de una
   nota, no una acción que haya que ofrecer con el mismo peso que apuntarla. */
.notas li [data-borrar-nota] {
  position: absolute;
  top: var(--k-space-2);
  right: var(--k-space-2);
}

/* --- Kabelo AI, desde el mostrador -------------------------------------------
   El reparto lo decide `data-listo`, que el script pone cuando hay algo que
   enseñar. Sin nada generado los pasos se llevan el ancho útil y el lienzo es
   una franja; con resultado, el lienzo se lleva la mitad.

   Antes el lienzo pedía media pantalla desde el principio para dibujar un
   recuadro vacío, y las opciones —lo único que se puede hacer todavía—
   quedaban apretadas en una columna estrecha. */

.sim-grid { display: grid; gap: var(--k-space-4); align-items: start; }

@media (min-width: 1024px) {
  /* Sin resultado: los pasos anchos y el lienzo a un lado, pequeño. */
  .sim-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 300px); }
  .sim-grid[data-listo] { grid-template-columns: minmax(0, 460px) minmax(0, 1fr); }

  /* El lienzo se queda quieto mientras se recorre la columna de opciones: sin
     esto, elegir el efecto de color deja el resultado fuera de la pantalla. */
  .sim-lienzo { position: sticky; top: var(--k-space-4); }

  /* Con sitio de sobra, las opciones se reparten en dos columnas en vez de
     estirarse a un ancho en el que un `<select>` mide 700px. */
  .sim-grid:not([data-listo]) .sim-cuerpo .row > div { flex: 1 1 240px; }
}

/* En teléfono se apilan, y el resultado va PRIMERO en cuanto existe: quien
   sostiene el teléfono lo gira para enseñarlo, no para leer el formulario. */
@media (max-width: 1023px) {
  .sim-grid[data-listo] .sim-form { order: 2; }
  .sim-grid[data-listo] .sim-lienzo { order: 1; }
}

/* --- Los pasos, en acordeón --------------------------------------------------
   Sólo uno abierto. Con los cinco desplegados no caben en pantalla ni en
   escritorio, y quince campos a la vez no dicen por dónde empezar. */

.sim-pasos { margin-top: var(--k-space-2); }

.sim-paso { border-top: 1px solid var(--border); }
.sim-paso:first-child { border-top: 0; }

.sim-cabeza {
  display: flex;
  align-items: center;
  gap: var(--k-space-3);
  width: 100%;
  padding: var(--k-space-3) 0;
  background: none;
  border: 0;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  min-height: 52px;
}
.sim-cabeza:hover:not(:disabled) { filter: none; }

.sim-num {
  display: grid;
  place-items: center;
  flex: none;
  width: 24px; height: 24px;
  border-radius: var(--k-radius-pill);
  background: var(--field);
  color: var(--muted);
  font: 600 11px/1 var(--k-font-sans);
  font-variant-numeric: tabular-nums;
}
/* El paso abierto marca su número: es la única señal de dónde está uno cuando
   los otros cuatro están cerrados y se ven iguales. */
.sim-paso[data-abierto] .sim-num { background: var(--copper); color: #1F1503; }

.sim-titulo { flex: none; font: 600 14px/20px var(--k-font-sans); }

/* El resumen de lo elegido, para poder leer el paso cerrado. Un acordeón sin
   esto obliga a abrir los cinco para saber qué se pidió, que es justo el
   problema que venía a resolver. */
.sim-eco {
  flex: 1 1 auto;
  min-width: 0;
  text-align: right;
  color: var(--muted);
  font: var(--k-small) var(--k-font-sans);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sim-eco.pendiente { color: var(--k-warning); }

.sim-cabeza svg {
  flex: none;
  width: 16px; height: 16px;
  color: var(--muted);
  transition: transform var(--k-fast) ease;
}
.sim-paso[data-abierto] .sim-cabeza svg { transform: rotate(180deg); }

.sim-cuerpo { display: none; padding: 0 0 var(--k-space-4); }
.sim-paso[data-abierto] .sim-cuerpo { display: block; }

@media (prefers-reduced-motion: reduce) {
  .sim-cabeza svg { transition: none; }
}

/* --- De quién es -------------------------------------------------------------
   Las citas de hoy primero: quien está en la silla ahora mismo casi siempre
   tiene cita hoy, y buscarla por nombre teniéndola a la vista es teclear lo que
   ya está en pantalla. */

.sim-etiqueta {
  margin: 0 0 var(--k-space-2);
  color: var(--muted);
  font: var(--k-small) var(--k-font-sans);
  text-transform: uppercase;
  letter-spacing: .04em;
}

.sim-citas { display: flex; flex-wrap: wrap; gap: var(--k-space-2); }
.sim-cita {
  display: flex;
  align-items: baseline;
  gap: var(--k-space-2);
  padding: var(--k-space-2) var(--k-space-3);
  border: 1px solid var(--border);
  border-radius: var(--k-radius-pill);
  background: var(--panel);
  color: var(--text);
  font: var(--k-body) var(--k-font-sans);
  min-height: 40px;
}
.sim-cita span { color: var(--muted); font: var(--k-small) var(--k-font-sans); }
.sim-cita.on {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}
.sim-cita.on span { color: inherit; opacity: .8; }

.sim-hoy { margin-bottom: var(--k-space-4); }

/* El buscador. Un salón con quinientos clientes tiene una lista de quinientos
   renglones: encontrar a alguien ahí es desplazarse, no elegir. */
.sim-buscador { position: relative; }
.sim-buscador input { width: 100%; }

.sim-resultados {
  position: absolute;
  z-index: 5;
  left: 0; right: 0;
  margin-top: 4px;
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--k-radius-md);
  background: var(--panel);
  box-shadow: var(--shadow-lg);
}
.sim-resultados button {
  display: flex;
  align-items: baseline;
  gap: var(--k-space-2);
  width: 100%;
  padding: var(--k-space-3);
  background: none;
  border: 0;
  border-radius: 0;
  color: var(--text);
  text-align: left;
  min-height: 44px;
}
.sim-resultados button + button { border-top: 1px solid var(--border); }
.sim-resultados button:hover { background: var(--field); filter: none; }
.sim-resultados span { color: var(--muted); font: var(--k-small) var(--k-font-sans); }

.sim-elegida {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--k-space-2);
  margin: var(--k-space-3) 0 0;
  padding: var(--k-space-2) var(--k-space-2) var(--k-space-2) var(--k-space-3);
  border-radius: var(--k-radius-md);
  background: var(--field);
  font-weight: 600;
}

/* --- Subir la foto ---------------------------------------------------------- */

.sim-soltar {
  display: grid;
  justify-items: center;
  gap: var(--k-space-1);
  width: 100%;
  padding: var(--k-space-6) var(--k-space-4);
  border: 1px dashed var(--border);
  border-radius: var(--k-radius-lg);
  background: var(--field);
  color: var(--text);
  font: var(--k-body) var(--k-font-sans);
  cursor: pointer;
  transition: border-color var(--k-fast) ease;
}
.sim-soltar:hover { border-color: var(--accent); filter: none; }
.sim-soltar svg { width: 24px; height: 24px; color: var(--muted); }

/* --- Ayuda para la foto ------------------------------------------------------
   Dos piezas y dos pesos distintos, a propósito.

   Los REPAROS hablan de la foto que acaban de elegir, así que se ven; los
   CONSEJOS son generales, así que van plegados. Al revés —consejos siempre
   abiertos y reparos discretos— se lee el sermón y se ignora lo que importa. */

.foto-reparos {
  margin-top: var(--k-space-3);
  padding: var(--k-space-3);
  border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
  border-radius: var(--k-radius-md);
  /*
    Ámbar, nunca rojo. El rojo de esta consola significa "no puedes seguir", y
    aquí sí se puede: el botón de generar no se toca. Un aviso pintado de error
    detiene a quien no tenía por qué detenerse.
  */
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}
.foto-reparos strong {
  display: block;
  font: var(--k-label) var(--k-font-sans);
  color: var(--text);
}
.foto-reparos ul {
  margin: var(--k-space-2) 0 0;
  padding-left: 1.1em;
  display: grid;
  gap: var(--k-space-2);
}
.foto-reparos li { font: var(--k-small) var(--k-font-sans); color: var(--muted); }

.foto-consejos { margin-top: var(--k-space-2); }
.foto-consejos summary {
  /* 44px de zona sensible: en un teléfono el triangulito de un `summary` mide
     lo que mide el texto, y aquí el texto es una línea de 12px. */
  padding: var(--k-space-2) 0;
  min-height: 44px;
  display: flex;
  align-items: center;
  font: var(--k-small) var(--k-font-sans);
  color: var(--muted);
  cursor: pointer;
}
.foto-consejos summary:hover { color: var(--text); }
.foto-consejos ul {
  margin: 0 0 var(--k-space-2);
  padding: 0 0 0 var(--k-space-3);
  list-style: none;
  border-left: 2px solid var(--border);
  display: grid;
  gap: var(--k-space-3);
}
.foto-consejos strong {
  display: block;
  font: var(--k-label) var(--k-font-sans);
  color: var(--text);
}
.foto-consejos span {
  display: block;
  margin-top: 2px;
  font: var(--k-small) var(--k-font-sans);
  color: var(--muted);
}

/* --- Los tintes --------------------------------------------------------------
   Muestras de color y no una lista desplegable: "Rubio Cenizo" y "Rubio Dorado"
   son dos palabras parecidas y dos colores muy distintos, y quien elige está
   mirando a una persona, no leyendo. */

.sim-tintes {
  display: grid;
  /* 72px y no 58: a 58 caben más muestras por fila y ningún nombre entero.
     "Castaño Claro / Miel" cortado a "Castaño" no distingue de "Castaño
     Oscuro", que es la muestra de al lado. */
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: var(--k-space-2);
}
.sim-tinte {
  display: grid;
  justify-items: center;
  gap: 4px;
  padding: 4px;
  border: 2px solid transparent;
  border-radius: var(--k-radius-md);
  background: none;
  min-height: 0;
  cursor: pointer;
}
.sim-tinte:hover { filter: none; }
.sim-tinte span {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--k-radius-sm);
  border: 1px solid var(--border);
}
/* "Tono Actual" no es un color: una diagonal de "ninguno", como la del selector
   de color de cualquier editor. A rayas de `--field` sobre `--panel` salía
   blanca, y se leía como el tinte blanco — que es uno que alguien podría querer
   de verdad. */
.sim-tinte span[data-actual] {
  background:
    linear-gradient(to top right,
      transparent calc(50% - 1px), var(--muted) calc(50% - 1px),
      var(--muted) calc(50% + 1px), transparent calc(50% + 1px)),
    var(--field);
}
.sim-tinte em {
  font: 10px/1.2 var(--k-font-sans);
  font-style: normal;
  text-align: center;
  color: var(--muted);
  /* Dos renglones y punto: el nombre más largo no puede empujar la rejilla. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.sim-tinte.on { border-color: var(--accent); }
.sim-tinte.on em { color: var(--text); font-weight: 600; }

/* Los deslizadores salían del azul del sistema operativo, que es de las cosas
   que delatan una pantalla sin terminar. 24px de alto para agarrarlos. */
.sim-cuerpo input[type="range"] {
  width: 100%;
  height: 24px;
  accent-color: var(--accent);
}

.sim-valor {
  color: var(--muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

/* --- El botón de generar ----------------------------------------------------- */

.sim-generar {
  width: 100%;
  margin-top: var(--k-space-4);
  /* El cobre, que esta hoja reserva por escrito para "acciones premium y
     momentos de Kabelo AI" (ver `button.accent` arriba). Es literalmente eso:
     la única acción de la pantalla que gasta un crédito. */
  background: var(--copper);
  color: #1F1503;
  min-height: 52px;
  border-radius: var(--k-radius-lg);
  font-size: 15px;
  font-weight: 700;
}
.sim-generar:disabled {
  background: var(--field);
  color: var(--muted);
  cursor: not-allowed;
}

/* --- El visor ---------------------------------------------------------------- */

.sim-visor {
  display: grid;
  place-items: center;
  /* Una franja mientras no hay nada. Crece sólo cuando hay foto: un recuadro
     vacío del tamaño de una foto ocupa el sitio de lo que sí se puede hacer. */
  min-height: 120px;
  padding: var(--k-space-3);
  border-radius: var(--k-radius-lg);
  background: var(--field);
}
.sim-grid[data-listo] .sim-visor { min-height: 320px; }
@media (min-width: 1024px) {
  .sim-grid[data-listo] .sim-visor { min-height: 520px; }
}

.sim-foto, .sim-comparar img, .sim-cargando img {
  display: block;
  max-width: 100%;
  max-height: 60vh;
  border-radius: var(--k-radius-md);
  object-fit: contain;
}

.sim-vacio {
  display: grid;
  justify-items: center;
  gap: var(--k-space-1);
  margin: 0;
  text-align: center;
  color: var(--muted);
}
.sim-vacio svg { width: 28px; height: 28px; opacity: .45; }

/* Antes y después a la vez, no un tirador: en el mostrador el teléfono lo
   sostiene alguien y la clienta mira desde el otro lado de la silla. */
.sim-comparar {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--k-space-2);
  width: 100%;
}
.sim-comparar figure { margin: 0; display: grid; gap: var(--k-space-1); justify-items: center; }
.sim-comparar figcaption { color: var(--muted); font: var(--k-small) var(--k-font-sans); }

/* Mientras genera se sigue viendo la foto, atenuada: una pantalla que se queda
   en blanco durante quince segundos parece rota. */
.sim-cargando { position: relative; display: grid; place-items: center; }
.sim-cargando img { opacity: .35; }
.sim-velo {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--k-space-2);
  text-align: center;
}
.sim-velo strong { color: var(--text); }

.spinner {
  width: 28px; height: 28px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: sim-girar .9s linear infinite;
}
@keyframes sim-girar { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 3s; }
}

.sim-acciones {
  display: flex;
  gap: var(--k-space-2);
  flex-wrap: wrap;
  margin-top: var(--k-space-3);
}
.sim-acciones button { flex: 1 1 140px; }

/*
  Los títulos de las secciones de la página pública.

  Diez campos seguidos en una columna se leen como un formulario largo y
  aburrido; en dos o tres columnas se leen como una lista de secciones, que es
  lo que son. `auto-fill` con un mínimo de 15rem decide solas cuántas caben,
  sin una media query por tamaño de pantalla.
*/
.titulos-secciones {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--k-space-3);
}
.titulo-seccion label {
  /* La etiqueta es el nombre de fábrica y el marcador del campo dice lo mismo:
     arriba en pequeño para que no compita con lo que la persona escriba. */
  font-size: .8rem;
  opacity: .85;
}
.titulo-seccion .sub {
  margin: .25rem 0 0;
  font-size: .75rem;
}

/*
  Los mensajes, agrupados por cuándo se mandan.

  Dos de ellos son la primera y la última línea del MISMO WhatsApp. En una lista
  plana nadie entendería que se leen juntos ni en qué orden; con el grupo
  delante, el par se lee como lo que es.
*/
.mensaje-grupo + .mensaje-grupo { margin-top: var(--space-5, 1.25rem); }
.mensaje-cuando {
  margin: 0 0 .4rem;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted, #6b7280);
}

/*
  Un número que pide que lo miren, dentro de una tabla.

  Las generaciones fallidas de un negocio no son un error del sistema: son la
  señal de que ahí pasa algo —fotos malas, un modelo que no le funciona— y de
  que hay que ir a ver. En el mismo gris que el resto de la fila, ese negocio no
  existe hasta que alguien suma las columnas a mano.

  Color y no fondo: es un dato de una tabla, no una alarma. Con pastilla
  competiría con el resto de la fila y la tabla se leería como un tablero de
  incidencias.
*/
.warn-text { color: var(--warn); font-weight: 700; }

/* Dos acciones en la misma celda de una tabla: en fila y con aire, no pegadas.
   Pegadas se tocan la equivocada con el pulgar, y una de éstas invalida un
   código que la clienta ya tiene apuntado. */
.acciones-fila { display: flex; gap: .4rem; flex-wrap: wrap; }

/*
  Un plan retirado de la venta: apagado, pero legible.

  Se atenúa entero para que la lista se lea de un vistazo —los que están a la
  venta saltan solos— y no se esconde: es la única pantalla desde la que se
  vuelve a activar, y lo que no se ve no se puede devolver.

  Con opacidad y no con un gris fijo: así el texto conserva su contraste
  relativo en los dos temas, en vez de quedar casi blanco sobre blanco en claro.
*/
tr.retirado { opacity: .58; }
tr.retirado:hover { opacity: 1; }

/* Borrar, entre enlaces que no borran nada. El color es la única diferencia
   antes de pulsarlo, porque todos se ven igual de inocentes. */
.peligro-enlace { color: var(--danger); }
