/* =========================================================================
   TARJETA DE PRODUCTO — parrilla, tarjeta, agotado, control de agregado, precio,
   disponibilidad y etiqueta de origen

   Todo lo que pinta un producto en un listado o en la ficha. El control de
   agregado se especifica una sola vez y lo comparten catálogo, Home y ficha.

   Parte de la cadena de hojas de componentes. El ORDEN DE CASCADA lo
   garantiza la cadena de dependencias declarada en inc/assets.php: ninguna
   vista encola estas hojas directamente, todas dependen del handle
   'nvl-components', que cierra la cadena. Mover una regla de archivo puede
   cambiar qué gana; mover un archivo de sitio en la cadena, también.

   Mobile-first. Todo valor visual es token var(--nvl-*).
   ========================================================================= */

/* --- Grid de productos --------------------------------------------------- */

.nvl-product-grid {
    list-style: none;
    display: grid;
    /* Cuentas EXPLÍCITAS, no `auto-fill`: el número de columnas es una
       decisión, no una propiedad emergente de un track mínimo. Con auto-fill
       no hay un mínimo que dé tres columnas en escritorio sin dejar una sola
       a 600px de viewport, donde caben dos holgadas.
       Tres columnas sobre 1136px útiles dan tarjetas de 362,7px y, en 16:9,
       una foto de 204px — la misma banda de 200px que la landing de
       referencia renderiza en sus tarjetas de ~356px. */
    grid-template-columns: minmax(0, 1fr);
    gap: var(--nvl-space-md);
}

@media (min-width: 640px) {
    .nvl-product-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

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

@media (min-width: 1024px) {
    .nvl-product-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

.nvl-product-grid__item {
    display: flex;
}

/* --- Tarjeta de producto --------------------------------------------------
   Markup de components/product-card.php SIN CAMBIOS: las variantes de precio se
   resuelven aquí por contexto, nunca añadiendo clases nuevas al PHP.

   El estado AGOTADO ya no se resuelve con CSS (owner, 2026-11-05): no pinta
   nada distinto. Lo dicen la cinta y el botón deshabilitado, y los dos salen
   del servidor. Ver el bloque del final de esta hoja. */

.nvl-product-card {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    background: var(--nvl-color-surface);
    border-radius: var(--nvl-radius-lg);
    box-shadow: var(--nvl-shadow-md);
    overflow: hidden;
    transition:
        transform var(--nvl-duration-slow) var(--nvl-ease-out),
        box-shadow var(--nvl-duration-slow) var(--nvl-ease-out);
}

@media (hover: hover) and (pointer: fine) {
    .nvl-product-card:hover {
        transform: translateY(-4px);
        box-shadow: var(--nvl-shadow-xl);
    }

    .nvl-product-card:hover .nvl-product-card__image {
        transform: scale(1.05);
    }
}

/* La tarjeta NO pinta foco. Lo hacía con :focus-within, que no distingue
   teclado de ratón: bastaba hacer clic en cualquier botón interior para dejar
   la tarjeta entera rodeada de azul. Cada control conserva su propio anillo
   por el :focus-visible global de nvl-base.css, que sí lo distingue, y el
   enlace de la imagen va con tabindex="-1", así que los focos reales son el
   título y los tres botones. Misma decisión que la landing de referencia. */

.nvl-product-card:active {
    transform: translateY(-1px) scale(0.995);
}

.nvl-product-card__media {
    position: relative;
    display: block;
    aspect-ratio: var(--nvl-ratio-product);
    background: var(--nvl-color-section-alt);
    overflow: hidden;
}

.nvl-product-card__image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: var(--nvl-media-focus);
    transition: transform var(--nvl-duration-slower) var(--nvl-ease-out);
}

.nvl-product-card__image--empty {
    display: block;
}

.nvl-product-card__body {
    display: flex;
    flex-direction: column;
    /* Vuelve a sm/md y 2xs, revirtiendo la subida de la auditoría ui-ux
       §4.1/§8.2. No es un cambio de opinión: cambió el supuesto. Aquella
       corrección de saturación se tomó cuando el stepper estaba oculto y se
       revelaba al agregar; hoy el bloque de acciones es permanente y aporta
       ~132px fijos, así que el aire vertical sobra. El lateral se conserva
       íntegro: es el que se lee como premium.
       Los tres datos —nombre, precio y disponibilidad— son un dato compuesto;
       a 12px de separación se leían como tres bloques sueltos. */
    gap: var(--nvl-space-2xs);
    padding: var(--nvl-space-sm) var(--nvl-space-md);
    flex-grow: 1;
}

/* Fila de meta del cuerpo: estado (solo "pocas unidades") y procedencia.
   El badge de origen vive aquí y no superpuesto a la foto: es un argumento de
   venta, así que su vecino natural es el precio, no la imagen, y sobre la foto
   se leía antes que el nombre del producto. (Tuvo un tercer motivo —se
   desaturaba con la media agotada— que murió al retirarse ese filtro el
   2026-11-05; los dos primeros siguen en pie y bastan.) */
.nvl-product-card__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--nvl-space-2xs);
}

.nvl-product-card__title {
    font-size: var(--nvl-text-base);
    font-weight: var(--nvl-weight-regular); /* Título de tarjeta: baja a regular con los tres pesos (2026-10-17) — dentro de la tarjeta manda el precio, el nombre es la etiqueta */
    line-height: var(--nvl-leading-heading);
    overflow-wrap: anywhere;
}

@media (min-width: 768px) {
    .nvl-product-card__title {
        font-size: var(--nvl-text-md);
    }
}

/* Tarjeta compacta (parrilla de destacados de la Home, .nvl-product-grid--compact) */
.nvl-product-grid--compact .nvl-product-card__title {
    font-size: var(--nvl-text-sm);
}

@media (min-width: 768px) {
    .nvl-product-grid--compact .nvl-product-card__title {
        font-size: var(--nvl-text-base);
    }
}

.nvl-product-card__link {
    color: var(--nvl-color-text-primary);
    text-decoration: none;
    transition: color var(--nvl-duration-base) var(--nvl-ease-standard);
}

.nvl-product-card__link:hover {
    color: var(--nvl-color-primary-dark);
}

.nvl-product-card__actions {
    display: flex;
    flex-direction: column;
    padding: var(--nvl-space-sm) var(--nvl-space-md);
    border-top: var(--nvl-border-hairline) solid var(--nvl-color-border);
    background: var(--nvl-color-background);
}

.nvl-product-grid--compact .nvl-product-card__actions {
    padding: var(--nvl-space-xs);
}

/* Densidad compacta preservada (§8.2): la corrección de padding/gap del
   cuerpo de la tarjeta es para el catálogo principal, no para la parrilla de
   destacados de la Home. */
.nvl-product-grid--compact .nvl-product-card__body {
    padding: var(--nvl-space-sm) var(--nvl-space-md);
    gap: var(--nvl-space-2xs);
}

.nvl-product-card__add {
    width: 100%;
    padding: var(--nvl-space-xs) var(--nvl-space-sm);
}

/* --- Cinta de agotado (§4.6) — texto real en HTML, nunca content: -------- */

/* Cuelga de .nvl-product-card y no de __media. El motivo original ya no
   existe: la media agotada llevaba `filter: grayscale(1)`, ese filtro alcanzaba
   a todo su subárbol sin forma de revertirlo desde dentro, y ahí la cinta se
   veía gris. El filtro se retiró el 2026-11-05 (ver más abajo), así que esto es
   HISTORIA, no una restricción vigente — que nadie la lea como si lo fuera.
   Se queda donde está porque .nvl-product-card es su contenedor posicionado y
   el sitio correcto para la cinta de todas formas. */
.nvl-product-card__ribbon {
    position: absolute;
    z-index: var(--nvl-z-base);
    inset-block-start: var(--nvl-space-md);
    inset-inline-end: calc(var(--nvl-space-xl) * -1);
    width: calc(var(--nvl-space-5xl) + var(--nvl-space-lg));
    padding: var(--nvl-space-3xs) var(--nvl-space-2xs);
    transform: rotate(45deg);
    background: var(--nvl-color-danger);
    /* Texto BLANCO, como la plantilla de referencia (fuente de verdad, decisión
       del owner 2026-09-16). Da 3.76:1 sobre este rojo: por debajo de AA para
       12px bold; se acepta de forma consciente para replicar la referencia. */
    color: var(--nvl-color-surface);
    text-align: center;
    box-shadow: var(--nvl-shadow-md);
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-snug);
    text-transform: uppercase;
}

/* --- Control de agregado: selector de cantidad + botón (components/add-control.php)
   Bloque compartido por la tarjeta del catálogo, la parrilla compacta de la
   Home y la ficha: se especifica una sola vez aquí y se reutiliza en las tres
   vistas.

   EL REVELADO NO SE ANIMA. No es una preferencia estética: el revelado animado
   del stepper hacía crecer la columna de decisión de la ficha, que es una caja
   con scroll propio (nvl-product.css, .nvl-product-info), y el desbordamiento
   momentáneo dibujaba barras de scroll durante la transición. La fila aparece
   y desaparece de golpe, sin fotogramas intermedios — si alguna transición de
   layout vuelve aquí, el defecto vuelve con ella.

   Estados de la raíz, todos los pone assets/js/nvl-cart.js (el servidor
   siempre imprime el de reposo, porque el carrito vive en localStorage):
   - sin clase      el producto no está en el carrito: solo el botón.
   - .nvl-has-qty   hay línea en el carrito: stepper visible.
   - .nvl-is-update el botón confirmará una cantidad nueva.

   No hay estado de "quitar": bajar el borrador a 0 con el − saca el producto
   en el acto y el bloque vuelve a reposo (assets/js/nvl-cart.js).

   Disposición: apilada por defecto (la tarjeta no tiene ancho para más: con
   280px de columna el stepper se lleva la mitad) y en una sola fila con el
   modificador --inline, que usa la ficha. -------------------------------- */

.nvl-add-control {
    display: flex;
    flex-direction: column;
    gap: var(--nvl-space-xs);
}

/* Colapsada en reposo con display:none y no con visibility/opacity: así no
   reserva hueco, no deja el gap del flex column colgando y saca los pasos
   − / + del orden de tabulación sin depender de otra propiedad. */
.nvl-add-control__qty {
    display: none;
    align-items: center;
    justify-content: space-between;
    gap: var(--nvl-space-2xs);
}

.nvl-add-control.nvl-has-qty .nvl-add-control__qty {
    display: flex;
}

.nvl-add-control__label {
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    text-transform: uppercase;
    color: var(--nvl-color-text-primary); /* sube de la tinta secundaria: sobre banda, el #64748b oficial da 4.36:1 — franja de acciones (#f5f5f5), y es text-2xs: el peor caso */
}

/* .nvl-add-control__stepper y .nvl-add-control__step viven en nvl-elements.css,
   compartidos con el stepper de la línea del carrito: una sola anatomía para el
   mismo control. No se redeclaran aquí — esta hoja va después en la cascada y
   los pisaría.

   RETIRADO (owner, 2026-09-22): aquí había un `.nvl-add-control__stepper` con
   `display:flex` y `gap: var(--nvl-space-2xs)` que hacía exactamente lo que el
   comentario de al lado prohibía: pisaba el `gap: 0` del grupo. Los tres
   segmentos quedaban separados 8px Y ADEMÁS divididos por hairlines, que es lo
   que hacía ver el control pesado y despiezado. */

.nvl-add-control__value {
    min-width: var(--nvl-space-lg);
    text-align: center;
    font-size: var(--nvl-text-md);
    /* 400 y no 700 (owner, 2026-09-22): es el peso medido en el control de la
       referencia, a este mismo tamaño. La cifra no es un dato que haya que
       destacar sobre el precio; es el estado de un control. `tabular-nums`
       sigue sosteniendo que no salte de ancho al cambiar de dígito. */
    font-weight: var(--nvl-weight-regular);
    line-height: var(--nvl-leading-snug);
    font-variant-numeric: tabular-nums;
}

/* El botón NO conmuta a un acuse: ni color, ni tamaño. Antes pasaba a un
   "Agregado" verde durante medio segundo y eso se leía como un parpadeo, como
   si el carrito se estuviera corrigiendo solo. El acuse vive donde el usuario
   ya sabe que vive el carrito: el aviso temporal (components/cart-toast.php) y
   el contador de la cabecera (.nvl-cart-launcher__count en nvl-base.css).

   Lo que SÍ cambia es la etiqueta, porque el botón hace dos cosas distintas
   según el estado y tiene que decir cuál. Las dos se imprimen en el HTML
   —traducibles, con su icono— y aquí se muestra una sola: el JS no construye
   texto ni iconos, solo cambia la clase de la raíz. El ancho no baila: el
   botón es width:100% en las dos disposiciones. */
.nvl-add-control__label-add {
    display: none;
    align-items: center;
    gap: var(--nvl-space-2xs);
    pointer-events: none; /* el target del clic es siempre el botón */
}

.nvl-add-control [data-nvl-add-label="add"],
.nvl-add-control.nvl-is-update [data-nvl-add-label="update"] {
    display: inline-flex;
}

.nvl-add-control.nvl-is-update [data-nvl-add-label="add"] {
    display: none;
}

/* --- Variante en línea: stepper y botón en una misma fila ------------------
   La pide la ficha (single-nvl_product.php pasa 'modifier'); la tarjeta del
   catálogo y la parrilla de la Home NO la usan y conservan el apilado, porque
   con 280px de columna el botón se quedaría sin ancho útil.

   En reposo el botón ocupa la fila entera (una sola columna) y al entrar el
   stepper la rejilla pasa a `auto 1fr` de golpe, SIN transición: el stepper
   mide lo que mide y el botón se lleva el resto. La celda del botón encoge
   gracias al `min-width: 0` de .nvl-product-info__add (nvl-product.css). */
/* `stretch` y no `center` (owner, 2026-09-22): el alto del botón NO lo fija su
   texto sino su icono —24px contra 19.2px de línea—, así que el botón mide 59px
   y el stepper 47px por su objetivo táctil. Centrados, los dos filos quedaban
   descuadrados 6px arriba y 6px abajo, que es justo lo que delata un control
   pegado. Estirando, el stepper copia el alto del botón sea cual sea su
   contenido: si la etiqueta pasa a dos renglones en un ancho estrecho, los dos
   crecen juntos. Los 44×44 no se tocan, solo crecen. */
.nvl-add-control--inline {
    display: grid;
    grid-template-columns: 1fr;
    align-items: stretch;
    gap: var(--nvl-space-xs);
}

.nvl-add-control--inline.nvl-has-qty {
    grid-template-columns: auto 1fr;
}

/* El estirado de arriba muere en este envoltorio, que es flex y centra a sus
   hijos: sin esta línea el stepper sigue midiendo sus 47px dentro de una celda
   de 59. En la tarjeta apilada NO se toca —ahí el stepper comparte fila con la
   etiqueta y estirarlo la deformaría—, por eso va colgado de la variante. */
.nvl-add-control--inline .nvl-add-control__qty {
    align-items: stretch;
}

/* La etiqueta "Cantidad" no cabe en un renglón compartido con el botón y no
   hace falta: el stepper es un role="group" con nombre propio y los dos pasos
   llevan aria-label completo. Se oculta solo aquí; la tarjeta la conserva. */
.nvl-add-control--inline .nvl-add-control__label {
    display: none;
}

/* --- AGOTADO: LA TARJETA NO SE APAGA (owner, 2026-11-05) ------------------
   Aquí vivían tres reglas que, con el producto sin existencias, pintaban la
   media en `filter: grayscale(1)` con `opacity`, bajaban el precio a gris
   secundario y apagaban la insignia «-X%». Las tres SE RETIRARON.

   El motivo es de negocio y no de gusto: en este catálogo la foto ES el
   producto, y una tarjeta desaturada no se lee «agotado», se lee «rota». Dos
   tarjetas así en una parrilla de tres hunden la fila entera.

   UN PRODUCTO AGOTADO SE COMUNICA CON DOS COSAS, Y NINGUNA TOCA LA FOTO:
     1. la cinta roja AGOTADO (aquí abajo, con su texto real en el HTML);
     2. el botón deshabilitado DE VERDAD, con su atributo `disabled` puesto en
        el servidor (components/add-control.php).

   Así que no se añade nada aquí: lo que hacía falta ya existía, y lo que
   sobraba era esto. Si alguien echa de menos una señal más, la respuesta es que
   el owner pidió exactamente estas dos.

   El modificador --unavailable sigue en el marcado como gancho de estado, pero
   desde hoy NINGUNA regla cuelga de él: antes de volver a usarlo, conviene
   releer este comentario. */

/* --- Precio -----------------------------------------------------------
   Identidad deliberada (ver docs/home-especificacion-visual.md §5.3): el
   precio en reposo pasa de --nvl-color-primary a tinta casi negra, para que
   el precio rebajado destaque de verdad. Aplica a Home, categoría y ficha. */

.nvl-price {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--nvl-space-2xs);
    margin-top: auto;
}

.nvl-price__amount {
    color: var(--nvl-color-secondary);
    font-weight: var(--nvl-weight-extrabold);
    font-size: var(--nvl-text-lg);
    line-height: var(--nvl-leading-snug);
    font-variant-numeric: tabular-nums;
}

.nvl-price--sale .nvl-price__amount {
    color: var(--nvl-color-promo-ink);
}

.nvl-price__original {
    color: var(--nvl-color-text-secondary);
    font-size: var(--nvl-text-sm);
    font-weight: var(--nvl-weight-regular);
    line-height: var(--nvl-leading-snug);
    text-decoration-thickness: 1px;
}

/* Precio con campaña de descuento vigente (Etapa 2, Fase 2) */
.nvl-price--campaign .nvl-price__amount {
    color: var(--nvl-color-promo-ink);
}

.nvl-price__campaign-badge {
    display: inline-flex;
    align-items: center;
    padding: var(--nvl-space-3xs) var(--nvl-space-2xs);
    border-radius: var(--nvl-radius-pill);
    background: var(--nvl-color-promo-badge);
    color: var(--nvl-color-surface);
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    white-space: nowrap;
}

/* Descuento PROGRESIVO visible (components/price.php, rama --tier): réplica
   de la plantilla de referencia. Insignia «-X%» a la izquierda y, a su lado en
   columna, el precio sin descuento tachado sobre el rebajado. nvl-cart.js
   (renderTierPrices) actualiza porcentaje y precio en vivo. */
.nvl-price--tier {
    align-items: center;
    flex-wrap: nowrap;
}

.nvl-price__tier-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    min-inline-size: var(--nvl-space-3xl);
    padding: var(--nvl-space-3xs) var(--nvl-space-2xs);
    border: var(--nvl-border-hairline) solid transparent; /* sostiene la forma en forced-colors */
    border-radius: var(--nvl-radius-sm);
    background: var(--nvl-color-promo-badge);
    color: var(--nvl-color-surface); /* 6.29:1 */
    box-shadow: 0 2px 4px rgb(79 70 229 / 0.2); /* derivación por alfa del índigo oficial, como la referencia */
    font-size: var(--nvl-text-sm);
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-snug);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.nvl-price__stack {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}

.nvl-price--tier .nvl-price__original {
    line-height: 1;
}

.nvl-price--tier .nvl-price__amount {
    color: var(--nvl-color-promo-ink);
}

/* --- Disponibilidad ------------------------------------------------------ */

/* Tercera línea de la jerarquía, detrás del nombre y del precio: baja a xs
   para no pesar lo mismo que el cuerpo. El señalamiento lo da el color, no el
   tamaño. */
/* CHIP DE ESTADO. El color oficial pasa de tinta a RELLENO, con la tinta en
   --nvl-color-secondary: es la única forma de usar el verde, el ámbar y el rojo
   de la paleta sin quedar por debajo del mínimo legible. Como tinta sobre claro
   daban 2.54, 2.15 y 3.76; como relleno dan 7.04, 8.31 y 4.74.

   Radio CUADRADO, igual que .nvl-origin-badge: la píldora redonda queda
   reservada al badge de campaña, y forma distinta = naturaleza distinta. Un chip
   de disponibilidad redondo se leería como una segunda oferta en la columna.

   Baja de text-xs semibold a text-2xs bold: el chip ocupa más caja que el texto
   suelto y al mismo tamaño competiría con el precio. El relleno compensa de
   sobra. El punto de color desapareció del markup: con relleno era redundante.

   Esta anatomía la comparten el importe de descuento, la línea de regalo, el
   envío gratis y el ahorro por transferencia. Un solo sistema, no cinco parches. */
.nvl-availability {
    display: inline-flex;
    align-items: center;
    padding: var(--nvl-space-3xs) var(--nvl-space-2xs);
    border-radius: var(--nvl-radius-xs);
    border: var(--nvl-border-hairline) solid transparent; /* en forced-colors el relleno se descarta y sin borde el chip se aplana */
    color: var(--nvl-color-secondary);
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold);
    line-height: var(--nvl-leading-snug);
    text-transform: uppercase;
}

.nvl-availability--disponible {
    background: var(--nvl-color-success);
    color: var(--nvl-color-on-success); /* blanco u oscuro según Configuración del sitio (token en nvl-base.css) */
}

.nvl-availability--pocas_unidades {
    background: var(--nvl-color-warning);
}

.nvl-availability--agotado {
    background: var(--nvl-color-danger);
}

/* --- Etiqueta de origen -------------------------------------------------- */

/* Sello, no sticker. Radio CUADRADO a propósito: la píldora queda reservada al
   badge de campaña, y forma distinta = naturaleza distinta —atributo permanente
   frente a promoción temporal—. Dos píldoras en la misma columna se leerían como
   dos ofertas. Sin sombra: existía para despegarlo de la foto, y sobre
   superficie blanca es suciedad. */
.nvl-origin-badge {
    display: inline-flex;
    align-items: center;
    padding: var(--nvl-space-3xs) var(--nvl-space-2xs);
    border-radius: var(--nvl-radius-xs);
    background: var(--nvl-color-section-alt);
    color: var(--nvl-color-primary-dark); /* 5.35:1 sobre --nvl-color-section-alt — AA */
    font-size: var(--nvl-text-2xs);
    font-weight: var(--nvl-weight-bold); /* micro-etiqueta: sin caja alta separada, el trazo a 12px lo sostiene el peso */
    line-height: var(--nvl-leading-snug);
    text-transform: uppercase;
}
