/* ============================================================
   MOVIMIENTO — este proyecto NO respeta `prefers-reduced-motion`.

   Es deliberado, no un olvido: se decidió el 2026-08-28 después de
   comprobar que en una máquina con "reducir movimiento" activado el
   sitio se veía sin una sola animación fuera del Hero y parecía roto
   —esta card, en particular, no hacía zoom al pasar el mouse—.

   La contrapartida está asumida: alguien que activó esa preferencia
   porque el movimiento en pantalla lo marea, acá va a ver las
   animaciones igual.

   NO repongas el bloque `@media (prefers-reduced-motion: reduce)` que
   estaba acá abajo. Si hay que revertirlo, está en el historial de
   git: el commit anterior a "forzar animaciones".

   Ojo con lo que SÍ hay que dejar: los `@media (hover: hover)` de este
   archivo son otra cosa —evitan que la card quede pegada en estado
   hover después de un toque en pantalla táctil— y sacarlos rompe el
   sitio en celulares.
   ============================================================ */

/* ============================================================
   PRODUCT CARD — componente compartido
   Usado por: index.html (Últimos lanzamientos) y catalogo.html
   (Ofertas destacadas + grilla del catálogo).

   ÚNICA implementación del hover del producto. El comportamiento es
   el que ya tenía la grilla del catálogo —escala 1.03, elevación
   profunda y neutra, 0.4s— y ahora es el mismo en ambas páginas.

   Requiere las variables de :root de cada página (--surface, --accent,
   --border, --radius…), que son idénticas en landing y catálogo.

   IMPORTANTE — orden de carga: este archivo debe enlazarse DESPUÉS de
   aos.css. AOS genera reglas con selectores de especificidad alta
   ([data-aos][data-aos][data-aos-duration='600'] → 0,3,0) que pisarían
   la transición de la card. Por eso las reglas de aquí repiten la clase.
   ============================================================ */

.product-card {
    position: relative;
    background: linear-gradient(170deg, rgba(23,14,38,0.92) 0%, rgba(12,6,22,0.96) 100%);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    cursor: pointer;
    box-shadow: 0 4px 18px -10px rgba(0,0,0,0.7);

}

/* Especificidad (0,3,0): supera la duración que inyecta AOS
   ([data-aos][data-aos][data-aos-duration='600']) por orden de origen,
   pero NO su regla de delay ([…][data-aos-delay='300'].aos-animate →
   0,4,0), que debe seguir escalonando la entrada. Ese delay se retira
   por JS al terminar la entrada (ver initProductCards) para que el
   hover responda al instante. */
.product-card.product-card.product-card {
    will-change: transform;
    transition: transform .4s cubic-bezier(.4,0,.2,1),
                box-shadow .4s cubic-bezier(.4,0,.2,1),
                border-color .4s cubic-bezier(.4,0,.2,1),
                opacity .4s ease;
}

/* --- HOVER -----------------------------------------------------------
   Se expande la card entera y nada más. Antes se movían tres cosas a la
   vez —la card escalaba, la foto subía y escalaba por su cuenta, y el
   foco de luz crecía—, y los tres gestos superpuestos se leían como
   ruido en lugar de como una respuesta.

   Queda: la escala de la card, su borde, su sombra, y que el foco de
   luz se encienda (sin crecer). La foto ya no se mueve ni escala por
   su cuenta: acompaña a la card, que es lo que se está agrandando.

   Se acota a punteros finos para que en táctil la card no quede
   "pegada" en estado hover tras el tap. */
/* Cuánto escala la card lo define cada GRILLA con --card-hover-scale,
   no el componente: el valor correcto depende del ancho de la card, y
   ese ancho lo fijan las columnas, el gap y el max-width de la grilla.
   El criterio en todas es crecer hasta dejar ~4px de aire, o sea
   `gap − 4` de crecimiento, y la escala sale de `1 + (gap−4)/anchoCard`.

   El valor por defecto va acá dentro del var() y NO como declaración
   sobre .product-card. Puesto sobre la card gana sobre el que hereda de
   la grilla —una custom property declarada en el propio elemento pisa a
   la heredada— y todas las grillas terminarían escalando igual. Pasó:
   con el default en .product-card las cuatro grillas crecían un 4.5%
   sin importar lo que dijera su tramo.

   1.045 aguanta la card más ancha del proyecto sin tocar a su vecina. */
@media (hover: hover) {
    .product-card.product-card.product-card:hover {
        z-index: 2;
        transform: scale(var(--card-hover-scale, 1.045));
        border-color: rgba(255,255,255,0.12);
        box-shadow: 0 20px 60px rgba(0,0,0,0.4);
    }
    .product-card:hover .product-img::before { opacity: 1; }
}

/* Táctil: respuesta al tap en lugar de hover persistente */
@media (hover: none) {
    .product-card.product-card.product-card:active { transform: scale(0.99); }
}

/* --- Imagen ---------------------------------------------------------
   El producto es el protagonista: cuadrado, con aire alrededor y luz
   ambiental detrás. */
.product-img {
    width: 100%; aspect-ratio: 1; padding: 28px;
    background: linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 100%);
    display: flex; align-items: center; justify-content: center;
    position: relative; overflow: hidden; flex-direction: column; gap: 8px;
    /* Estilo del placeholder de wireframe cuando aún no hay foto.
       ESTE 0.22 QUEDÓ AFUERA DEL BARRIDO A BLANCO DEL PROMPT 10, A PROPÓSITO:
       es el color de la palabra "[Imagen]" de las cards que todavía no tienen
       foto —hoy 7 de las 9 del catálogo— y está apagado porque es un marcador
       de AUSENCIA: tiene que retroceder. En blanco pleno, "IMAGEN" pasaría a
       ser lo más brillante de esas cards y el catálogo entero gritaría que le
       faltan fotos. No la "completes": el día que existan las 7 fotos, esta
       regla deja de verse sola. */
    color: rgba(255,255,255,0.22); font-size: 10.5px; font-weight: 500;
    letter-spacing: 2.4px; text-transform: uppercase;
}
/* --- Badge de descuento ---------------------------------------------
   Sólo aparece cuando el producto tiene `precio_anterior` mayor que
   `precio`. Lo inyecta product-card.js desde los dos números; el
   porcentaje no se escribe a mano en ningún lado.

   EL FONDO ES BLANCO DESDE EL PROMPT 09, POR PEDIDO. Era var(--champagne),
   y el razonamiento con el que se eligió sigue abajo entero: no se borra
   porque sigue siendo válido si hay que volver, y volver es una palabra
   (`background: var(--champagne)`; la variable sigue en el :root de
   base.css justamente por esto).

   Lo que el cambio conserva y lo que no:
   · Conserva el argumento de la forma llena — el precio ahora también es
     blanco, igual que antes los dos eran champagne. El badge es superficie
     y el precio es texto, así que no compiten aunque compartan color.
   · No conserva la temperatura. El champagne era un acento cálido que leía
     a etiqueta de producto caro; el blanco puro sobre foto oscura pega más
     fuerte y puede pasar de acento a grito. Está medido en la bitácora del
     09 con primeros planos de las 3 cards con descuento a 1440 y 390.

   Las razones del diseño ORIGINAL, para que no se cambien sin querer:

   · Champagne y no morado — el morado está reservado para lo
     interactivo (hover, botones, links, ver la nota de .product-info)
     y este cartel no se clickea. Vale igual para el blanco.
   · Champagne y no rojo — --red es el color de error del sistema, y un
     rojo saturado sobre esta paleta oscura lee a supermercado. El
     champagne lee a etiqueta de producto caro.
   · Forma llena y no texto — el precio también es champagne. Como el
     badge es una superficie llena y el precio es texto, no compiten
     aunque compartan color.
   · Izquierda y no derecha — el ojo entra por la izquierda, y la
     esquina derecha queda libre para un favorito más adelante.
   · Sombra neutra y no de color — hay un hallazgo `dark-glow` abierto
     justamente por sombras de color sobre fondo oscuro (ver bitácora).
     No se suma otra.

   No lleva `position: relative` en .product-img: ese elemento ya lo
   tiene (línea de arriba), y repetirlo sería una segunda declaración
   del mismo hecho. */
.product-badge {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 2;
    background: #fff;
    color: var(--bg);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .3px;
    padding: 4px 10px;
    border-radius: 100px;
    /* Dos capas y no una. Con `.floating` la card pierde el padding de 28px
       y la foto llega hasta el borde, así que el badge dejó de apoyarse
       sobre el margen de la card y pasó a apoyarse sobre la foto misma.
       Medido contra el píxel de foto que queda debajo del centro de la
       pastilla, a 1440 y 390 px, con la pastilla BLANCA (los valores entre
       paréntesis son los de la champagne en el MISMO píxel, para que la
       comparación sea honesta):

         9PM    (foto oscura)  19.45:1 @1440 · 13.03:1 @390   (12.56 · 8.41)
         Eros   (foto oscura)  16.09:1 @1440 · 15.23:1 @390   (10.39 · 9.83)
         Khamra (foto CLARA)    1.89:1 @1440 ·  1.17:1 @390   ( 1.22 · 1.33)

       O sea: el blanco pega bastante MÁS fuerte sobre las fotos oscuras, y
       sobre la clara sigue sin tener silueta propia —a 390 incluso un poco
       peor que la champagne—. El texto adentro pasó de 12.98:1 a 20.10:1 y
       nunca fue el problema; lo que se pierde es la SILUETA de la pastilla,
       y por eso esta sombra sigue haciendo falta igual que antes.
       La primera capa es una sombra de contacto pegada al borde: define el
       contorno sobre cualquier fondo. La segunda es la difusa de antes.
       Se arregla con la sombra y no subiendo el color, que rompería el
       criterio de por qué el badge es champagne. Neutras las dos: hay un
       hallazgo `dark-glow` abierto por sombras de color. */
    box-shadow: 0 1px 2px rgba(0,0,0,0.55), 0 3px 10px rgba(0,0,0,0.4);
    /* .product-img define tipografía de placeholder (uppercase, tracking
       2.4px, 10.5px) para el "[Imagen]". El badge es su hijo y la
       heredaría. */
    text-transform: none;
    line-height: 1;
}

/* Foco de luz detrás del producto: da aire y profundidad sin tocar sus colores */
.product-img::before {
    content: ''; position: absolute; top: 44%; left: 50%; width: 82%; aspect-ratio: 1;
    transform: translate(-50%, -50%) scale(1); border-radius: 50%; opacity: .75;
    background: radial-gradient(closest-side, rgba(168,104,240,0.16) 0%, rgba(110,44,190,0.07) 46%, transparent 74%);
    transition: opacity .4s ease, transform .4s cubic-bezier(.4,0,.2,1);
}
/* Sombra de apoyo + degradado inferior */
.product-img::after {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background:
        radial-gradient(ellipse 42% 7% at 50% 82%, rgba(0,0,0,0.5) 0%, transparent 70%),
        linear-gradient(180deg, transparent 58%, rgba(0,0,0,0.22) 100%);
}
/* Foto del producto — z-index 1 para quedar sobre el foco radial (::before)
   y bajo la sombra de apoyo (::after). Sin filtros: los colores reales
   del frasco no se tocan. */
.product-img img {
    position: relative; z-index: 1; width: 100%; height: 100%;
    object-fit: contain; object-position: center 46%;
    filter: drop-shadow(0 18px 22px rgba(0,0,0,0.55));
    transition: transform .4s cubic-bezier(.4,0,.2,1);
}

/* --- Información ----------------------------------------------------
   Jerarquía: nombre (protagonista) → precio (blanco, 17px/600) →
   marca (etiqueta silenciosa). El púrpura queda reservado para lo
   interactivo —hover, botón, foco—, no para etiquetar información:
   cuando todo es acento, nada destaca. */
.product-info { padding: 20px; position: relative; z-index: 1; }
.product-brand { font-size: 10.5px; font-weight: 500; color: #fff; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }

/* --- Género --------------------------------------------------------
   Comparte fila con la marca porque es del mismo orden de información
   —metadato de ficha, no descripción— y así no le suma altura a la
   card. */
.product-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.product-head .product-brand { margin-bottom: 0; }
/* Blanco pleno por pedido (prompt 09, 2E). El color NO sale de
   --text-muted sino de un rgba() escrito a mano, así que el pase a blanco
   de 2D no la alcanzaba: hay que cambiarla acá. Consecuencia medida y
   aceptada: a 10px, esta etiqueta queda MÁS brillante que el nombre del
   perfume, que está en --text (#f0eaf8, 17.07:1 contra 20.10:1 del blanco).
   Lo que la mantiene en segundo plano es el tamaño y el tracking, no el
   color. Volver es `rgba(255,255,255,0.34)`. */
.product-gender { font-size: 10px; font-weight: 500; color: #fff; text-transform: uppercase; letter-spacing: 1.6px; white-space: nowrap; }

/* --- Alineación del bloque inferior ---------------------------------
   Este andamiaje existía scopeado con :has(.product-accords) y se borró
   al sacar los acordes, con el argumento de que sin ellos todas las
   cards de una fila tenían contenido de alto fijo.

   Era falso: el NOMBRE también es de alto variable. A 390px "J'adore
   Parfum d'Eau" envuelve a dos líneas y "Libre Intense" a una, y sus
   botones quedaban 22px desfasados. A 1440 y 700 no se veía porque ahí
   los cinco nombres entran en una línea.

   Vuelve sin el :has(): ahora aplica a todas las cards, que es lo que
   corresponde — el problema nunca dependió de los acordes.

   Con flex y no con un min-height en el nombre: el min-height es una
   apuesta sobre cuántas líneas ocupa, y falla en cuanto el mismo nombre
   pasa a tres en una card más angosta. El margin-top: auto no necesita
   saber cuántas líneas hay. */
.product-card { display: flex; flex-direction: column; }
.product-card .product-img { flex-shrink: 0; }
.product-info { flex: 1; display: flex; flex-direction: column; }
.product-info .product-bottom { margin-top: auto; }

/* Peso 500, no 400: en Inter el nombre a peso normal se confundiría con el
   texto corriente de la card; la serif se distinguía por la familia. */
.product-name { font-family: 'Inter', sans-serif; font-size: 17px; font-weight: 500; color: var(--text); margin-bottom: 14px; line-height: 1.3; }
/* El nombre del producto es un <a> de verdad, no un div con onclick: así
   se alcanza con Tab, se abre en pestaña nueva con ctrl+clic, y si el JS
   falla la persona igual llega a producto.html. En la landing el modal le
   hace preventDefault; en el catálogo navega de verdad.

   Vivía en el <style> de la landing. Se mudó acá al pasar a ser el markup
   de las cuatro grillas: si se queda allá, las cards del catálogo pierden
   el anillo de foco y no hay forma de ver dónde está el teclado. */
.product-name-link { color: inherit; }
.product-name-link:focus-visible {
    outline: 2px solid var(--accent-light);
    outline-offset: 3px;
    border-radius: 3px;
}
/* En cards angostas (2 columnas en mobile) precio y stock no entran en una
   línea: en vez de partir "En stock" por la mitad, el estado baja entero.

   `justify-content: flex-start` y no `space-between`: con el precio
   tachado la fila pasa a tener dos hijos del mismo grupo (precio nuevo +
   precio anterior) y `space-between` los mandaba a los extremos opuestos
   de la card, que es justo lo que no se quiere. Quien empuja el estado a
   la derecha es el `margin-left: auto` de .product-stock, no el
   justify-content — o sea que el cambio es neutro para las cards sin
   descuento, y así se evita tener que envolver los dos precios en un
   contenedor extra que rompería la equivalencia de markup entre las
   grillas. Medido: 0 px de diferencia en las cards existentes. */
.product-bottom { display: flex; align-items: baseline; justify-content: flex-start; gap: 4px 10px; flex-wrap: wrap; }
/* Blanco y no var(--champagne): el prompt 09 retiró el champagne de todo
   lo que es texto. Lo que jerarquiza el precio dentro del bloque inferior
   pasa a ser el tamaño (17px contra 12.5 del tachado y 11 del estado) y el
   peso 600, no el color. */
.product-price { font-size: 17px; font-weight: 600; letter-spacing: .2px; color: #fff; }
/* Precio anterior, tachado. Va DESPUÉS del nuevo y más chico: al revés
   obliga a leer dos números antes de llegar al que importa.
   Sin margin-left propio: la separación es el `column-gap: 10px` que ya
   declara .product-bottom. Sumarle los 8px del diseño daría 18. */
.product-price-old {
    font-size: 12.5px;
    color: var(--text-muted);
    text-decoration: line-through;
}
/* El estado sólo aparece cuando dice algo (últimas unidades, agotado):
   "En stock" en todas las cards es ruido, no información. */
.product-stock { font-size: 11px; color: var(--text-muted); text-align: right; white-space: nowrap; margin-left: auto; }
.product-stock.low { color: #e0a04a; }

/* --- Acción ---------------------------------------------------------- */
/* En reposo el botón se mantiene discreto —una grilla entera de botones
   sólidos en acento saturaría la página— y al hover adopta el degradado
   de los CTA de la landing. */
.product-btn {
    display: block; width: 100%; margin-top: 16px; padding: 12px;
    background: linear-gradient(135deg, rgba(141,32,216,0.20) 0%, rgba(176,38,255,0.14) 100%);
    color: #fff; border: 1px solid rgba(176,38,255,0.34); border-radius: var(--radius-sm);
    font-size: 12.5px; font-weight: 600; font-family: inherit; letter-spacing: .4px;
    cursor: pointer; position: relative; overflow: hidden;
    transition: background .25s ease, border-color .25s ease,
                transform .25s cubic-bezier(.4,0,.2,1), box-shadow .25s cubic-bezier(.4,0,.2,1);
}
/* Mismo barrido de luz que los botones de la landing */
.product-btn::before {
    content: ''; position: absolute; top: 0; left: 0; width: 45%; height: 100%;
    background: linear-gradient(115deg, transparent, rgba(255,255,255,0.32), transparent);
    transform: translateX(-133.33%) skewX(-18deg);
    transition: transform .7s cubic-bezier(.4,0,.2,1);
}
.product-btn:hover:not(:disabled) {
    background: linear-gradient(135deg, #8d20d8 0%, var(--accent) 52%, #8f2ae0 100%);
    border-color: transparent; transform: translateY(-1px);
    box-shadow: 0 10px 26px -12px rgba(3,1,8,0.95), 0 0 0 1px rgba(255,255,255,0.18) inset;
}
.product-btn:hover:not(:disabled)::before { transform: translateX(288.89%) skewX(-18deg); }
.product-btn:active:not(:disabled) { transform: translateY(0); }
.product-btn:focus-visible { outline: 1px solid rgba(217,70,239,0.7); outline-offset: 2px; }
.product-btn:disabled {
    background: rgba(255,255,255,0.04); color: var(--text-muted);
    border-color: rgba(255,255,255,0.07);
    cursor: not-allowed; transform: none;
}
.product-btn:disabled::before { display: none; }

/* ============================================================
   VARIANTE "VITRINA" — la usan las CUATRO grillas de producto.
   La caja exterior sigue sin fondo, borde ni sombra: el nombre,
   el precio y la acción respiran sobre la escena de cada sección.
   Lo que vuelve es una superficie SÓLO detrás de la foto.

   Por qué: los packshots no comparten iluminación ni recorte. Sobre
   el fondo oscuro, los frascos claros conservan la silueta pero los
   negros se mezclan con la página, y sin un plano común las fotos se
   leen como recortes sueltos. La vitrina normaliza ese material sin
   convertir la grilla entera en una colección de cajas pesadas.

   No lleva blur: son hasta 14 superficies grandes visibles en el
   catálogo y un backdrop-filter obligaría a recomponerlas al hacer
   scroll. La profundidad sale de gradientes y una sombra neutra.

   Vivía en el <style> de index.html cuando sólo la usaba Últimos
   lanzamientos. Se mudó acá entera, con sus comentarios, al
   aplicarla también a Ofertas destacadas y al catálogo: desde el
   <style> de una página el catálogo no podía verla. Mismo
   movimiento que .product-name-link.

   Sigue colgando de .floating, o sea que se prueba y se saca
   cambiando una palabra en el HTML de cada grilla.

   Nota de especificidad: product-card.css repite la clase
   (.product-card.product-card.product-card) para ganarle a las
   reglas que inyecta AOS. Para pisar esas reglas hay que
   repetirla también acá — de ahí los selectores largos.
   ============================================================ */
.floating .product-card {
    background: none;
    border-color: transparent;
    box-shadow: none;
}
/* La card queda abierta para que su escala de hover no se recorte. La
   vitrina, en cambio, RECORTA: ahora tiene borde y radio visibles, y
   los gradientes tienen que terminar exactamente en esa silueta.

   Esto ya no corta el frasco como en la primera versión flotante:
   entonces la foto subía y escalaba por su cuenta; hoy el único gesto
   es la escala de la card entera, así que imagen y vitrina se mueven
   juntas. */
.floating .product-card { overflow: visible; }
/* El centro claro no es otro color de marca: sale de --surface-3
   mezclado con blanco y se apaga hacia los mismos --surface / --bg
   del sitio. Es el plano que devuelve silueta a un frasco negro.

   El padding es fluido: 10px en una card de 170px y hasta ~18px en
   una de 330. Un valor fijo de 20px achicaría demasiado el producto
   en las dos columnas del celular; sin padding volverían los recortes
   tocando el borde que motivaron esta variante. */
.floating .product-img {
    overflow: hidden;
    padding: clamp(10px, 5.5%, 20px);
    border: 1px solid color-mix(in srgb, var(--text) 9%, transparent);
    border-radius: var(--radius);
    background:
        radial-gradient(ellipse 72% 70% at 50% 42%,
            color-mix(in srgb, var(--surface-3) 82%, white) 0%,
            color-mix(in srgb, var(--surface-2) 88%, transparent) 52%,
            transparent 78%),
        linear-gradient(155deg,
            color-mix(in srgb, var(--surface-2) 86%, var(--bg)) 0%,
            color-mix(in srgb, var(--surface) 78%, var(--bg)) 58%,
            var(--bg) 100%);
    box-shadow:
        inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent),
        0 20px 46px -36px rgba(0,0,0,0.92);
    transition: border-color .4s ease, box-shadow .4s ease;
}
/* El foco violeta compartido se conserva, pero ya no tiene que hacer
   solo el trabajo de separar la foto del fondo. A .62 acompaña la
   vitrina sin convertir su centro en una mancha saturada. */
.floating .product-img::before { opacity: .62; }
/* Recuperar la base elíptica es lo que termina de apoyar el objeto
   adentro de la vitrina. El degradado inferior es más tenue que en la
   card cerrada: acá el borde ya define la superficie. */
.floating .product-img::after {
    display: block;
    background:
        radial-gradient(ellipse 40% 6% at 50% 83%, rgba(0,0,0,0.48) 0%, transparent 72%),
        linear-gradient(180deg, transparent 66%, rgba(0,0,0,0.13) 100%);
}
/* Una línea de luz de un píxel ayuda a los frascos negros sin cambiar
   el color real de la foto. La segunda sombra los despega de la base. */
.floating .product-img img {
    filter:
        drop-shadow(0 0 1px rgba(255,255,255,0.16))
        drop-shadow(0 24px 30px rgba(0,0,0,0.58));
    transition: transform .5s cubic-bezier(.22,1,.36,1),
                filter .5s cubic-bezier(.22,1,.36,1);
}
.floating .product-info { padding: 20px 4px 0; }

/* Hover: la escala se la deja al componente compartido, igual
   que las otras tres grillas. Acá sólo se anulan el borde y la
   sombra, que sí aparecerían y le dibujarían a la card la caja
   que justamente no tiene.

   Antes esta regla traía `transform: none` y el gesto lo hacía
   el frasco por su cuenta (subía 12px y escalaba). Se quitó por
   lo mismo que se quitó de product-card.css: en hover se expande
   la card entera y nada más. Sin caja visible, escalar la card
   igual mueve el frasco —va adentro—, pero como parte de un
   único gesto en vez de dos superpuestos.

   La sombra del frasco sí se abre: no es movimiento propio, es
   la misma sombra acompañando el tamaño nuevo. */
.floating .product-card.product-card.product-card:hover {
    border-color: transparent;
    box-shadow: none;
}
.floating .product-card:hover .product-img {
    border-color: color-mix(in srgb, var(--accent-light) 26%, transparent);
    box-shadow:
        inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent),
        0 26px 56px -38px rgba(0,0,0,0.96);
}
.floating .product-card:hover .product-img img {
    filter:
        drop-shadow(0 0 1px rgba(255,255,255,0.18))
        drop-shadow(0 34px 38px rgba(0,0,0,0.64));
}

/* ============================================================
   LOS TRES ESTADOS DE UNA GRILLA — cargando, vacío y error.

   Están acá y no en el <style> de cada página por lo mismo que la
   card: son cuatro grillas en dos archivos, y cuatro copias de esto
   es cómo terminan viéndose distinto entre sí.

   La regla dura: **vacío y error NO se pueden ver igual**. Una grilla
   en blanco porque la consulta falló se lee como "no hay productos",
   y esa es una mentira que nadie va a ir a revisar. Por eso el error
   tiene borde, color propio y un botón para reintentar, y el vacío es
   una frase gris sin adorno.

   Ocupar la fila entera pide DOS declaraciones, una por tipo de
   contenedor: sin ellas el mensaje queda metido en una columna o
   encogido a su contenido.
     · `grid-column: 1 / -1` para .catalog-grid, que sigue siendo grid;
     · `flex-basis: 100%` para las tres grillas de sección, que desde el
       prompt 27 son flex para poder centrar la fila incompleta. Ahí el
       `grid-column` es inerte y el bloque quedaría del ancho de su
       texto, con el borde rojo del error abrazando la frase en vez de
       la fila.
   Cada una es inerte en el contenedor de la otra, así que conviven.
   ============================================================ */
.grid-estado {
    grid-column: 1 / -1;
    flex-basis: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    /* Alto mínimo parecido al de una fila de cards: sin esto la página
       da un salto de varios cientos de píxeles cuando llegan los
       datos, justo cuando la persona ya empezó a leer. */
    min-height: 220px;
    padding: 40px 20px;
    text-align: center;
}

.grid-estado-texto {
    margin: 0;
    font-size: 14px;
    font-weight: 300;
    line-height: 1.6;
    max-width: 42ch;
    color: var(--text-muted);
}

/* --- Cargando --------------------------------------------------- */
.grid-estado-spinner {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 2px solid rgba(255,255,255,0.10);
    border-top-color: var(--accent);
    animation: gridEstadoGira .7s linear infinite;
}
@keyframes gridEstadoGira { to { transform: rotate(360deg); } }

/* --- Vacío: gris y sin caja. No pasó nada malo, no hay nada. ----- */
.grid-estado--vacio .grid-estado-texto { color: var(--text-muted); }

/* --- Error: caja, borde y acción. Pasó algo y se puede reintentar.
       El rojo es el MISMO valor que `--red` (#ef4444), el token que ya
       usa .cart-item-remove, escrito como rgba porque hacen falta dos
       alfas distintos y una variable de color no los lleva. Queda
       anotado: son dos de las declaraciones literales que CLAUDE.md
       cuenta como pendiente. -- */
.grid-estado--error {
    border: 1px solid rgba(239, 68, 68, 0.28);
    border-radius: var(--radius);
    background: rgba(239, 68, 68, 0.05);
}
.grid-estado--error .grid-estado-texto { color: #fff; }

.grid-estado-btn {
    font-family: inherit;   /* sin esto el navegador le pone Arial */
    font-size: 13px;
    font-weight: 500;
    color: #fff;
    background: var(--accent);
    border: none;
    border-radius: var(--radius-sm);
    padding: 10px 22px;
    cursor: pointer;
    transition: background .2s ease, transform .2s ease;
}
.grid-estado-btn:hover { background: var(--accent-hover); transform: translateY(-1px); }
.grid-estado-btn:focus-visible { outline: 1px solid var(--accent-light); outline-offset: 2px; }
