/* ============================================================================
   Juicy — Dashboard Design System · LIGHTBOX + DIALOG
   ----------------------------------------------------------------------------
   Figma: "Lightboxes" (1:2806). Tres piezas:
     Lightbox Main (1:2807)  Size = Small 480 | Medium 800 | Big 1100
     Dialog        (1:2829)  480×207 — la confirmación
     Overlay       (1:2840)  el velo + el Dialog centrado

     <div id="…" class="lightbox lightbox--sm destroy_on_close">
       <div class="lightbox__overlay" data-close-lightbox></div>
       <div class="lightbox__content">
         <div class="lightbox__header">
           <span class="lightbox__title">Editar producto</span>
           <span class="lightbox__close" data-close-lightbox><i class="fi fi-rr-cross"></i></span>
         </div>
         <div class="lightbox__body">…</div>
       </div>
     </div>

   `lightbox`, `lightbox_active` y `destroy_on_close` son CONTRATO CON EL JS
   (main.js abre/cierra y decide si lo destruye). Conservan su nombre, como el
   `bubble_wrapper` de ds/bubble.css: comportamiento y estilo dejan de compartir
   clase. El cierre lo dispara `[data-close-lightbox]`, un atributo — así que la
   clase quedó libre y `close_lightbox` pasa a ser `lightbox__close`.

   AQUÍ NO HAY SIDEBAR. `.lightbox` cubre su contenedor entero; el desplazamiento
   de 70px que el dashboard le mete para no tapar su barra lateral se queda en
   main.css, que es donde vive esa barra. El DS no sabe que existe — es el mismo
   contrato de portabilidad que hace que esto pueda viajar a Electron. Ver nota 1.

   EL LIGHTBOX ADMITE VARIAS CAJAS, y por eso el shell es un CONTENEDOR FLEX y no
   un sitio donde centrar UNA. Las cajas van de hermanas, se centran como GRUPO y
   cada una conserva su propio ancho:

     <div class="lightbox lightbox_active">
       <div class="lightbox__overlay" data-close-lightbox></div>
       <aside class="cobro_panel">…</aside>      ← una caja auxiliar, su ancho
       <div class="lightbox__content">…</div>    ← el modal, su max-width
     </div>

   Hasta 0.9.190 `.lightbox__content` se centraba con `absolute` + `top/left:50%`
   + `translate(-50%,-50%)`, o sea SACADO DEL FLUJO: dos cajas se apilaban una
   encima de otra y la única salida era colocar la segunda a mano (es literalmente
   lo que hacía el panel «Modo de cobro» de Restaurant, con `right:100%`). Ver
   nota 9.

   Y CUANDO EL GRUPO HAY QUE MAQUETARLO, se mete un nivel OPCIONAL entre el shell y
   las cajas para poder usar la rejilla de `ds/grid.css`:

     <div class="lightbox lightbox--lg">
       <div class="lightbox__overlay" data-close-lightbox></div>
       <div class="lightbox__group">        ← lo que se centra en pantalla
         <div class="row">                  ← una o varias
           <div class="col-3">…una o varias cajas…</div>
           <div class="col-9">…una o varias cajas…</div>
         </div>
       </div>
     </div>

   Un lightbox de UNA caja NO lo lleva y no cambia nada: `.lightbox__group` es puro
   añadido. Ver nota 13, que es donde está el porqué de que el TAMAÑO se mude del
   contenido al grupo.
   ============================================================================ */

/* ---- Shell ---------------------------------------------------------------- */

.lightbox {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  /* Dónde empieza el área del modal. El DS no sabe que hay un sidebar: el HOST
     lo dice (el dashboard le cede 70px al suyo desde main.css, y sus modales
     bloqueantes lo devuelven a 0 redefiniendo esta variable). Ver nota 1.
     Es variable y no un valor por dos razones: el DS carga DESPUÉS de main.css,
     así que un `left:0` aquí le ganaría al offset del producto — y así el host
     no necesita pelear especificidad para decir algo que es suyo. */
  left: var(--lightbox-inset-start, 0px);
  width: calc(100% - var(--lightbox-inset-start, 0px));
  z-index: 99;

  /* EL CHASIS. Centra el GRUPO de cajas, no una caja. Cada hijo en flujo (el
     velo no lo está) es una caja que conserva su propio ancho: `flex: 0 1 auto`
     de serie, o sea "tu tamaño, y solo encoges si no cabes". Que sea el
     CONTENEDOR quien centra es lo que permite que haya dos. Ver nota 9.

     `flex-wrap: wrap` + `align-content: center` no hacen nada con UNA caja
     (medido: diff 0 contra el `absolute` de antes) y son lo que evita que dos
     cajas se aplasten cuando la pantalla no da: se parten en dos filas y el
     grupo sigue centrado.

     NO lleva `height: 100vh`. Con `top/right/bottom: 0` y `position: fixed` ya
     mide la pantalla entera, y `100vh` además MIENTE en móvil (no descuenta la
     barra de direcciones, que es justo el caso en el que `100vh` se nota). El
     inset basta: se midió antes de escribirlo. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  align-content: center;
  justify-content: center;
  gap: var(--lightbox-gap, 15px);

  /* EL TAMAÑO ES DEL LIGHTBOX, NO DE LA CAJA (0.9.194). Ver nota 13.
     Se declaran AQUÍ, en el shell, y no en `.lightbox__content`, por dos razones:
       · un lightbox ANIDADO vuelve a partir del default en vez de heredar el tamaño del
         de fuera. `gwfield_lightbox` emite un `.lightbox` por cada campo de tipo lightbox,
         DENTRO del formulario, así que la forma existe aunque hoy no se dé: contados en el
         navegador, 0 anidados en `/productos/editar`, `/productos/agregar` y
         `/ajustes-de-cuenta`. Sin esta línea, el día que un formulario así se abra dentro
         de un modal `--sm`, el de dentro se encogería a 480 sin que nadie lo pidiera;
       · `.lightbox__group` sólo tiene que cambiar DOS variables para quedarse el tope. */
  --lightbox-size: 800px;                        /* Figma: Size=Medium. Es el default. */
  --lightbox-box-width: calc(100% - 40px);       /* el margen contra la pantalla */
  --lightbox-box-max: var(--lightbox-size);

  opacity: 0;
  visibility: hidden;
  transition: opacity .5s, visibility .5s;
}

.lightbox.lightbox_active {
  opacity: 1;
  visibility: visible;
}

/* El velo. Charcoal al 70%, que es lo que mide el Figma (despejado del render:
   #22333B sobre blanco al 70% da exactamente el (101,113,118) que pinta).
   El producto lo tenía al 80% — y su diálogo, a un charcoal DISTINTO. Nota 4. */
.lightbox__overlay {
  position: absolute;
  inset: 0;
  /* DEBAJO DE TODA CAJA DEL GRUPO, esté posicionada o no. Ver nota 11: sin esto, una
     caja en flujo SIN `position` queda tapada por el velo —que sí está posicionado— y
     sus clics los recoge el velo, que cierra el modal. Con `-1` el velo se pinta antes
     que el contenido en flujo, sigue cubriendo lo mismo y sigue recibiendo los clics
     donde no hay ninguna caja encima. Los tres, medidos. */
  z-index: -1;
  background: color-mix(in srgb, var(--brand-charcoal) 70%, transparent);
}

.lightbox__content {
  /* EN FLUJO. Quien centra es el chasis (nota 9). Sigue POSICIONADO —`relative`—
     porque es el bloque contenedor de lo que se ancle dentro, y eso no puede
     cambiar: `.lightbox__body` tiene `overflow:auto`, así que lo que se posiciona
     contra ESTA caja es justamente lo que puede salirse del cuerpo sin que lo
     recorten. */
  position: relative;

  /* Sin grupo esto vale `calc(100% - 40px)` / el tamaño del lightbox — o sea,
     EXACTAMENTE lo de antes. Dentro de un `.lightbox__group` el margen contra la
     pantalla y el tope los pone el grupo, y la caja llena su columna. Nota 13. */
  width: var(--lightbox-box-width);
  max-width: var(--lightbox-box-max);
  /* Un ítem flex no encoge por debajo de su contenido salvo que se le diga. Con
     UNA caja no se llega a encoger nunca (sobran los 40px), así que esto NO
     cambia lo de hoy; con dos, es lo que evita que la fila reviente. */
  min-width: 0;
  background: var(--surface-page);
  border-radius: var(--radius-md);
}

/* ---- Size ----------------------------------------------------------------- */

/* MIDEN EL LIGHTBOX, no la caja: con una sola caja el lightbox ES la caja y estos
   tres valores son los mismos de siempre; con `.lightbox__group` el tope se lo
   queda el grupo y las cajas se reparten su interior por la rejilla. Nota 13.

   Van DESPUÉS del bloque `.lightbox` a propósito: son la MISMA especificidad
   (0,1,0) sobre el MISMO elemento, así que quien gana es el orden del archivo. */
.lightbox--sm { --lightbox-size: 480px; }        /* Figma: Size=Small */

/* Figma: Size=Medium. Coincide con el default, pero como clase EXPLÍCITA: un modal
   ancho (p.ej. el checkout horizontal) la pide por nombre en vez de depender de él. */
.lightbox--m  { --lightbox-size: 800px; }

.lightbox--lg { --lightbox-size: 1100px; }       /* Figma: Size=Big. Ver nota 3. */

/* ---- Grupo (OPCIONAL) ------------------------------------------------------ */

/* EL NIVEL PARA MAQUETAR CON LA REJILLA. Es opcional y tiene que seguir siéndolo:
   los 48 lightboxes que existen hoy no lo llevan y no se tocan. Nota 13.

     <div class="lightbox lightbox--lg">
       <div class="lightbox__overlay" data-close-lightbox></div>
       <div class="lightbox__group">
         <div class="row">
           <div class="col-3"><aside class="cobro_panel">…</aside></div>
           <div class="col-9"><div class="lightbox__content">…</div></div>
         </div>
       </div>
     </div>

   NO lleva `position` ni `z-index`, y no los necesita: el velo va en `z-index:-1`
   (nota 11), o sea en el paso 3 del pintado del shell, ANTES que todo lo demás de
   ese contexto — posicionado o no, a la profundidad que sea. Así que una caja a tres
   niveles (grupo → fila → columna → caja) queda encima sin declarar nada.
   Comprobado con `elementFromPoint` sobre una caja neutra, y comprobado también el
   sentido contrario: añadirle aquí `position:relative; z-index:0` NO rompe nada (el
   contexto que crearía ordena a sus hijos ENTRE SÍ, y el grupo entero sigue pintándose
   después del velo). Se deja fuera por no declarar lo que no hace falta, no por miedo. */
.lightbox__group {
  width: calc(100% - 40px);
  max-width: var(--lightbox-size);
  min-width: 0;

  /* El margen contra la pantalla y el tope pasan a ser del GRUPO: dentro, la caja
     llena su columna. Sin esto una caja en una `col-3` saldría 40px más estrecha
     que su columna (su margen de pantalla, repetido) y topada a 800 (el tope del
     lightbox entero, aplicado a un tercio de él). */
  --lightbox-box-width: 100%;
  --lightbox-box-max: none;
}

/* LAS CAJAS, PEGADAS ARRIBA. `.row` no declara `align-items`, así que las columnas
   se estiran y una caja-bloque de dentro ya queda arriba por sí sola — pero SOLO
   mientras la caja esté DENTRO de la columna. En cuanto alguien escriba
   `<div class="col-3 lightbox__content">` (que es la forma corta y evidente), la
   caja ES el ítem flex y `stretch` la estira hasta el alto de la fila más alta.
   Se declara para que la promesa valga en las dos formas. Va acotado al grupo: la
   rejilla del resto del producto no se toca.

   Y EL HUECO ENTRE CAJAS SIGUE SIENDO EL DEL LIGHTBOX, no el de la rejilla. Sin
   estas dos líneas el grupo se come el `gap` del shell —las cajas dejan de ser
   ítems flex suyos y pasan a ser una sola caja— y lo que queda es el gutter de
   `ds/grid.css`: 24px en horizontal y **0 en vertical**, porque `.row` nace con
   `--juicy-gutter-y: 0`. Medido en el cobro de Restaurant a 1199px: el panel
   acababa pegado a la cabecera del modal, sin un pixel de separación, cuando antes
   había 15. Lo levantó la revisión adversaria. Se usa la MISMA variable que el
   `gap` del shell para que el hueco no dependa de si el grupo existe o no. */
.lightbox__group > .row {
  align-items: flex-start;
  --juicy-gutter-x: var(--lightbox-gap, 15px);
  --juicy-gutter-y: var(--lightbox-gap, 15px);
}

/* ---- Header ---------------------------------------------------------------- */

.lightbox__header {
  display: flex;
  align-items: center;
  justify-content: space-between;

  height: 55px;                     /* Figma. El producto tenía 50. */
  padding-left: 24px;
  background: var(--surface-sunken-light);   /* nota 5 */
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}

/* Sin X que empujar a la derecha, el título va centrado. 9 lightboxes lo usan
   (los que no se cierran con la cruz sino con sus propios botones): antes lo
   pedían con `d-flex justify-content-center` a mano en cada uno. */
.lightbox__header--center {
  justify-content: center;
  padding-inline: 24px;
}

.lightbox__title {
  margin: 0;
  font-size: 16px;                  /* nota 6 */
  font-weight: 500;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-primary);
}

.lightbox__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;

  width: 55px;
  height: 55px;
  border-radius: 0 var(--radius-md) 0 0;
  color: var(--icon-field);
  cursor: pointer;
  transition: background .3s, color .3s;

  /* El cierre debería ser un `<button>` y no el `<span>` con el que nació: un span no se
     alcanza con Tab ni responde a Enter, así que el modal solo se cerraba con el ratón. Este
     reset es lo que permite las dos formas — sin él, un button sale con el borde y el fondo del
     navegador (medido en el front nuevo: un recuadro gris alrededor de la cruz). No cambia nada
     para los spans que ya existen. */
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
}

.lightbox__close > i {
  font-size: 15px;
  line-height: 1;
}

.lightbox__close:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

.lightbox__body {
  padding: 25px;
  max-height: calc(100dvh - 150px);
  overflow: auto;
}

/* ---- Dialog ---------------------------------------------------------------- */

/* Figma: "Dialog" (1:2829), 480 de ancho. Es la confirmación destructiva, y NO
   es un `.lightbox__content` con otro nombre: no tiene header, ni X, ni cuerpo
   con scroll — es una tarjeta con título, mensaje y dos botones.
   Lo construyen main.js y pos.js (no las plantillas).

     <div id="…" class="lightbox lightbox--dialog destroy_on_close">
       <div class="lightbox__overlay" data-close-lightbox></div>
       <div class="dialog">
         <h3 class="dialog__title">Vaciar papelera</h3>
         <div class="dialog__body"><p>…</p></div>
         <div class="dialog__footer">
           <button class="btn btn--outline tone-neutral-lv2">Cancelar</button>
           <button class="btn tone-error">Vaciar</button>
         </div>
       </div>
     </div> */

.dialog {
  /* En flujo, como `.lightbox__content` y por lo mismo: lo centra el chasis. */
  position: relative;

  display: flex;
  flex-direction: column;
  gap: 16px;

  width: 480px;                     /* Figma. El producto tenía 380. */
  max-width: calc(100% - 40px);
  min-width: 0;
  padding: 24px;
  background: var(--surface-page);
  border-radius: var(--radius-md);  /* el producto usaba 8 — fuera de escala */
  box-shadow: var(--shadow-lg);
}

.dialog__title {
  margin: 0;
  font-size: 18px;
  font-weight: 600;                 /* nota 7 */
  line-height: 1.3;
  text-align: center;               /* Figma lo centra; el producto no. */
  color: var(--text-primary);
}

.dialog__body {
  font-size: 14px;
  line-height: 1.5;
  text-align: center;
  color: var(--text-secondary);
}

.dialog__body p        { margin: 0; }
.dialog__body p + p    { margin-top: 8px; }

.dialog__footer {
  display: flex;
  gap: 10px;
  justify-content: center;
  width: 100%;
}

/* ============================================================================
   NOTAS

   1. EL DS NO SABE QUE HAY UN SIDEBAR. `.lightbox` traía `left:70px; width:
      calc(100% - 70px)` — o sea, el ancho de la barra lateral del dashboard
      metido dentro de un componente. Y como eso obligaba a des-hacerlo para los
      modales que SÍ tapan todo, había dos parches más (`.allblock_lightbox`,
      `.partialblock_lightbox`) cuyo trabajo era devolverlo a `left:0`.
      Aquí el componente cubre su contenedor y ya. El offset del sidebar se queda
      en main.css, junto al sidebar. Con eso, los dos parches se vuelven
      innecesarios en el DS (siguen en main.css mientras el sidebar viva ahí).

   2. LAS DOS PIEZAS SON DISTINTAS, y el producto ya lo sabía: su
      `.confirm_lightbox` decía en un comentario "Card centrada propia: NO usa el
      .lightbox_content con offset de sidebar". Correcto — pero la solución fue
      CLONAR: una tarjeta a mano, con sus propios botones, sus propios colores y
      su propio overlay. Aquí el Dialog es su propio componente y los botones son
      `.btn` del DS.

   3. TAMAÑOS. El Figma da Small 480 · Medium 800 · Big 1100. El producto tenía
      small 500, medium 600, default 800, big 1000 y full. Contado en el markup:
        small_lightbox   16 usos
        default (800)    el resto
        medium_lightbox   0   ← muerto
        big_lightbox      0   ← muerto
        full_lightbox     0   ← muerto
      O sea que el producto usaba DOS tamaños y tenía CINCO. El default (800)
      coincide exacto con el Medium del Figma; `small` baja de 500 a los 480 del
      Figma. `--lg` (1100) se define porque el DS lo define, sin consumidor.
      Los tres muertos no se migran: mueren.

   4. DOS OVERLAYS PARA UN VELO. El lightbox usaba `rgba(34,51,59,.8)` = el
      charcoal al 80%; el confirm, `rgba(15,24,29,.8)` = OTRO azul-negro que no
      es ningún primitivo. Y `.allblock`/`.partialblock` subían el primero a .95.
      El Figma tiene uno solo: charcoal al 70% (despejado del render por álgebra:
      34·0.7 + 255·0.3 = 101, y mide 101). Aquí hay uno.

   5. EL HEADER NO LLEVA BORDE. El producto le ponía `border-bottom: 1px solid
      #eaedf0` — un literal que no es ningún token, y el Figma no dibuja borde:
      bajo su header hay blanco puro (medido). Se va. El header sí conserva su
      fondo, que ya era `neutral-150` = `surface/sunken-light`.

   6. EL TÍTULO SE QUEDA EN 16, y no porque el Figma lo diga: porque no se puede
      leer de un render. Su cap-height mide 10, que con Montserrat da ~14 — pero
      ±1px de medición mueve esa cuenta a 15 o 16, y no hay variable que
      consultar. Misma regla que el padding del item del menú (ds/bubble.css
      nota 4): si solo se puede estimar, manda lo medido en el producto y se
      anota la duda. Pregunta abierta: ¿el título del lightbox es 14 o 16?

   7. `font-weight: 600` en el título del Dialog, transcrito — PERO el tema no
      carga el 600 de Montserrat (300;400;500;700;900), así que renderiza 700.
      Mismo hueco que en los totales del archive (ds/archive.css nota 6) y en el
      header de notificaciones. Van tres: vale la pena decidirlo de una vez.

   8. LOS ROLES DE LOS BOTONES ESTÁN AL REVÉS ENTRE EL FIGMA Y EL PRODUCTO, y
      aquí manda el producto:
        Figma:     CANCELAR outline ROJO · ACEPTAR relleno oscuro
        Producto:  CANCELAR outline neutro · CONFIRMAR relleno ROJO (is_danger)
      El rojo señala la acción DESTRUCTIVA, y la destructiva es la que confirma,
      no la que cancela. Ponerlo como el Figma haría que "Cancelar" — la salida
      segura — fuera lo que grita. Se conserva el producto y se anota el bug
      allá. (El `is_primary` del producto, para confirmaciones no destructivas,
      apuntaba a `--brand-500`, que tampoco es el primario del DS. Ahora es
      `tone-primary`.)

      CORRECCIÓN (F2f, 2026-07-17) — esta nota afirmó que `--brand-500` "NO
      EXISTE en ningún archivo del producto" y que caía a su fallback `--gw-200`
      (#3787D6). ES FALSO, y el cambio a `tone-primary` fue lo correcto por la
      razón equivocada. `--brand-500` SÍ existe y SÍ resuelve: vale **#2563eb**,
      lo define `[data-brand="pos"]` en `juicy-pos/wp-content/plugins/juicy-pos/
      assets/brand.css`, y el navegador lo confirma (`data-brand="pos"`, la hoja
      cargada, el valor computado). El error fue de método: grepeé `juicy-core`
      buscando `verticals/<vertical>/brand.css`, no lo encontré y concluí "no existe" —
      pero la capa de marca vive en el MÓDULO DEL VERTICAL, que es justo donde
      CLAUDE.md (regla 2) manda que viva. Buscar en un repo y concluir sobre
      cinco. `juicy-core/verticals/` sí existe, con un README y nada más, lo que
      reforzó la conclusión falsa.

      Lo que sí destapó buscarlo bien: hay DOS fuentes de marca que se
      contradicen. `brand.css` dice azul (#2563eb) y el preset del theme.json
      dice carmesí (`--wp--preset--color--custom-500` = #D62A49); donde se
      consulta el preset —el login lo hace— gana el carmesí y el azul del
      vertical no se ve nunca. Cuál manda es una decisión pendiente (§3.b);
      pertenece al modelo de marca (F2e), no aquí.

   9. EL SHELL CENTRA UN GRUPO, NO UNA CAJA (0.9.191).
      ------------------------------------------------------------------------
      `.lightbox__content` y `.dialog` se centraban cada uno por su cuenta con
      `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%)`. Eso
      centra perfectamente **una** caja y hace imposible la segunda: dos cajas
      absolutas con las mismas coordenadas se apilan. El producto ya pagaba ese
      precio — el panel «Modo de cobro» de Restaurant se colgaba del modal con
      `position:absolute; right:100%`, o sea colocado a mano y ajeno al centrado.
      Ahora el shell es `display:flex` + `align-items/justify-content: center` y
      las cajas van EN FLUJO. El grupo se centra; cada caja conserva su ancho.

      QUÉ SE MIDIÓ ANTES DE ESCRIBIRLO (`e2e/foto-estilos.mjs`, dos fotos del
      MISMO lado, que es lo único que puede contestar "¿se movió producción?" —
      el espejo compara PHP contra React y las dos mitades comparten esta hoja,
      así que un cambio de chasis se le escapa por construcción):

        /pos y /usuarios de PHP + /app/pos de React, 7 selectores del lightbox
        → 0 diferencias de `caja.{x,y,w,h}` en las 4 cajas de contenido medidas.
        Lo único que cambia son las propiedades que se cambiaron a mano
        (`position`, `top`, `left`, `transform` en el contenido; `display`,
        `alignItems`, `justifyContent`, `gap`, `flexWrap` en el shell).

      UN CONTENIDO MÁS ALTO QUE LA PANTALLA SE COMPORTA IGUAL, ni mejor ni peor,
      y conviene saberlo: con `translate(-50%,-50%)` sobresalía lo mismo por
      arriba que por abajo y no había forma de llegar a lo de arriba; con
      `align-items:center` pasa exactamente lo mismo (un ítem flex más alto que su
      línea desborda simétrico). No se ha "arreglado" a propósito: hacerlo pide
      `overflow:auto` en el shell, que hace aparecer una barra de scroll donde hoy
      no la hay y eso SÍ movería las pantallas de producción. Además hoy casi no
      se llega al caso: `.lightbox__body` va topado a `calc(100dvh - 150px)`.

  10. EL HUB TIENE SU PROPIA COPIA DE ESTA HOJA Y **NO** LA RECIBE.
      `juicy-core-hub/.../assets/css/ds/lightbox.css` es un fork (diverge ya en
      tres `font-family` y en el reset del `<button>` de cerrar). Este cambio no
      llega ahí, así que el Hub se queda con el chasis viejo — que funciona, pero
      solo con una caja. Queda dicho para que nadie lo descubra midiendo.

  11. EL VELO IBA POR ENCIMA DE LAS CAJAS QUE NO ESTÁN POSICIONADAS (0.9.193).
      ------------------------------------------------------------------------
      Lo levantó la revisión adversaria de la nota 9 y estaba VIVO: el panel «Modo
      de cobro» de Restaurant quedaba DEBAJO del velo y cada clic en él —elegir
      modalidad, pulsar «15%», escribir la propina— lo recogía el velo, que lleva
      `data-close-lightbox`. O sea: el panel no se podía usar y tocarlo CERRABA el
      cobro. Medido con `document.elementFromPoint` sobre el botón de 15%:
      devolvía `lightbox__overlay`.

      La causa no es del panel, es del chasis. En un contenedor flex los ítems se
      pintan como bloques en línea (paso 5 del orden de pintado) y el velo, que es
      `position:absolute`, va en el paso 6: encima. `.lightbox__content` y `.dialog`
      se salvaban por casualidad — se les dejó `position: relative` por OTRA razón
      (ser bloque contenedor de lo de dentro). Cualquier caja nueva que no lo
      declare cae en la trampa, y la siguiente caja del grupo la escribe otro.

      Por eso el arreglo va en el VELO (`z-index: -1`) y no en el panel: así toda
      caja del grupo queda por encima sin tener que saberlo. Comprobado que el velo
      no pierde nada: cubre el mismo rectángulo (1430×1000 @70,0) y sigue cerrando
      al pulsarlo donde no hay caja.

      Lo guarda `e2e/tests/lightbox.spec.ts`, que inyecta una caja **sin ninguna
      clase del DS** —o sea sin `position`— y comprueba `elementFromPoint`. La
      versión anterior de esa prueba inyectaba un `.lightbox__content`, que trae
      `position:relative` de aquí: verde con el defecto delante.

  12. UN GRUPO QUE NO CABE ES COSA DE QUIEN LO ARMA, Y HAY QUE SABERLO.
      ------------------------------------------------------------------------
      El shell NO tiene scroll y no va a tenerlo: su velo es `position:absolute`
      dentro de él, así que un `overflow:auto` aquí haría que el velo se desplazara
      con el contenido y dejara de ser un velo. Con una caja no importa
      (`.lightbox__body` va topado a `calc(100dvh - 150px)`); con dos, el que añade
      la segunda tiene que topar las dos para que el grupo quepa.

      No es teoría: al envolver a dos filas (`flex-wrap`), un panel «Modo de cobro»
      con una cuenta larga empujaba el grupo a 1359px de alto en una pantalla de
      800 — el principio del panel en `y = -559` y el final del modal en 1359, sin
      forma de llegar a ninguno de los dos. Se topan en la hoja del módulo
      (`cobro.css`), que es quien sabe cuánto vale cada caja.

  13. EL GRUPO SE PUEDE MAQUETAR, Y EL TAMAÑO SE MUDA A ÉL (0.9.194).
      ------------------------------------------------------------------------
      El chasis de la nota 9 sabe centrar N cajas en fila, y ahí se acaba: no hay
      forma de decir «ésta ocupa un tercio y ésta dos», ni de poner dos filas. Eso
      es exactamente lo que la rejilla de `ds/grid.css` ya sabe hacer, así que lo
      que faltaba no era una rejilla nueva sino UN SITIO DONDE PONERLA:
      `.lightbox__group`, un nivel entre el shell y las cajas.

      **EL TAMAÑO NO PODÍA QUEDARSE EN LA CAJA.** `.lightbox--sm/m/lg` topaban
      `.lightbox__content` a 480/800/1100. Con rejilla eso no se sostiene: una
      `col-3` es el 25% de la fila y la fila el 100% del grupo, así que si el ancho
      vive en la caja, la columna no tiene de qué ser un cuarto — el grupo mediría
      lo que midiera su contenido y las columnas repartirían aire. Desde 0.9.194
      las tres clases miden **el LIGHTBOX**: sin grupo el lightbox ES la caja y los
      números no se mueven ni un pixel; con grupo, el tope se lo queda el grupo.

      Se hace con variables y no con selectores (`.lightbox__group .lightbox__content
      { … }`) porque un descendiente gana por especificidad y por orden, y eso son
      dos formas de que una hoja de módulo lo deshaga sin querer. Con variables, el
      grupo sólo redefine `--lightbox-box-width` y `--lightbox-box-max` para lo que
      cuelgue de él, y la caja no tiene que saber si está dentro de un grupo o no.

      **QUE EL GRUPO SEA OPCIONAL NO ES UN DETALLE: ES EL REQUISITO.** El contrato
      de hoy (`.lightbox > .lightbox__content`) lo consumen **31 plantillas PHP del
      core** —16 con `--sm`, 3 con `--m`— y **20 sitios en React**. Las de PHP corren
      en PRODUCCIÓN (POS y Espresso). Si el grupo fuera obligatorio, esto dejaría de
      ser un ajuste del chasis y sería reescribir ~50 modales en dos mundos a la vez.
      Un lightbox de una caja sigue midiendo lo mismo SIN tocarlo; el grupo se añade
      sólo cuando se quiere la rejilla.

      QUÉ SE MIDIÓ (`e2e/foto-estilos.mjs`, dos fotos del mismo lado, con el modal
      ABIERTO — el instrumento aprendió a repetir `--clic` para poder llegar a un
      lightbox real, que no se abre de un solo clic):

        POS y Espresso · «Editar categoría» (`--m`) y el Dialog de «Eliminar
        usuario» (480) → 0 diferencias en las dos pantallas de los dos Locales.

      ⚠ ESAS CUATRO FOTOS SON DEL LADO REACT, no del dashboard de PHP: las dos
      pantallas están SELLADAS en los dos verticales, así que sólo se pueden
      fotografiar con `--lado react`. Comparten esta hoja pero no la cascada (PHP
      carga 45 hojas más), así que la foto no sostiene por sí sola lo que aquí se
      dice de las 31 plantillas de PHP. Quien sí lo sostiene es la revisión
      adversaria: montó las **16 combinaciones distintas de clases de shell** que
      emite todo el producto y midió `.lightbox__content` y `.dialog` con esta hoja
      y con la anterior insertada en la misma posición del `<head>`, en la cascada de
      PHP (`/facturas`) y en la del bundle, a 1500/1200/1199/900 → **0 diferencias en
      las 16, en las dos cascadas, en los cuatro anchos**.

      Y lo fija `e2e/tests/lightbox.spec.ts`, que ahora afirma los tres tamaños
      (480/800/1100) sin grupo, y con grupo: el reparto por columnas, el hueco entre
      cajas en LOS DOS EJES, que las cajas quedan PEGADAS ARRIBA aunque una sea mucho
      más alta, y que una caja neutra a tres niveles de hondo sigue siendo pulsable
      (o sea, encima del velo). Corre sobre LAS DOS SUPERFICIES —`/facturas`, que
      carga esta hoja tal cual, y `/app/pos`, que carga la copia compilada en el
      bundle—: mientras sólo corría sobre el bundle, romper este archivo sin
      reconstruir dejaba las pruebas verdes.

      EL HUECO ENTRE CAJAS FUE EL PRECIO ESCONDIDO, y lo cobró la revisión adversaria.
      Metidas en un grupo, las cajas dejan de ser ítems flex del shell y el `gap` que
      las separaba deja de correr: lo que queda es el gutter de la rejilla, 24px en
      horizontal y **0 en vertical**. En el cobro de Restaurant por debajo de 1200px
      el panel acababa pegado a la cabecera del modal, donde antes había 15. Por eso
      el grupo ata `--juicy-gutter-x/y` a `--lightbox-gap`: el hueco es del lightbox,
      no de la rejilla, y no cambia por si el grupo existe o no.

      LO QUE EL GRUPO NO ARREGLA, dicho aquí para que no se descubra midiendo: la
      nota 12 sigue entera. El shell no tiene scroll, así que un grupo más alto que
      la pantalla se sale igual, y quien lo monta sigue teniendo que topar sus cajas.
      La rejilla reparte ANCHO; el ALTO sigue siendo del que arma el grupo.
   ============================================================================ */
