/* =========================================================================
   NVL PRODUCT — estilos de la maestra de ficha de producto
   (single-nvl_product.php): galería, bloque de compra y atributos por
   categoría. Depende de nvl-components.css. Mobile-first.
   ========================================================================= */

/* --- Layout: una sola zona de contenido -----------------------------------
   La ficha ya no reserva columna para el resumen del carrito: aquí el usuario
   mira UN producto, y un panel permanente de 360px le quitaba ancho a la
   galería y a la columna de decisión sin resolver nada de lo que estaba
   haciendo. El carrito vive en el panel lateral (components/cart-drawer.php),
   a un clic del botón de la cabecera.

   Lo que eso devuelve al contenido: 360px del aside + 32px de gap ≈ 392px.
   Es lo que hace posible la composición de abajo. */

.nvl-product-layout {
    display: grid;
    gap: var(--nvl-space-xl);
    align-items: start;
    padding-top: var(--nvl-space-xl);
}


/* --- Composición de la ficha ----------------------------------------------
   Tres zonas, no tres columnas:

     ┌──────────────────────┬──────────────────┐
     │                      │  nombre          │
     │      GALERÍA         │  precio          │
     │      (protagonista)  │  disponibilidad  │
     │                      │  [ agregar ]     │
     ├──────────────────────┤  atributos       │
     │  descripción         │  (sticky)        │
     └──────────────────────┴──────────────────┘

   Reparto 1.25fr / 1fr: la galería se lleva la parte mayor porque en este
   catálogo la imagen ES el producto —un pez se compra por cómo se ve—, y la
   columna de decisión conserva ancho suficiente para que un nombre largo
   entre en dos líneas en vez de romperse en cuatro.

   La descripción hereda el ancho de la galería, no el del contenedor: a
   1200px una línea llevaría ~120 caracteres y el ojo pierde el renglón al
   volver; bajo la galería lleva ~65 (--nvl-measure), que es la medida cómoda.

   La columna de decisión es sticky: cuando la descripción es larga, el precio
   y el botón acompañan al usuario mientras lee en lugar de quedarse arriba.

   Peces y accesorios comparten esta composición y no necesitan una distinta:
   lo que cambia entre ellos es el VOLUMEN de contenido —un pez trae siete
   atributos y una descripción larga, un accesorio trae menos—, y la rejilla
   absorbe esa diferencia sola. Una plantilla por tipo de contenido, nunca una
   por categoría (Nuvora-Labs.md §6). */

.nvl-product-summary__grid {
    display: grid;
    gap: var(--nvl-space-xl);
    /* NO ES DECORATIVO: es lo que le da recorrido a la galería pegajosa de más
       abajo. Con el valor por defecto (`stretch`) la galería llenaría su celda
       y no tendría por dónde moverse. */
    align-items: start;
    /* Dónde se detiene la galería al subir. Suma la barra de administración de
       WordPress y lo que ocupe la barra de campaña SI está fijada —hoy 0, con
       el interruptor de nvl-base.css—, más el respiro de la referencia. Si
       algún día se vuelve a fijar la barra, la galería se aparta sola. */
    --nvl-gallery-sticky-top: calc(
        var(--wp-admin--admin-bar--height, 0px)
        + var(--nvl-promo-bar-stuck-h)
        + var(--nvl-space-lg)
    );
    /* Y CUÁNTO QUEDA DE PANTALLA PARA ELLA: la ventana menos donde se detiene
       al subir, menos el mismo respiro por abajo. Es el presupuesto del que
       sale el tope del escenario, y lo que permite que el sticky no lleve
       guardia de alto ninguno.

       SE DEFINE AQUÍ Y NO EN :root A PROPÓSITO. Allí --nvl-gallery-sticky-top
       todavía no existe, así que el calc se congelaría contra su valor vacío y
       la variable nacería inválida: es la trampa del var() anidado que ya mordió
       al héroe de la Home. */
    --nvl-gallery-fit: calc(100svh - var(--nvl-gallery-sticky-top) - var(--nvl-space-lg));
}

@media (min-width: 1024px) {
    .nvl-product-summary__grid {
        /* Dos zonas, no tres (owner, 2026-09-22): la descripción salió de esta
           rejilla y es ahora una banda propia a ancho completo, debajo.

           1.18 / 0.82 (owner, 2026-09-23) es el reparto medido en la
           referencia: la galería se lleva el 59% y no el 56%. El suelo de 24rem
           en la columna de decisión es el equivalente de su minmax(390px, …):
           impide que la caja se estreche hasta romper la fila de selector y
           botón. */
        grid-template-columns: minmax(0, 1.18fr) minmax(24rem, 0.82fr);
        grid-template-areas: "gallery info";
        align-items: start;
    }

    .nvl-product-summary__grid > .nvl-gallery { grid-area: gallery; }
    .nvl-product-info                          { grid-area: info; }

    /* SIN STICKY (owner, 2026-09-22). Lo hubo, y su motivo era «cuando la
       descripción es larga, el precio y el botón acompañan al usuario mientras
       lee». Ese motivo queda derogado: con pago, envío, garantía, escalera y
       resumen dentro de la caja, ya no hay que irse a leer a otro sitio para
       decidir.

       Y sobre todo, ya no cabía: la caja completa mide ~796px y el tope del
       sticky en un portátil de 800px era ~760. Un panel que no cabe no
       acompaña, deja el botón de compra a 450px por dentro de un scroll
       dentro de otro scroll.

       Con el sticky se va también su `overflow`, y con él la compensación que
       habría hecho falta para que la sombra de la caja no se recortara: el
       resultado es MÁS simple que lo que había, no más complejo.

       El acceso permanente al carrito no depende de esto: lo da la cápsula
       flotante (components/cart-bar.php), presente en todas las vistas. */
}

/* --- LA GALERÍA ACOMPAÑA AL SCROLL (owner, 2026-11-02) ---------------------
   La columna de decisión es mucho más alta que la galería, así que bajo las
   fotos quedaba un hueco en blanco enorme. Medido en staging (Planta Elodea,
   ventana de 900px):

       ancho   galería   decisión   hueco
       1024      571       1213      642
       1280      740       1179      439
       1440      775       1157      382

   SALE CASI GRATIS porque la rejilla ya tiene la forma que `sticky` pide:
   `align-items: start` le deja recorrido dentro de su celda —que mide lo que
   la columna más alta— y .nvl-gallery es hijo DIRECTO de la rejilla, así que
   no hace falta el envoltorio con `height: 100%` que sí necesitó el recibo del
   checkout. Ningún ancestro tiene `overflow`, que es lo único que lo habría
   dejado sin efecto y sin aviso.

   SIN GUARDIA DE ALTO, y esto es una CORRECCIÓN, no una simplificación (owner,
   2026-11-02, segunda revisión en staging). Esto nació con dos media queries de
   `min-height` —770 y 800 primero, 710 y 745 después— y con ellas el efecto no
   se activaba en NINGUNA de las dos fichas, aunque la galería cupiera de sobra.
   Medido con la hoja ya publicada, sin sesión:

       ventana     galería (3 fotos)   ¿cabe?       position
       1440×760          675           sí           sticky
       1440×740          675           sí           static   ←
       1440×700          675           sí, justo    static   ←
       1280×700          640           sí, +36      static   ←
       1200×700          595           sí, +81      static   ←
       1100×660          531           sí, +105     static   ←

   LA CAUSA ES DE FORMA Y NO DE CALIBRADO. El alto de la galería depende del
   ANCHO —la foto es cuadrada: una columna más estrecha da una galería más
   baja—, y un `min-height` mide la VENTANA sin saber nada de ese ancho. Un
   número único tiene que servir para el caso más alto de todos, y en cuanto la
   ventana se estrecha se vuelve absurdo: a 1100px exigía 710 de ventana para
   una galería de 531. Encima se compara con el viewport REAL, que en un
   portátil de 13" se queda en ~700-740 después de la barra de menú de macOS, la
   del navegador y el Dock — justo la franja donde todo cabe y nada se pegaba.

   Así que la condición desaparece, y en su lugar la galería NO PUEDE ser más
   alta que el espacio visible: lo garantiza el tope del escenario, más abajo.
   Si nunca sobra de la ventana, el sticky no necesita permiso. Quedan cero
   umbrales que calibrar, y ninguno que volver a medir cuando cambie el alto de
   cualquier otra cosa.

   Los umbrales que hubo son historia útil: el primer par se midió sobre una
   ficha de UNA foto y dejaba fuera a las de varias, ~100px más bajas porque la
   tira le quita 80px de ancho al escenario y la proporción los convierte en
   alto. El segundo par corrigió ese sesgo y seguía fallando por lo de arriba.
   Por eso no se vuelve a intentar con un tercer par. */
@media (min-width: 1024px) {
    .nvl-product-summary__grid > .nvl-gallery {
        position: sticky;
        inset-block-start: var(--nvl-gallery-sticky-top);
    }
}

/* Ritmo vertical: mismo escalón de escritorio grande que el layout de
   categoría. */
@media (min-width: 1280px) {
    .nvl-product-layout,
    .nvl-product-summary__grid {
        gap: var(--nvl-space-2xl);
    }
}

/* Móvil y tablet (< 1024px): una sola columna en el orden del DOM —galería,
   decisión, descripción—. Es el orden correcto también aquí: primero qué es,
   luego cuánto cuesta y cómo comprarlo, y solo después el detalle.

   No se usan dos columnas por debajo de 1024px: a 768px cada una quedaría en
   ~350px y el nombre volvería a romperse en cuatro líneas, que es justo el
   problema que esta composición corrige. */

/* --- Descripción: banda propia (owner, 2026-09-22) ------------------------- */

/* Sección a ancho completo con la receta de banda oscura que el tema ya usa en
   el hero de marca y en la CTA de contacto: --nvl-color-secondary de fondo y
   superficie de tinta, 17.85:1 medido. Es el equivalente dentro de nuestra identidad
   del bloque negro de la referencia; no se copia su color ni se crea un tono.

   El ancho de lectura lo sigue topando --nvl-measure (~65 caracteres): la banda
   es ancha, el renglón no. */
/* PROFUNDIDAD DE LA BANDA (owner, 2026-10-23): «se ve demasiado plano y
   quisiera darle más profundidad y dimensión visual». El diagnóstico es el
   mismo que ya tuvo el héroe de la Home: el problema no es la tinta —el blanco
   sigue dando 17.85:1, el máximo posible sobre esta superficie— sino el CAMPO,
   que no tenía ni dirección de luz ni canto. Una banda de tinta plana entre dos
   zonas blancas lee como un agujero recortado en la página, no como una losa
   apoyada encima.

   Se resuelve con la receta que el tema YA tiene escrita para esto, la misma
   del héroe y de la caja de cierre: el mismo radial, el mismo
   --nvl-color-tinta-glow, la misma caída al 64%. CERO TOKENS NUEVOS.

   POR QUÉ EL RADIAL SE ESCRIBE AQUÍ Y NO SE USA var(--nvl-gradient-hero).
   El comentario de ese token en nvl-base.css dice que el foco se recoloca
   «redefiniendo --nvl-hero-glow-at en el contenedor» y que el gradiente nunca
   se reescribe. ESO NO FUNCIONA, y está comprobado en el navegador: una
   propiedad personalizada se calcula CON SUS var() YA SUSTITUIDOS en el
   elemento donde se declara, y --nvl-gradient-hero se declara en :root. Lo que
   hereda cualquier descendiente es el gradiente con el foco de :root —50% 82%,
   el de móvil— congelado dentro; redefinir --nvl-hero-glow-at más abajo llega
   tarde y no mueve nada. Medido: con --nvl-hero-glow-at: 30% 38% en esta
   sección, el background-image calculado seguía diciendo `at 50% 82%`.

   Así que el foco solo se puede colocar escribiendo el radial en el mismo
   elemento que lo declara. Es la única duplicación del literal en el tema y
   queda anotada en los dos sitios. Si algún día se arregla de raíz (declarar
   --nvl-gradient-hero sobre `*` en vez de :root), esta regla vuelve a ser
   `background-image: var(--nvl-gradient-hero)` y nada más. OJO: ese arreglo
   movería también el foco del héroe de la Home en ≥768, que hoy arrastra el
   mismo defecto y lleva meses viéndose con el foco de móvil.

   El foco NO va al centro geométrico: la columna de lectura se topa en
   --nvl-measure y queda pegada a la izquierda del contenedor, así que en
   escritorio la luz se corre con ella. Si alguna vez el cuerpo se centra
   (margin-inline: auto), este 30% vuelve al 50%.

   TRES CAPAS Y NINGUNA MÁS, que es lo que el owner pidió — volumen «sin
   recargarlo ni afectar la legibilidad»:
     1. el brillo radial, que es el volumen;
     2. el canto superior iluminado: --nvl-color-overlay-light, el 12% que la
        hoja de tokens describe como «el filo que hace que una superficie de
        tinta lea como objeto iluminado en vez de como agujero». Va de inset y
        de un solo píxel, así que no mueve ni el relleno ni el texto;
     3. la sombra de contacto hacia abajo. Es --nvl-shadow-float y no
        --nvl-shadow-lg a propósito: esa última está calibrada para separar una
        tarjeta BLANCA de un fondo gris y bajo un objeto de tinta no se ve,
        porque el objeto ya es más oscuro que su propia sombra (lo dice su
        propio comentario en nvl-base.css). Hacia ARRIBA no hay sombra: la luz
        cae desde arriba y un objeto no se proyecta sobre su propia fuente.

   El background-color se queda DEBAJO del background-image a propósito, igual
   que en el héroe: si color-mix() fallara, la banda sigue siendo tinta legible
   y el texto no se pierde. Contrastes: los del héroe, porque es exactamente el
   mismo gradiente — en el punto más claro (#0b304e) el blanco da 13.57:1, que
   es el peor caso para este texto. */
.nvl-product-story {
    --nvl-hero-glow-at: 50% 32%;
    margin-block-start: var(--nvl-space-xl);
    padding-block: var(--nvl-space-2xl);
    background-color: var(--nvl-color-secondary);
    background-image: radial-gradient(118% 92% at var(--nvl-hero-glow-at),
                      var(--nvl-color-tinta-glow), var(--nvl-color-secondary) 64%);
    box-shadow: inset 0 var(--nvl-border-hairline) 0 0 var(--nvl-color-overlay-light),
                var(--nvl-shadow-float);
    color: var(--nvl-color-surface);
}

/* En forced-colors esto es campo, nunca información: fuera el gradiente y
   fuera el relieve, que el modo de alto contraste pinta Canvas plano y el
   texto vuelve a CanvasText. */
@media (forced-colors: active) {
    .nvl-product-story {
        background-image: none;
        box-shadow: none;
    }
}

/* EL AZUL DEL ANTETÍTULO NO ES EL DE .nvl-eyebrow (owner, 2026-10-24). Pidió
   «el color azul para mantener la coherencia visual», y el de la clase base
   —--nvl-color-primary-dark, pensado para fondo claro— sobre esta banda da
   2.29:1 contra la parte iluminada del degradado: ilegible. El acento claro da
   6.34:1 ahí y 8.33:1 sobre la tinta plana, así que pasa AA en todo el ancho de
   la luz. Es además el mismo azul y la misma excepción que ya usa el antetítulo
   del héroe sobre tinta (.nvl-home-hero__eyebrow): una declaración, no un token
   nuevo. */
.nvl-product-story__eyebrow {
    margin: 0 0 var(--nvl-space-2xs);
    color: var(--nvl-color-accent);
}

.nvl-product-story__title {
    margin: 0 0 var(--nvl-space-lg);
    font-size: var(--nvl-text-xl);
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-heading);
    color: var(--nvl-color-surface);
}

.nvl-product-story__body {
    max-width: var(--nvl-measure);
}

@media (min-width: 768px) {
    .nvl-product-story {
        /* La luz se corre con la columna de lectura, que aquí ya no ocupa el
           ancho del contenedor. Mismo criterio que --nvl-hero-glow-at-wide en
           el héroe; el valor es otro porque la composición es otra. */
        --nvl-hero-glow-at: 30% 38%;
        padding-block: var(--nvl-space-3xl);
    }

    .nvl-product-story__title {
        font-size: var(--nvl-text-2xl);
    }
}

/* --- Galería --------------------------------------------------------------- */

/* Se topa el ANCHO, no el alto. Topar el alto con width:100% sacaría la caja
   de 4:3 y `cover` volvería a recortar por arriba y por abajo: dos criterios
   de encuadre otra vez, que es justo lo que este cambio elimina.
   En escritorio la columna de galería mide 613px y el tope (640) no llega a
   actuar, así que la composición 1.25fr/1fr queda intacta. Actúa donde estaba
   el problema: la banda de una sola columna, donde el cuadrado producía una
   foto de 959×959. Sin margin-inline:auto — la media queda al mismo filo que
   la descripción, que hereda su columna. */
/* CONTENEDOR (owner, 2026-09-22): tira + escenario son un solo objeto. El
   tope de ancho SE MUDA aquí desde __main, y eso corrige un defecto vivo: en
   la banda de una sola columna el escenario se topaba en 640px pero la tira se
   estiraba hasta 959 y quedaban desalineados.

   La tira va PRIMERO en el marcado, así que basta conmutar la dirección para
   tenerla encima en móvil y a la izquierda en escritorio: sin `order`, sin
   `row-reverse`, y con el orden de lectura, visual y de foco siempre iguales. */
/* min-inline-size: 0 NO es decorativo, sostiene toda la vista en móvil.
   La tira dejó de envolverse en varias filas para ser una fila única con
   scroll, y con ello su tamaño min-content pasó a ser la fila entera —8×96 más
   los huecos, 832px—. Como celda de la rejilla, la galería hereda ese suelo
   recortado por max-width (640px) y NO baja de ahí: a 360 y 390px la ficha
   completa desbordaba en horizontal, no solo la galería. Con el suelo a 0 la
   galería mide lo que mida la columna y el desbordamiento se queda DENTRO de
   la tira, que para eso tiene scroll propio. */
.nvl-gallery {
    display: flex;
    flex-direction: column;
    gap: var(--nvl-space-xs);
    min-inline-size: 0;
}

/* El tope solo actúa donde hace falta: la banda de UNA columna (owner,
   2026-09-23). Ahí evita una foto a ancho completo de pantalla. En escritorio
   el ancho ya lo fija la rejilla, y aplicarlo también allí recortaba la columna
   de 717 a 640 — 77px que se le quitaban a la foto sin ningún motivo. */
@media (max-width: 1023px) {
    .nvl-gallery {
        max-width: var(--nvl-gallery-max);
    }
}

/* El escenario FLOTA (sombra md), y la caja de decisión un escalón por encima
   (sombra lg). Son los DOS únicos objetos elevados de la vista: todo lo que va
   dentro de ellos es plano. Esa es la disciplina que impide que la ficha se
   convierta en una pared de tarjetas, no el no elevar nada.

   position: relative ancla las flechas, el contador y la insignia. */
/* EL ALTO DEL ESCENARIO ES LO ÚNICO QUE CRECE (owner, 2026-10-25). El contador
   se encimaba sobre la foto, y la causa no era su tamaño: era que aquí no
   sobraba alto por debajo de la fotografía. Se probó encogiendo la cápsula y en
   móvil seguía solapándose, así que el arreglo está donde estaba el problema.

   LA RECETA ES LA DE LA REFERENCIA, leída de su CSS: una proporción que da la
   forma (`aspect-ratio: 1/1.08`) y un suelo que la rescata donde se queda corta
   (`min-height`), ganando el mayor de los dos. Su imagen RELLENA el escenario y
   `object-fit: contain` mantiene la foto cuadrada y centrada, con el sobrante
   repartido arriba y abajo. El contador vive en ese sobrante.

   `inline-size: 100%` NO ES DECORATIVO. Con `aspect-ratio`, un `min-block-size`
   empuja también a lo ANCHO: sin esta línea el escenario se iba de 625 a 630px
   y de 437 a 463 en cuanto el suelo entraba en juego, y el owner pidió
   expresamente que el ancho no se moviera. Con el ancho fijado, la proporción
   solo puede decidir el alto.

   Hueco entre la foto y el contador, medido en la ficha real: +23.4 a 1440,
   +22.1 a 1280, +14.1 a 1100, +6.5 a 1024, +14.3 a 900 y 768, +22.7 a 390 y
   +18.6 a 360. El punto más ajustado, 1024, es el mismo que tiene la referencia
   a esa anchura (+6.3). Si cambia la proporción, el suelo, el relleno de la
   imagen o el alto de la cápsula, esas ocho cifras se vuelven a medir. */
.nvl-gallery__main {
    position: relative;
    inline-size: 100%;
    aspect-ratio: var(--nvl-ratio-gallery-stage);
    min-inline-size: 0;              /* puede encoger dentro del flex en fila */
    /* BLANCO (owner, 2026-09-22): el escenario venía en --nvl-color-section-alt,
       pero con la foto a `cover` ese fondo no se veía JAMÁS —la imagen lo
       tapaba entero— y el contenedor que habíamos acordado no existía en
       pantalla. La referencia usa una tarjeta blanca con la foto contenida
       dentro; el fondo es lo que se ve alrededor de la foto, así que tiene que
       ser la superficie, no el tono de sección. */
    background: var(--nvl-color-surface);
    border: var(--nvl-border-hairline) solid var(--nvl-color-border);
    /* 24px Y NO 16 (owner, 2026-10-24): «las esquinas más redondeadas le dan un
       aspecto más limpio y atractivo al diseño», con redlemon.com.mx de
       referencia. No es un valor nuevo: --nvl-radius-xl ya era el radio de las
       superficies GRANDES del tema (tarjeta de categoría, media del héroe), y
       las dos tarjetas de la ficha son de esa escala. Las cajas de DENTRO
       —medios de pago, escalera, entrega— se quedan en --nvl-radius-md: esa
       diferencia es la que hace que la tarjeta se lea como contenedor y no como
       una caja más, y es también lo que hace la referencia.
       La curva no muerde la foto aunque el relleno (6%) quede por debajo de 24:
       el vértice de la imagen cae dentro del círculo del radio, no fuera. */
    border-radius: var(--nvl-radius-xl);
    box-shadow: var(--nvl-shadow-md);
    overflow: hidden;
}

/* `contain` y no `cover`: la foto deja de recortarse y se ve completa, como en
   la referencia. El relleno va en la IMAGEN y no en la figura a propósito: el
   bloque contenedor de las flechas, el contador y la insignia es la caja de
   relleno de la figura, así que rellenar la figura los empujaría 16px más hacia
   dentro y descuadraría los tres a la vez.

   object-position pasa a `center` porque --nvl-media-focus ya no encuadra nada:
   ese token existe para decidir QUÉ PARTE se recorta, y con `contain` no se
   recorta ninguna. Las miniaturas conservan `cover`: son recortes de 96px y
   contenidas quedarían casi vacías. */
.nvl-gallery__image {
    width: 100%;
    /* RELLENA EL ESCENARIO y ya no impone su proporción (owner, 2026-10-25).
       La tuvo —cuadrada— y por eso el escenario era cuadrado y no quedaba sitio
       para el contador. Ahora la forma la decide el contenedor y `object-fit:
       contain`, que ya estaba, mantiene la foto CUADRADA, del mismo tamaño que
       antes y centrada, con el sobrante repartido arriba y abajo.
       El recorte 1:1 del campo ACF no cambia, ni --nvl-ratio-gallery, que
       siguen usando las miniaturas y las líneas del recibo. */
    block-size: 100%;
    /* Relleno en PORCENTAJE, no en token fijo: se resuelve contra el ancho del
       CONTENEDOR y no contra la ventana, que es lo que hace falta aquí — el
       escenario cambia de ancho según haya tira de miniaturas o no, y un `vw`
       daría el mismo valor en los dos casos.

       6% es el margen que tiene la referencia en su banda de una columna
       (medido: 6.1% a 768px, con la foto llenando el 87.7% del cuadro). Desde
       1024px se topa —ver más abajo—, igual que el suyo. */
    padding: 6%;
    object-fit: contain;
    object-position: center;
}

/* AL CAMBIAR DE IMAGEN NO SE ANIMA NADA (owner, 2026-10-25), y por eso esta
   imagen ya no declara `transition`. Hubo aquí una regla que la fundía a
   `opacity: 0` mientras se intercambiaba, y medida en la ficha real dejaba la
   tarjeta EN BLANCO ~390ms por clic: el fundido arrancaba antes de tener la
   foto nueva. Ahora nvl-gallery.js descarga y decodifica primero y asigna
   después, así que el cambio es de un fotograma y la foto anterior se ve hasta
   el final. El porqué completo, y el fundido cruzado que se evaluó y descartó,
   están en la cabecera de ese archivo. */

/* --- Flechas -------------------------------------------------------------
   Mismo objeto que los botones del stepper: círculo de 44px, superficie
   blanca, borde y glifo heredando currentColor.

   BLANCO OPACO, no translúcido como la referencia: con alfa, el contraste del
   glifo dependería de la foto que haya debajo. Opaco da 16.7:1 siempre.

   El glifo es `arrow-left`, la flecha fina que el sprite estrenó para esto
   (owner, 2026-10-25): el chevron macizo que había antes se leía pesado al
   lado del de la referencia. */
.nvl-gallery__arrow {
    position: absolute;
    z-index: var(--nvl-z-base);
    top: 50%;
    transform: translateY(-50%);
    width: var(--nvl-tap-min);
    height: var(--nvl-tap-min);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: var(--nvl-border-hairline) solid var(--nvl-color-border);
    border-radius: var(--nvl-radius-circle);
    background: var(--nvl-color-surface);
    color: var(--nvl-color-secondary);
    box-shadow: var(--nvl-shadow-md);
    cursor: pointer;
}

.nvl-gallery__arrow--prev { inset-inline-start: var(--nvl-space-2xs); }
.nvl-gallery__arrow--next { inset-inline-end: var(--nvl-space-2xs); }

/* `arrow-left` apunta a la izquierda, así que «anterior» no gira y «siguiente»
   da media vuelta. La rotación va en el ICONO, nunca en el botón, que ya usa
   transform para centrarse en vertical. */
.nvl-gallery__arrow--next .nvl-icon { transform: rotate(180deg); }

/* 20px y no 24: la flecha de la referencia ocupa 16 de sus 44px de botón, y
   24 de 44 es parte de por qué la nuestra se leía pesada. */
.nvl-gallery__arrow .nvl-icon {
    width: var(--nvl-icon-fixed);
    height: var(--nvl-icon-fixed);
}

/* Anillo por DENTRO: por fuera caería sobre la foto, donde ningún color tiene
   contraste garantizado. */
.nvl-gallery__arrow:focus-visible {
    outline-offset: calc(var(--nvl-focus-width) * -1);
}

@media (hover: hover) and (pointer: fine) {
    .nvl-gallery__arrow:hover {
        border-color: var(--nvl-color-secondary);
        box-shadow: var(--nvl-shadow-lg);
    }
}

.nvl-gallery__arrow:active {
    background: var(--nvl-color-background);
    box-shadow: none;
    transform: translateY(-50%) scale(0.94); /* el centrado NO se pierde */
}

@media (prefers-reduced-motion: no-preference) {
    .nvl-gallery__arrow {
        transition:
            border-color var(--nvl-duration-fast) var(--nvl-ease-standard),
            box-shadow   var(--nvl-duration-fast) var(--nvl-ease-standard);
    }
}

/* --- Contador y insignia -------------------------------------------------
   --nvl-color-overlay es el único token del sistema pensado para superponerse
   a contenido arbitrario. Peor caso calculado: sobre foto blanca pura compone
   rgb(99 104 117) y el texto blanco encima rinde 5.57:1. Es imposible que baje
   de ahí, porque no hay fondo más claro que el blanco.

   Los bordes transparentes sostienen la forma en forced-colors. */
.nvl-gallery__counter,
.nvl-gallery__discount {
    position: absolute;
    z-index: var(--nvl-z-base);
    margin: 0;
    border: var(--nvl-border-hairline) solid transparent;
    border-radius: var(--nvl-radius-pill);
    color: var(--nvl-color-surface);
    line-height: var(--nvl-leading-snug);
    font-variant-numeric: tabular-nums;
}

/* El descuento se queda en el suelo de 12px; el contador sube a 13 para
   igualar a la referencia. Separados porque ya no comparten tamaño. */
.nvl-gallery__discount { font-size: var(--nvl-text-2xs); }
.nvl-gallery__counter  { font-size: var(--nvl-text-xs); }

/* LA CÁPSULA DEL CONTADOR, CALCADA DE LA REFERENCIA (owner, 2026-10-25). La
   suya mide 13px en peso 400 con relleno 7/11 sobre `rgba(23,24,27,.78)`; la
   nuestra era de 12px en BOLD con relleno 4/8, y el owner la vio «pequeña e
   insignificante». Sube de tamaño, baja de peso y estrena el gris neutro.

   SIN `letter-spacing`, aunque la referencia lo lleve: el tema no usa tracking
   en ninguna parte desde el 17-oct y es decisión del owner, vigilada por
   c-tipografia. A 13px y dos cifras, no se echa de menos.

   El fondo es el único color nuevo, y mejora lo que había: sobre una zona
   BLANCA de la foto —el peor caso— el blanco pasa de 5.57:1 a 8.73:1.

   LA CÁPSULA NO SE ENCOGE PARA CABER (owner, 2026-10-25, segunda vuelta).
   Hubo un intento: bajarle el relleno vertical a 3xs para que entrara en la
   banda de relleno de la foto. Funcionaba en escritorio y seguía solapándose
   en móvil, y el owner lo rechazó con razón — el problema no era su tamaño,
   era que al escenario no le sobraba alto. El alto se arregla donde toca, en
   .nvl-gallery__main; aquí la cápsula conserva sus 33.6px. */
.nvl-gallery__counter {
    inset-block-end: var(--nvl-space-sm);
    inset-inline-end: var(--nvl-space-sm);
    padding: var(--nvl-space-2xs) var(--nvl-space-xs);
    background: var(--nvl-color-overlay-neutral);
    font-weight: var(--nvl-weight-regular);
}

/* Solo se imprime cuando el PRECIO no lleva ya su insignia: la ficha nunca
   muestra dos descuentos a la vez (components/gallery.php). */
.nvl-gallery__discount {
    inset-block-start: var(--nvl-space-sm);
    inset-inline-start: var(--nvl-space-sm);
    padding: var(--nvl-space-3xs) var(--nvl-space-xs);
    background: var(--nvl-color-promo-badge);
    box-shadow: var(--nvl-shadow-md);
    font-weight: var(--nvl-weight-bold);
}

/* EL SUELO, solo donde la proporción no alcanza: por debajo de 768 el escenario
   baja hasta 320px de ancho y un 8% de eso no da para la cápsula, que mide 33.6
   se mire donde se mire. Es el `min(112vw, 540px)` de la referencia. Aquí manda
   él y la proporción sube sola a 1.25-1.26, igual que en la suya. */
@media (max-width: 767px) {
    .nvl-gallery__main {
        min-block-size: var(--nvl-gallery-stage-floor);
    }

    .nvl-gallery__counter {
        inset-block-end: var(--nvl-space-2xs);
        inset-inline-end: var(--nvl-space-2xs);
    }

    .nvl-gallery__discount {
        inset-block-start: var(--nvl-space-2xs);
        inset-inline-start: var(--nvl-space-2xs);
    }
}

/* UNA sola fila (o columna), nunca dos: con flex-wrap la tira se partía con 8
   imágenes, cambiaba de alto y empujaba la descripción.

   El padding de 4px NO es decorativo: el anillo de foco alcanza 4px
   (--nvl-focus-offset + --nvl-focus-width) y el overflow lo recortaría.

   Señal de que hay más: el CORTE de la última miniatura visible. Sin máscaras
   de desvanecido, que exigirían un degradado literal fuera de la escala. */
.nvl-gallery__thumbs {
    list-style: none;
    display: flex;
    flex-wrap: nowrap;
    gap: var(--nvl-space-2xs);
    margin-top: 0;
    padding: var(--nvl-space-3xs);
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scroll-snap-type: x proximity;
    scroll-padding-inline: var(--nvl-space-3xs);
    scrollbar-width: thin;
}

/* flex: none en el ítem es imprescindible en las dos disposiciones: sin él las
   miniaturas se encogen para caber en vez de desplazarse, y el scroll no
   aparece nunca. */
.nvl-gallery__thumbs-item {
    flex: none;
    scroll-snap-align: start;
}

/* --- Desde 1024px: la tira pasa a VERTICAL, a la izquierda ---------------

   REJILLA DE DOS PISTAS, COMO LA REFERENCIA (owner, 2026-10-25). Hasta hoy la
   tira se sacaba del flujo (`position: absolute`) y el escenario compensaba con
   dos declaraciones: un `margin-inline-start` cuando había tira y, SIEMPRE, un
   `max-inline-size: calc(100% - 92px)` con `margin-inline: auto` para que una
   ficha de una sola foto midiera lo mismo que una con tira (23-sep).

   Ese par era el margen que el owner señaló en amarillo: en un producto con una
   sola imagen reservaba 92px para una tira que no existe y los repartía 46 y 46.

   Y arrastraba un acoplamiento invisible. Un margen automático en el eje
   transversal ANULA el estiramiento, así que el escenario pasaba a medir lo que
   mide su contenido — y el ancho intrínseco de una imagen con `srcset` ES el
   valor de su atributo `sizes`. Medido en la ficha real: con `sizes="200px"` el
   escenario medía 202px. El tope solo recortaba; nunca estiraba. Mientras
   `sizes` valía `100vw` el contenido era enorme y el tope mandaba, y por eso el
   defecto estuvo dormido hasta que la galería estrenó un `sizes` honesto.

   La referencia no tiene nada de eso: su galería es `display: grid` con
   `grid-template-columns: 82px 1fr` y la tira EN EL FLUJO, como primera pista.
   Ocupa espacio porque está ahí; cuando no está, no hay pista que reservar y el
   escenario se queda con la columna entera. Y al ser una pista `1fr`, el
   escenario no puede encoger a su contenido: `sizes` deja de poder moverlo.

   Aquí va lo mismo. Con ello desaparecen el `position: absolute`, el
   `position: relative`, el margen compensatorio, el tope, los márgenes
   automáticos y la variable --nvl-gallery-strip, cuyo único cometido era
   mantener sincronizados el margen y el tope. */
@media (min-width: 1024px) {
    /* Una pista por defecto: sin tira, el escenario es la columna. */
    .nvl-gallery {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        column-gap: var(--nvl-space-xs);
    }

    /* CON UNA SOLA FOTO EL ESCENARIO ES CUADRADO (owner, 2026-11-02). El 8% de
       más de --nvl-ratio-gallery-stage existe para que el CONTADOR no se encime
       sobre la fotografía, y components/gallery.php no imprime ni contador ni
       flechas ni tira cuando solo hay una imagen ($nvl_gallery_multiple): se
       estaba reservando hueco para controles que no existen. Medido a 1440: 58px
       de blanco muerto, repartidos arriba y abajo.

       LA FOTO NO CAMBIA NI UN PÍXEL, y son cuentas: el escenario mide 717 de
       ancho con 48px de relleno a cada lado, así que a la imagen —cuadrada, con
       `contain`— le quedan 619 de caja útil. A 1/1.08 la caja era 619×677 y la
       foto se pintaba a 619; a 1/1 la caja es 619×621 y se pinta igual. La
       limita el ANCHO, que no se toca. Lo único que desaparece es el hueco.

       Se reutiliza --nvl-ratio-gallery, que ya ES el recorte cuadrado del campo
       ACF: el escenario pasa a medir exactamente lo que la fotografía.

       SOLO AQUÍ, y `:not(--multi)` no es opcional: con tira, el contador y las
       flechas sí se imprimen y necesitan su hueco. Y por debajo de 768 manda el
       suelo (min-block-size), que existe porque a ese ancho un 8% no da para la
       cápsula. El `sizes` de la imagen no se mueve: depende del ancho. */
    .nvl-gallery:not(.nvl-gallery--multi) .nvl-gallery__main {
        aspect-ratio: var(--nvl-ratio-gallery);
        /* Sin tira el escenario es cuadrado, así que el presupuesto de alto se
           aplica tal cual: topar el ancho en él deja el alto exacto. */
        max-inline-size: var(--nvl-gallery-fit);
    }

    /* NINGÚN ESCENARIO PASA DEL ESPACIO VISIBLE (owner, 2026-11-02, segunda
       revisión en staging). Es lo que SUSTITUYE a los guardias de alto: en vez
       de prohibir el sticky donde la galería no cabría, se hace que quepa
       siempre, y entonces se puede fijar en cualquier ventana.

       EL TOPE ES DE ANCHO, NO DE ALTO, y de ahí que la foto no se recorte
       jamás: con `aspect-ratio`, limitar el ancho baja el alto en la misma
       proporción y el cuadro conserva su forma. Caparlo en alto sí la habría
       recortado, y por eso no se hace.

       Se topa el ESCENARIO y no la galería para no volver a escribir los 92px
       de la tira: ese acoplamiento se borró el 25-oct y no vuelve. La tira se
       ajusta sola con su par `block-size: 0` + `min-block-size: 100%` —medido:
       tira de 652 con escenario de 652— y el sobrante se queda a la derecha,
       dentro de una galería que no pinta nada.

       SOLO ENTRA CUANDO HACE FALTA. A 1440, la ficha de una foto conserva sus
       717px hasta que la ventana baja de ~765, y la de varias sus 675 hasta
       ~723; por encima de ahí estas dos declaraciones no hacen nada. Medido:
       1440×900 y 1440×780 dan 717×675 en el Angel, idéntico a antes.

       Y LA COMPOSICIÓN INTERIOR NO SE APRIETA: el relleno de la imagen es
       min(9%, 48px), no un fijo, así que al estrecharse el escenario el margen
       alrededor de la foto SUBE del 6.7% al 9% —la banda medida en la
       referencia—, nunca baja. La fotografía original no se toca: ni el
       archivo, ni su recorte 1:1 del campo ACF, ni su srcset.

       El 1.08 es el alto de --nvl-ratio-gallery-stage; el banco exige que los
       dos sigan diciendo lo mismo, así que cambiar la proporción del escenario
       pone las pruebas en rojo en vez de dejar este tope mintiendo. */
    .nvl-gallery--multi .nvl-gallery__main {
        max-inline-size: calc(var(--nvl-gallery-fit) / 1.08);
    }

    /* Con tira, la pista `auto` la mide ella misma y el escenario toma el resto.
       El 92px de antes ya no se escribe en ninguna parte: son los 80 de la tira
       más el hueco de la rejilla. */
    .nvl-gallery--multi {
        grid-template-columns: auto minmax(0, 1fr);
    }

    /* `block-size: 0` CON `min-block-size: 100%` ES EL PAR QUE SOSTIENE ESTO, y
       sin él la vista se rompe de una forma que cuesta ver: la tira en el flujo
       estira la fila hasta su contenido —ocho miniaturas miden más que el
       escenario— y el escenario deja de ser cuadrado. Medido antes de añadirlo:
       625.4 × 640.

       Con el par, la tira no aporta altura para calcular la fila (su tamaño es
       0) pero se ajusta a la que fije el escenario con su `aspect-ratio`, y su
       contenido desborda hacia el scroll que ya tenía. Es el mismo resultado
       determinista que daba `inset-block: 0` cuando estaba fuera del flujo, que
       era precisamente la razón de haberla sacado. La referencia resuelve su
       tira igual: 700.7px de alto, exactamente el de su escenario.

       `flex-direction` es lo único que conmuta: en móvil la tira es una fila
       encima, aquí una columna al lado. Sin `order` y sin `row-reverse`: el
       orden de lectura, el visual y el de foco siguen siendo el mismo. */
    .nvl-gallery--multi .nvl-gallery__thumbs {
        flex-direction: column;
        /* 72px de miniatura + los 8 del relleno. El suelo anterior era 96px, y
           lo imponía la proporción 16:9: con ella, un ancho menor dejaba el
           objetivo táctil por debajo de 44px de alto. Con la proporción casi
           cuadrada del escenario esa restricción desaparece —72 de ancho son 78
           de alto— y la tira devuelve 24px al escenario. */
        inline-size: calc(var(--nvl-space-4xl) + var(--nvl-space-2xs));
        block-size: 0;
        min-block-size: 100%;
        overflow-x: hidden;
        overflow-y: auto;
        overscroll-behavior-block: contain;
        overscroll-behavior-inline: initial;
        scroll-snap-type: y proximity;
        scroll-padding-block: var(--nvl-space-3xs);
    }

    /* El relleno se TOPA en 48px, el valor medido en la referencia a 1280 y a
       1440. El 9% manda mientras el escenario es estrecho —a 1024 da 39px y la
       referencia tiene 41— y a partir de ~533px se queda en los 48 exactos: a
       1440 el margen baja del 9.1% al 7.7%, que es su mismo valor (7.5%). */
    .nvl-gallery__image {
        padding: min(9%, 48px);
    }
}

/* 96×72: el ancho sale de un token y la altura la deduce el ratio, así que la
   fila de miniaturas no se mueve respecto a los 72px anteriores y la
   miniatura pasa a ser una versión a escala de la principal — el criterio
   único de encuadre se ve literalmente. Supera --nvl-tap-min en los dos ejes.
   El borde baja a --nvl-border-thick: el estado seleccionado se distingue por
   COLOR, no por grosor, y medio píxel no comunica nada que justifique ampliar
   la escala de bordes. */
.nvl-gallery__thumb {
    display: block;
    /* 72px, no 96: el suelo anterior lo imponía la proporción 16:9, que a menos
       ancho dejaba el objetivo táctil por debajo de 44px de alto. Con la
       proporción casi cuadrada, 72 de ancho son 78 de alto y los dos ejes
       superan el objetivo con holgura. */
    width: var(--nvl-space-4xl);
    aspect-ratio: var(--nvl-ratio-gallery);
    padding: 0;
    background: var(--nvl-color-section-alt);
    border: var(--nvl-border-thick) solid var(--nvl-color-border);
    border-radius: var(--nvl-radius-md);
    overflow: hidden;
    cursor: pointer;
    transition: border-color var(--nvl-duration-base) var(--nvl-ease-standard);
}

@media (hover: hover) and (pointer: fine) {
    .nvl-gallery__thumb:hover {
        border-color: var(--nvl-color-border-hover);
    }
}

.nvl-gallery__thumb.nvl-is-selected {
    border-color: var(--nvl-color-primary);
}

.nvl-gallery__thumb-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--nvl-media-focus);
}

/* --- Información y compra -------------------------------------------------- */

/* La columna solo reparte: caja arriba, características debajo. El hueco sube
   a lg porque la sombra de la caja ocupa 14.5px de él; con sm, el encabezado
   «Características» aterrizaría pisado por ella. */
.nvl-product-info {
    display: flex;
    flex-direction: column;
    gap: var(--nvl-space-lg);
}

/* --- CAJA DE COMPRA (owner, 2026-09-22) ---------------------------------
   Adaptación directa de la referencia, escrita con la receta que el sistema ya
   tiene para «panel elevado que no se toca» (.nvl-cart-summary y el diálogo
   del modal). No es un objeto nuevo: es el mismo aplicado a una superficie que
   no lo tenía. Eso es lo que impide que parezca pegada de otra web.

   EL HAIRLINE ES PORTANTE, no decorativo. --nvl-shadow-lg (0 10px 15px -3px)
   no alcanza NADA hacia arriba, así que sin borde el filo superior lo
   definiría solo el salto #ffffff/#f5f5f5, que es 1.09:1 — justo la sensación
   de pegatina. Y en forced-colors el fondo se fuerza a Canvas: el borde es lo
   único que sobrevive, y por eso no hace falta un bloque aparte.

   SIN hover, SIN transición, SIN transform: no es un enlace ni un control.

   Regla del sistema que conviene no perder: en toda caja con overflow y radio,
   padding >= border-radius, o la curva muerde el contenido. Esta caja NO
   recorta, así que la regla no la ata: desde 768 el relleno iguala al radio
   (24 = 24) y por debajo se queda en 20 contra 24 sin que nada se muerda,
   porque lo que hay en las esquinas es texto ya sangrado por el relleno.

   24px DESDE EL 2026-10-24 (owner): misma decisión y mismo porqué que en
   .nvl-gallery__main — las dos tarjetas de la ficha son la pareja que el owner
   marcó, y suben juntas o el par se descuadra. */
.nvl-product-info__buy {
    display: flex;
    flex-direction: column;
    gap: var(--nvl-space-sm);
    padding: var(--nvl-space-md);
    background: var(--nvl-color-surface);
    border: var(--nvl-border-hairline) solid var(--nvl-color-border);
    border-radius: var(--nvl-radius-xl);
    box-shadow: var(--nvl-shadow-lg);
}

/* Fila superior: origen a la izquierda, disponibilidad a la derecha. Donde la
   referencia pone la marca va la procedencia, que aquí sí es argumento de
   venta; la categoría no, porque ya está en las migas.
   Con space-between, si no hay origen la píldora se queda a la derecha sin
   hueco muerto: cero reglas para ese caso. */
.nvl-product-info__brand-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--nvl-space-2xs);
}

.nvl-product-info__brand-row > :last-child {
    margin-inline-start: auto;
}

/* Indicador de existencias: punto de color + texto, el patrón de la referencia
   (owner, 2026-09-22). Sustituye a la pareja píldora ámbar + «Quedan N», que
   decía dos veces lo mismo.

   El texto va en tinta primaria y NO en la secundaria de la referencia: a
   --nvl-text-xs sobre blanco, el #64748b oficial se queda en 4.76:1 y esta es
   la única señal de existencias que le queda a la ficha. */
.nvl-product-info__stock {
    display: inline-flex;
    align-items: center;
    gap: var(--nvl-space-2xs);
    color: var(--nvl-color-text-primary);
    font-size: var(--nvl-text-xs);
    font-weight: var(--nvl-weight-bold);
    font-variant-numeric: tabular-nums;
}

/* 8px con halo de 4px, como el de la referencia. El halo es una derivación por
   ALFA del mismo verde oficial (token ya existente, el de la confirmación del
   pedido): no es un tono nuevo.

   El borde no es decorativo: en forced-colors el sistema descarta background y
   box-shadow, y sin él el punto desaparecería del todo. Aun así el punto es
   aria-hidden y nunca es la única forma de conocer el estado — el texto lo dice
   al lado—, así que cumple 1.4.1 sin depender del color. */
.nvl-product-info__dot {
    flex-shrink: 0;
    inline-size: var(--nvl-space-2xs);
    block-size: var(--nvl-space-2xs);
    border: var(--nvl-border-hairline) solid transparent;
    border-radius: var(--nvl-radius-circle);
    background: var(--nvl-color-success);
    box-shadow: 0 0 0 var(--nvl-border-accent) var(--nvl-color-success-tint);
}

/* Verde también con «pocas unidades», que es lo que hace la referencia: el
   punto dice SI HAY existencias, y cuántas quedan lo dice la cifra de al lado.
   Agotado es el único estado que cambia de color. */
.nvl-product-info__stock--agotado .nvl-product-info__dot {
    background: var(--nvl-color-danger);
    box-shadow: 0 0 0 var(--nvl-border-accent) var(--nvl-color-danger-tint);
}

@media (forced-colors: active) {
    .nvl-product-info__dot {
        border-color: CanvasText;
    }
}

/* --- FORMAS DE PAGO, EN UNA SOLA CAJA (owner, 2026-11-03) ----------------
   Sustituye a las dos mini-tarjetas del 15-oct. El PORQUÉ está en
   components/payment-methods.php; aquí, solo lo que decide la hoja.

   CABECERA SOBRE BANDA Y CUERPO EN BLANCO: los dos niveles se separan por
   SUPERFICIE y no solo por color. Es lo que hace que el bloque se lea como un
   módulo terminado sin confundirse con las cuatro cajas de envío de más abajo
   (.nvl-trust), que son enteras de ese mismo gris.

   LA CAJA NO LLEVA RELLENO PROPIO: lo llevan la banda, la lista y los logos,
   porque el tintado de la cabecera tiene que llegar a los filos.

   NI UNA CONSULTA DE MEDIOS, y eso es una mejora, no un descuido: la rejilla
   50/50 que tenían las dos tarjetas solo se activaba en la banda de una
   columna, que es justo donde fabricaba la lectura de «elige uno». Las filas
   funcionan igual de 320 a 600px.

   EL AZUL ES EL DE MARCA, EN EL PASO QUE AGUANTA EL FONDO CLARO. El
   --nvl-color-accent del héroe (#38bdf8) da 2.14:1 sobre blanco, por debajo del
   3:1 que el tema le exige a un glifo —la regla escrita en el comentario de
   --nvl-color-danger—; allí funciona porque vive sobre tinta oscura. Aquí se usa
   su misma familia Sky dos pasos más abajo: Sky 600 en los glifos (4.1:1) y Sky
   700 en el rótulo (5.35:1 sobre la banda).

   Y «PAGO SEGURO» NO SE QUEDA EN GRIS SECUNDARIO: sobre la banda cae a 4.29:1.
   Sube a tinta primaria (13.19:1), que es la regla que nvl-base.css ya documenta
   para los usos de ese gris que caen sobre banda. */
.nvl-pay-box {
    display: grid;
    border: var(--nvl-border-hairline) solid var(--nvl-color-border);
    border-radius: var(--nvl-radius-md);
    background: var(--nvl-color-surface);
    color: var(--nvl-color-text-primary);
}

.nvl-pay-box__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--nvl-space-2xs);
    padding: var(--nvl-space-2xs) var(--nvl-space-xs);
    border-block-end: var(--nvl-border-hairline) solid var(--nvl-color-border);
    /* El radio de la caja, repetido arriba: sin esto el tintado asoma por fuera
       de la curva en las dos esquinas de arriba. */
    border-start-start-radius: var(--nvl-radius-md);
    border-start-end-radius: var(--nvl-radius-md);
    background: var(--nvl-color-section-alt);
}

/* La receta de .nvl-trust__title, que esta misma caja de compra ya repite
   cuatro veces más abajo: es lo que integra el bloque con la ficha en vez de
   estrenar un rótulo propio. */
.nvl-pay-box__title {
    margin: 0;
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-heading);
    text-transform: uppercase;
    color: var(--nvl-color-primary-dark);
}

/* El sello sube de nivel por POSICIÓN —segundo punto de lectura de la caja—,
   no por color ni por fondo: el verde de la píldora es el único acento
   cromático que esta caja admite. */
.nvl-pay-box__seal {
    display: inline-flex;
    align-items: center;
    gap: var(--nvl-space-3xs);
    margin: 0;
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-regular);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-text-primary);
}

.nvl-pay-methods {
    display: grid;
    gap: var(--nvl-space-3xs);
    margin: 0;
    padding: var(--nvl-space-xs);
    list-style: none;
}

/* FILAS, NO TARJETAS: sin borde, sin fondo y sin relleno propio. Un solo
   contenedor para los dos métodos. */
.nvl-pay-method {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--nvl-space-2xs);
    font-size: var(--nvl-text-xs);
    line-height: var(--nvl-leading-snug);
}

.nvl-pay-box__seal > .nvl-icon,
.nvl-pay-method > .nvl-icon {
    color: var(--nvl-color-primary);
}

.nvl-pay-method__label {
    flex: 1 1 auto;
    min-width: 0;
}

/* AL FILO DERECHO, SOLA: así las condiciones de los dos métodos caen en la
   misma columna y se comparan sin leerlas enteras. Si una no cabe —en móvil,
   con el nombre largo de la tarjeta— baja a su propia línea y se queda a la
   derecha, que es donde el ojo ya la busca. */
.nvl-pay-method__tag {
    margin-inline-start: auto;
    font-size: var(--nvl-text-2xs);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-text-secondary);
    /* El porcentaje no baila al cambiarlo. */
    font-variant-numeric: tabular-nums;
}

/* LA MISMA PÍLDORA QUE EL CHECKOUT (.nvl-pay-option__note--saving): misma
   afirmación, misma forma, en las dos pantallas donde se dice. Sin relleno
   vertical —el alto lo da el interlineado— y el filo como `outline` y no
   `border`, para que la fila no crezca ni un píxel; se ve en forced-colors,
   donde el relleno se descarta. La tinta es la de todas las etiquetas verdes y
   la elige el owner en Configuración del sitio.

   El verde dejó de pintar el método entero (.nvl-pay-method--free, retirada):
   el tinte decía «este método es el bueno» y priorizar uno es decisión del
   checkout, con el total delante. Ahora marca la CONDICIÓN, que es lo único
   cierto aquí. --nvl-color-success-tint no se toca: lo sigue usando la
   confirmación del pedido. */
.nvl-pay-method__tag--free {
    padding-inline: var(--nvl-space-2xs);
    border-radius: var(--nvl-radius-pill);
    outline: var(--nvl-border-hairline) solid transparent;
    background: var(--nvl-color-success);
    color: var(--nvl-color-on-success);
    font-weight: var(--nvl-weight-bold);
    white-space: nowrap;
}

/* Los logos pertenecen al BLOQUE y no al método de tarjeta, que es lo que dice
   el dato: nvl_medios_pago son los medios que acepta la tienda. El filete es
   suyo, y ahí está la gracia — con el repetidor vacío no se imprime la lista, y
   el filete se va con ella: cero CSS de estado vacío. */
.nvl-pay-box .nvl-pay-logos {
    margin: 0 var(--nvl-space-xs) var(--nvl-space-xs);
    padding-block-start: var(--nvl-space-2xs);
    border-block-start: var(--nvl-border-hairline) solid var(--nvl-color-border);
}

/* 24px: más que la «firma» de 20 que tenían dentro de la tarjeta de pago, menos
   que los 32 de cuando iban sueltos bajo el precio. */
.nvl-pay-box .nvl-pay-logos__logo {
    height: var(--nvl-space-lg);
}

/* --- Sub-cajas internas: con tinte y PLANAS ------------------------------
   Como en la referencia: solo flota la caja exterior. Si estas llevaran
   sombra, la ficha sí sería una pared de tarjetas. */

/* Escalera de descuento: la sub-caja de mayoreo de la referencia con nuestro
   mecanismo. --nvl-color-promo-tint está declarado como «fondo del bloque de
   progreso de campaña»: es literalmente su rol. */
.nvl-tier-card {
    padding: var(--nvl-space-sm);
    border: var(--nvl-border-hairline) solid var(--nvl-color-promo-badge);
    border-radius: var(--nvl-radius-md);
    background: var(--nvl-color-promo-tint);
    box-shadow: none;
    color: var(--nvl-color-text-primary);
}

.nvl-tier-card__title {
    margin: 0 0 var(--nvl-space-2xs);
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    text-transform: uppercase;
}

/* Una sola línea de beneficio (owner, 2026-09-22). Sustituye a la lista de
   peldaños: el cliente lee una promoción, no el mecanismo interno de la
   campaña. La cifra va en <b> con la tinta de promoción; el resto, en tinta
   primaria para que la frase se lea como frase y no como dato suelto. */
.nvl-tier-card__headline {
    margin: 0;
    font-size: var(--nvl-text-sm);
    line-height: var(--nvl-leading-snug);
}

.nvl-tier-card__headline b {
    font-weight: var(--nvl-weight-bold);
    color: var(--nvl-color-promo-ink);
    font-variant-numeric: tabular-nums;
}

/* Resumen de la descripción + enlace a la completa. */
.nvl-buy-summary {
    display: flex;
    flex-direction: column;
    gap: var(--nvl-space-3xs);
    align-items: flex-start;
}

.nvl-buy-summary__text {
    margin: 0;
    font-size: var(--nvl-text-sm);
    line-height: var(--nvl-leading-relaxed);
    color: var(--nvl-color-text-primary);
}

.nvl-buy-summary__more {
    font-size: var(--nvl-text-sm);
    font-weight: var(--nvl-weight-regular);
    color: var(--nvl-color-primary-dark);
}

/* Confianza: tinte, plana, y TODA la tinta en primaria — sobre
   --nvl-color-section-alt la secundaria cae a 4.29:1 y no llega a AA. */
/* COMPACTA DE VERDAD (owner, 2026-10-15, segunda pasada). Bajar solo el tamaño
   de la letra apenas movió el alto —de 106 a 100px— porque lo que llenaba la
   caja no era el texto: eran los 32px de relleno vertical, los 8 de hueco y un
   interlineado de 1.6 heredado del cuerpo, pensado para párrafos largos y no
   para una línea de 12px. El relleno baja a 12, el hueco a 4 y cada pieza
   declara su propio interlineado. */
.nvl-trust {
    display: flex;
    flex-direction: column;
    gap: var(--nvl-space-3xs);
    padding: var(--nvl-space-xs);
    border: var(--nvl-border-hairline) solid var(--nvl-color-border);
    border-radius: var(--nvl-radius-md);
    background: var(--nvl-color-section-alt);
    box-shadow: none;
}

.nvl-trust__title {
    margin: 0;
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    /* Una línea en caja alta no necesita el 1.6 del cuerpo: con él, el rótulo
       reservaba 19px para 12 de letra. */
    line-height: var(--nvl-leading-heading);
    text-transform: uppercase;
    color: var(--nvl-color-text-primary);
}

/* 12px y no 14 (owner, 2026-10-15): las cuatro cajas ocupaban más de lo que su
   información pesa y le restaban protagonismo al precio y al botón. Es el suelo
   tipográfico del sistema, el mismo del título de la caja, así que la jerarquía
   la sostienen el peso y la caja alta del título — no el tamaño.
   Único cambio de la tanda: ni relleno, ni huecos, ni colores. */
.nvl-trust__row {
    display: flex;
    align-items: center;
    gap: var(--nvl-space-2xs);
    margin: 0;
    font-size: var(--nvl-text-2xs);
    /* 1.4 y no el 1.6 heredado: en dos líneas de 12px eran 8px de aire que no
       aportaban legibilidad, solo alto. Sigue por encima del 1.2 del rótulo,
       que es de una línea. */
    line-height: var(--nvl-leading-normal);
    color: var(--nvl-color-text-primary);
}

/* Los logos los sube el owner: mientras el repetidor esté vacío, esta lista no
   se imprime. */
/* LOS LOGOS DE PAGO, en la caja de métodos de pago (owner, 2026-10-14). Bajan
   de la caja de confianza a su sitio natural: justo debajo de la frase que dice
   cómo se puede pagar. Misma receta de antes —el alto lo fija el CSS y el ancho
   lo deduce cada logo de su proporción—, con el margen medido contra la frase y
   no contra el final de una caja. */
.nvl-pay-logos {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--nvl-space-2xs);
    margin: var(--nvl-space-2xs) 0 0;
    padding: 0;
    list-style: none;
}

.nvl-pay-logos__logo {
    display: block;
    width: auto;
    /* 32px, no 24 (owner, 2026-09-23): a 24 los logos apenas se distinguían.
       El ancho lo deduce la proporción de cada logo. */
    height: var(--nvl-space-xl);
    object-fit: contain;
}

/* Entrega y garantía: dos mini-tarjetas planas dentro de la caja. */
.nvl-assurance {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--nvl-space-2xs);
    margin: 0;
    padding: 0;
    list-style: none;
}

.nvl-assurance__card {
    display: flex;
    align-items: center;
    gap: var(--nvl-space-2xs);
    padding: var(--nvl-space-2xs) var(--nvl-space-xs);
    border: var(--nvl-border-hairline) solid var(--nvl-color-border);
    border-radius: var(--nvl-radius-md);
    background: var(--nvl-color-surface);
    box-shadow: none;
    font-size: var(--nvl-text-2xs);
    color: var(--nvl-color-text-primary);
}

/* Con una sola tarjeta, media anchura se ve como un hueco. */
.nvl-assurance__card:only-child {
    grid-column: 1 / -1;
}

@media (max-width: 400px) {
    .nvl-assurance {
        grid-template-columns: minmax(0, 1fr);
    }
}

.nvl-product-info__title {
    font-size: var(--nvl-text-2xl); /* H1 vista comercial (auditoría ui-ux §1.5): 2xl → 3xl → 4xl */
    /* 600 y no 900 (owner, 2026-09-22): es el peso medido en el H1 de la ficha
       de referencia. El titular gana elegancia y la ficha entera deja de
       leerse pesada. CONSECUENCIA ASUMIDA: el H1 pasa a pesar lo mismo que los
       H2 de sección, así que a partir de aquí la jerarquía la sostienen el
       tamaño y el color, no el peso. Decisión de alcance: TODO el nivel, no
       solo la ficha. */
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-secondary);
}

.nvl-product-info .nvl-price__amount {
    font-size: var(--nvl-text-2xl);
}


/* Cuántas quedan, junto a la etiqueta de disponibilidad (owner, 2026-09-20).
   SOLO en la ficha y SOLO con el estado "pocas unidades" — lo decide el PHP,
   aquí no hay condición—: la ficha rotula siempre y es donde se elige la
   cantidad, así que la cifra informa.

   SIN fondo y SIN color propio, a propósito: la píldora de disponibilidad que
   va al lado ya lleva el papel cromático y dos chips de color contiguos
   compiten (mismo criterio que el icono del regalo en nvl-order.css). Tinta
   primaria y cifras tabulares. */



/* El ancho lo fija la celda `1fr` de .nvl-add-control--inline, no el botón:
   `min-width: 0` es lo que le permite encoger cuando el stepper entra en la
   fila. Sin él, la celda no baja de su tamaño de contenido y el stepper
   empujaría el botón fuera de la columna. */
.nvl-product-info__add {
    width: 100%;
    min-width: 0;
    padding: var(--nvl-space-sm) var(--nvl-space-lg);
}

@media (min-width: 768px) {
    .nvl-product-info__buy {
        padding: var(--nvl-space-lg);
    }

    .nvl-product-info__title {
        font-size: var(--nvl-text-3xl);
    }

    .nvl-product-info .nvl-price__amount {
        font-size: var(--nvl-text-3xl);
    }
}

@media (min-width: 1280px) {
    .nvl-product-info__title {
        font-size: var(--nvl-text-4xl);
    }
}

/* --- Bloque de atributos por categoría ------------------------------------- */

/* Acento por categoría: cada parcial fija su token; el genérico usa el
   primario. Añadir una categoría con parcial propio = declarar su acento. */
.nvl-attributes {
    --nvl-attr-accent: var(--nvl-color-primary);
}

/* Atributos dentro de la columna de decisión (single-nvl_product.php): el
   parcial imprime su propia <section class="nvl-section">, cuyo relleno de
   banda está pensado para separar franjas a ancho completo y aquí sobra.

   La hairline que los separaba SE RETIRÓ (owner, 2026-09-22): con la caja
   delante, una línea de 1px colgada sobre el gris de página, 24px por debajo
   de un objeto que ya tiene borde y sombra, no separa nada — está agarrada a
   nada. El hueco de la columna hace ese trabajo mejor. */
.nvl-product-info .nvl-attributes {
    padding: 0;
}

/* Las tarjetas de atributo PIERDEN su elevación. Con el escenario y la caja
   flotando, una tercera altura convertiría la ficha en una pared de tarjetas:
   la disciplina no está en no elevar, está en no elevar nada más. Conservan su
   hairline y su filo de acento, que sobre el gris de página bastan. */
.nvl-product-info .nvl-attribute {
    box-shadow: none;
}

/* Una sola columna de tarjetas: en ~490px la rejilla auto-fill de 220px
   metería dos por fila y los valores largos ("tamaño mínimo de acuario")
   quedarían partidos en tres líneas. */
.nvl-product-info .nvl-attributes__grid {
    grid-template-columns: 1fr;
    gap: var(--nvl-space-2xs);
}

/* El título de la sección baja de escalón: aquí no encabeza una franja de la
   página, encabeza un bloque dentro de una columna. */
.nvl-product-info .nvl-attributes__title {
    font-size: var(--nvl-text-md);
    margin-bottom: var(--nvl-space-2xs);
}

.nvl-attributes--peces {
    --nvl-attr-accent: var(--nvl-color-primary);
}

.nvl-attributes--plantas-acuaticas {
    --nvl-attr-accent: var(--nvl-color-success);
}

.nvl-attributes--alimentos {
    --nvl-attr-accent: var(--nvl-color-warning);
}

.nvl-attributes--accesorios {
    --nvl-attr-accent: var(--nvl-color-text-secondary);
}

/* El icono NO lleva el acento de familia: con los colores oficiales planos, el
   verde de plantas da 2.54:1 y el ámbar de alimentos 2.15:1, por debajo del 3:1
   de elemento no textual. El acento sigue identificando la familia en el filo
   de 4px de .nvl-attribute, que sí es decorativo. */
.nvl-attributes__icon {
    color: var(--nvl-color-text-primary);
    flex-shrink: 0;
}

.nvl-attributes__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr));
    gap: var(--nvl-space-sm);
}

.nvl-attribute {
    background: var(--nvl-color-surface);
    border: var(--nvl-border-hairline) solid var(--nvl-color-border);
    /* Filo decorativo: identifica la familia. Con los oficiales planos queda más
       pálido que antes (2.15-2.54 en plantas y alimentos), pero ni la etiqueta
       ni el valor dependen de él para leerse. */
    border-left: var(--nvl-border-accent) solid var(--nvl-attr-accent);
    border-radius: var(--nvl-radius-md);
    padding: var(--nvl-space-sm) var(--nvl-space-md);
    box-shadow: var(--nvl-shadow-sm);
}

.nvl-attribute__label {
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    text-transform: uppercase;
    color: var(--nvl-color-text-secondary);
    margin-bottom: var(--nvl-space-3xs);
}

.nvl-attribute__value {
    /* En REGULAR (tres pesos, 2026-10-17; venía de 500, y antes de 700). Son
       SIETE filas seguidas en la columna de decisión: con peso el bloque se lee
       como una mancha. La jerarquía de la fila la sostiene íntegra su etiqueta
       de arriba (2xs bold en versales). */
    font-weight: var(--nvl-weight-regular);
    color: var(--nvl-color-text-primary);
}

/* --- Colores forzados (galería) -------------------------------------------
   Sin la última regla la miniatura activa es indistinguible: el sistema
   descarta --nvl-color-primary y las ocho quedan con el mismo filo. */
@media (forced-colors: active) {
    .nvl-gallery__main,
    .nvl-gallery__arrow,
    .nvl-gallery__counter,
    .nvl-gallery__discount {
        border-color: CanvasText;
    }

    .nvl-gallery__thumb.nvl-is-selected {
        border-color: Highlight;
    }
}

/* --- Aviso de disponibilidad (components/interest-form.php) ---------------
   owner, 2026-10-28.

   Ocupa en la caja de compra el sitio exacto del control de agregado, porque
   en un pez agotado ES la acción de la ficha. Por eso tiene el peso de una
   caja y no el de una nota al pie: si se leyera como un aviso secundario,
   nadie dejaría su correo.

   RECETA HERMANA, NO COMPARTIDA. El campo se ve como el del checkout, pero no
   reutiliza .nvl-checkout-field: esa base vive en nvl-checkout-page.css, que
   NO se encola en la ficha, y moverla tocaría una cascada ya medida. Lo que se
   comparte son los TOKENS (--nvl-field-h, --nvl-color-border-field,
   --nvl-label-float-scale…). Consecuencia honesta: un cambio en una receta no
   viaja a la otra, y hay que hacerlo dos veces a propósito. */

/* DESTACADA, NO UNA CAJA MÁS (owner, 2026-10-28, 1.ª revisión en staging).
   Nació con el filete gris y el fondo de sección de todas las demás cajas de
   la columna, y ahí se perdía. El trazo pasa al PRIMARIO: mismo grosor —el
   borde sigue siendo sutil— pero el único de la columna que no es gris, que es
   lo que la separa de las tarjetas de método de pago y de la de descuento. */
.nvl-interest {
    padding: var(--nvl-space-sm);
    border: var(--nvl-border-hairline) solid var(--nvl-color-primary);
    border-radius: var(--nvl-radius-lg);
    background: var(--nvl-color-section-alt);
}

.nvl-interest__title {
    margin: 0 0 var(--nvl-space-3xs);
    font-size: var(--nvl-text-md);
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-text-primary);
}

.nvl-interest__lead {
    margin: 0 0 var(--nvl-space-sm);
    font-size: var(--nvl-text-sm);
    line-height: var(--nvl-leading-normal);
    color: var(--nvl-color-text-secondary);
}

/* En columna SIEMPRE: este bloque vive dentro de .nvl-product-info, que es la
   mitad estrecha de la ficha. Un campo y un botón uno al lado del otro ahí
   dejarían el botón sin ancho para su frase completa. */
.nvl-interest__row {
    display: grid;
    gap: var(--nvl-space-xs);
}

.nvl-interest__field {
    position: relative;
}

.nvl-interest__control {
    display: block;
    width: 100%;
    min-height: var(--nvl-field-h);
    /* Arriba cabe la etiqueta flotante; abajo, el aire del valor. Mismo
       cálculo que el campo del checkout. */
    padding: calc(var(--nvl-space-2xs) + var(--nvl-text-2xs) * var(--nvl-leading-snug)) var(--nvl-space-xs) var(--nvl-space-2xs);
    border: var(--nvl-border-hairline) solid var(--nvl-color-border-field);
    border-radius: var(--nvl-radius-md);
    background: var(--nvl-color-surface);
    /* 16px: menos y iOS hace zoom al enfocar. */
    font-size: var(--nvl-text-base);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-text-primary);
}

.nvl-interest__control[aria-invalid="true"] {
    border-color: var(--nvl-color-danger);
    box-shadow: 0 0 0 var(--nvl-border-hairline) var(--nvl-color-danger);
}

/* FOCO EN EL BORDE, NO EN UN ANILLO APARTE (owner, 2026-10-28, 2.ª revisión en
   staging). El :focus-visible global de nvl-base.css dibuja un trazo de 2px
   separado 2px del campo, y alrededor de una caja redondeada eso se lee como un
   borde azul doble y grueso. Es LA MISMA queja —y la misma solución— del
   2026-10-07 en los campos del checkout: un solo trazo, hecho de borde + sombra
   de un hairline, sin mover el texto.

   El foco NO se quita: un campo de formulario sin indicador visible deja fuera
   a quien navega con teclado. El outline se conserva transparente y pegado
   porque en forced-colors el navegador descarta las sombras y es lo único que
   queda. :focus y no :focus-visible, igual que el checkout: todo campo
   enfocado se marca igual, venga del teclado o del ratón. */
.nvl-interest__control:focus {
    border-color: var(--nvl-color-primary);
    box-shadow: 0 0 0 var(--nvl-border-hairline) var(--nvl-color-primary);
    outline: var(--nvl-focus-width) solid transparent;
    outline-offset: 0;
}

/* Enviado: el campo se bloquea y deja de pedir atención. */
.nvl-interest__control:disabled {
    background: var(--nvl-color-background);
    color: var(--nvl-color-text-secondary);
}

.nvl-interest__label {
    position: absolute;
    top: var(--nvl-space-2xs);
    inset-inline-start: calc(var(--nvl-space-xs) + var(--nvl-border-hairline));
    max-width: calc(100% - var(--nvl-space-xs) * 2);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--nvl-text-base);
    font-weight: var(--nvl-weight-regular);
    line-height: 1;
    color: var(--nvl-color-text-secondary);
    transform-origin: left top;
    /* En reposo hace de placeholder: centrada en el campo. */
    transform: translateY(calc((var(--nvl-field-h) - var(--nvl-border-hairline) * 2) / 2 - var(--nvl-space-2xs) - var(--nvl-text-base) / 2));
    transition: transform var(--nvl-duration-fast) var(--nvl-ease-standard);
    /* El clic pasa al campo, que está debajo. */
    pointer-events: none;
}

.nvl-interest__field:has(.nvl-interest__control:focus) .nvl-interest__label,
.nvl-interest__field:has(.nvl-interest__control:not(:placeholder-shown)) .nvl-interest__label,
.nvl-interest__field:has(.nvl-interest__control:autofill) .nvl-interest__label {
    transform: scale(var(--nvl-label-float-scale));
}

/* SIN `display` DECLARADO, Y ES LA CORRECCIÓN DE UN DEFECTO (2026-10-28).
   Esta regla tenía `display: flex` para colocar un glifo, y eso GANABA sobre el
   `display: none` que el navegador aplica a [hidden]: el aviso de error se veía
   SIEMPRE, vacío, en toda ficha agotada. Es la misma trampa que ya documenta
   .nvl-cart-summary__row en nvl-cart-summary.css. Un <p> ya es bloque, así que
   aquí no hace falta ningún `display`: sin declararlo, [hidden] funciona.

   Y SIN GLIFO ROJO (owner, en esa misma revisión): el punto de exclamación en
   círculo rojo leía como alerta en una caja que no la tiene. El mensaje se
   sostiene solo —dice qué falta, en tinta normal— y la señal no visual la da
   aria-invalid en el campo, con su borde rojo. */
.nvl-interest__error {
    margin: var(--nvl-space-3xs) 0 0;
    font-size: var(--nvl-text-2xs);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-text-primary);
}

/* REGISTRADO: el botón dice qué pasó, en verde (owner, 2026-10-28). Sigue
   deshabilitado —no hay nada más que pulsar—, así que esta regla tiene que
   ganarle a .nvl-button:disabled, que lo apagaría en gris: lo hace por
   especificidad (dos clases y la pseudoclase contra una clase y la
   pseudoclase), no por orden de cascada, para que no dependa de en qué hoja
   acabe cada una.

   Verde de RELLENO con la tinta oscura encima (7.04:1), que es el único uso
   permitido de --nvl-color-success: como tinta sobre claro no llega (2.54:1). */
.nvl-interest__submit.nvl-is-done:disabled {
    background: var(--nvl-color-success);
    color: var(--nvl-color-on-success);
    border-color: var(--nvl-color-success);
    cursor: default;
}

/* La región viva está SIEMPRE en el árbol: se vacía, no se quita. Una región
   que entra y sale del DOM no se anuncia de forma fiable, que es lo que ya
   documenta el resumen de errores del checkout. */
.nvl-interest__status {
    margin: var(--nvl-space-xs) 0 0;
    font-size: var(--nvl-text-sm);
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-primary);
}

.nvl-interest__status:empty {
    display: none;
}

.nvl-interest__note {
    margin: var(--nvl-space-xs) 0 0;
    font-size: var(--nvl-text-2xs);
    line-height: var(--nvl-leading-normal);
    color: var(--nvl-color-text-secondary);
}

/* La trampa del formulario. No se usa una clase del tipo "solo para lectores":
   eso sigue siendo enfocable y anunciable, y aquí hace falta justo lo
   contrario. El tabindex y el aria-hidden los pone el marcado. */
.nvl-interest__trap {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* Sin --nvl-color-border el bloque desaparece contra el fondo del sistema. */
@media (forced-colors: active) {
    .nvl-interest,
    .nvl-interest__control {
        border-color: CanvasText;
    }
}

/* --- Desplegable de tallas (components/variant-select.php) ---------------
   owner, 2026-10-30.

   Receta HERMANA de la del campo del checkout, no compartida: aquella vive en
   nvl-checkout-page.css, que no se encola en la ficha. Se reutilizan los
   tokens, no los selectores, igual que con el campo del aviso de
   disponibilidad. Un cambio en una no viaja a la otra. */

.nvl-variant {
    margin-bottom: var(--nvl-space-sm);
}

.nvl-variant__field {
    position: relative;
}

.nvl-variant__control {
    display: block;
    width: 100%;
    min-height: var(--nvl-field-h);
    padding: calc(var(--nvl-space-2xs) + var(--nvl-text-2xs) * var(--nvl-leading-snug)) var(--nvl-space-xs) var(--nvl-space-2xs);
    padding-inline-end: calc(var(--nvl-space-xs) * 2 + var(--nvl-icon-inline));
    appearance: none;
    cursor: pointer;
    border: var(--nvl-border-hairline) solid var(--nvl-color-border-field);
    border-radius: var(--nvl-radius-md);
    background: var(--nvl-color-surface);
    /* 16px: menos y iOS hace zoom al enfocar. */
    font-size: var(--nvl-text-base);
    line-height: var(--nvl-leading-snug);
    color: var(--nvl-color-text-primary);
}

/* Foco en el borde, no en un anillo aparte: la misma decisión que el campo del
   aviso de disponibilidad y la del checkout. El outline se conserva
   transparente porque en forced-colors es lo único que queda. */
.nvl-variant__control:focus {
    border-color: var(--nvl-color-primary);
    box-shadow: 0 0 0 var(--nvl-border-hairline) var(--nvl-color-primary);
    outline: var(--nvl-focus-width) solid transparent;
    outline-offset: 0;
}

/* La etiqueta va SIEMPRE arriba: un select muestra texto desde el primer
   momento, así que nunca puede hacer de placeholder. */
.nvl-variant__label {
    position: absolute;
    top: var(--nvl-space-2xs);
    inset-inline-start: calc(var(--nvl-space-xs) + var(--nvl-border-hairline));
    max-width: calc(100% - var(--nvl-space-xs) * 2);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--nvl-text-base);
    font-weight: var(--nvl-weight-regular);
    line-height: 1;
    color: var(--nvl-color-text-secondary);
    transform-origin: left top;
    transform: scale(var(--nvl-label-float-scale));
    pointer-events: none;
}

/* El chevron del inventario cerrado, no uno nuevo. No recibe el clic: el
   objetivo sigue siendo el propio select. */
.nvl-variant__field > .nvl-icon {
    position: absolute;
    inset-inline-end: var(--nvl-space-xs);
    top: calc(var(--nvl-field-h) / 2);
    transform: translateY(-50%);
    color: var(--nvl-color-text-secondary);
    pointer-events: none;
}

/* «Desde» en la tarjeta del catálogo: acompaña al precio mínimo de un producto
   con tallas. Más pequeño y secundario — la cifra es lo que se lee. */
.nvl-price__from {
    margin-inline-end: var(--nvl-space-3xs);
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-regular);
    color: var(--nvl-color-text-secondary);
}

@media (forced-colors: active) {
    .nvl-variant__control {
        border-color: CanvasText;
    }
}
