/* input(93,15): run-time error CSS1019: Unexpected token, found 'selector('
input(93,31): run-time error CSS1025: Expected comma or open brace, found ')' */
/* ScentArt loading state, shared across every surface that renders a fingerprint
   in the browser.

   Until now the only treatment was `opacity: 0` on the image until the renderer
   filled its src. That is correct for a fingerprint that paints in 200ms on a
   desktop GPU and wrong everywhere else: on a phone, or on any machine without a
   working GPU, it is an empty box that fills in silently much later, or never.
   The reader cannot tell "loading" from "broken" from "this perfume has no art".

   Keyed off the same state the renderer already maintains - an element carrying
   data-sa-profile has not been painted yet, and the renderer strips that
   attribute when it lands - so nothing new has to be tracked.

   The spinner lives on the CONTAINER, not the image, because the image is
   transparent while it waits and cannot show anything itself. */

.sa-loading {
    position: relative;
    background: var(--color-surface-alt);
}

/* Hide the image itself until it has been painted. The comment above assumed it was
   already transparent while waiting, which is true only where the markup omits src
   entirely. An <img> whose src is the EMPTY STRING is a broken image, and a broken image
   renders its ALT TEXT - so the gallery showed "Aquatica Eau de Parfum ScentArt
   visualisation" in twelve boxes, wrapped over the spinner. Both shapes are covered here
   rather than at each call site, since which one a page emits depends on whether its model
   had a prerendered URL to interpolate. Scoped to .sa-loading so it cannot reach a surface
   that has not opted in. */
.sa-loading [data-sa-profile]:not([src]),
.sa-loading [data-sa-profile][src=""] {
    opacity: 0;
}

/* CANVAS SURFACES TOO (2026-09-15).
   Everything above keys off an <img data-sa-profile>, which is the baked-image shape. A
   surface with no bake renders a <canvas data-profile> instead, and none of these selectors
   saw it - so /compare, whose panels take the canvas path whenever a perfume has no bake,
   showed two black boxes for the whole render with nothing to say they were working. It is
   not an edge case: production has no baked ScentArt at all (ISS-102), so every compare page
   takes that path.

   The painted signal is a SEPARATE marker, data-sa-painted, not the removal of data-profile.
   The image path can strip data-sa-profile safely because nothing else reads it; the canvas
   path cannot, because the full-screen renderer dropdown swaps renderers by cloning the old
   canvas and carrying data-profile across to the new one. Stripping it broke every swap on
   /perfume/{slug}/scentart and /compare/{a}/{b}/scentart at once. */
.sa-loading canvas[data-profile]:not([data-sa-painted]) {
    opacity: 0;
}

/* The ring. Sized from the container so one rule serves a 40px row tile and a
   900px comparison panel: clamped so it never dominates a small tile and never
   floats lost in a large one. */
.sa-loading:has(canvas[data-profile]:not([data-sa-painted]))::after,
.sa-loading:has([data-sa-profile]:not([src]))::after,
.sa-loading:has([data-sa-profile][src=""])::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: clamp(16px, 18%, 40px);
    height: clamp(16px, 18%, 40px);
    margin: calc(clamp(16px, 18%, 40px) / -2) 0 0 calc(clamp(16px, 18%, 40px) / -2);
    border-radius: var(--radius-full);
    border: 2px solid var(--color-border-subtle);
    border-top-color: var(--color-accent-text);
    animation: sa-spin 0.8s linear infinite;
    pointer-events: none;
}

@keyframes sa-spin {
    to { transform: rotate(360deg); }
}

/* A spinner that never stops is a lie once the render has failed. The renderer
   settles an image it could not paint by dropping data-sa-profile, which clears
   the selector above and leaves the container's own background - so a failure
   reads as "no art here", not as "still coming". */

@media (prefers-reduced-motion: reduce) {
    .sa-loading:has(canvas[data-profile]:not([data-sa-painted]))::after,
    .sa-loading:has([data-sa-profile]:not([src]))::after {
        animation: none;
        border-top-color: var(--color-border-subtle);
        opacity: 0.6;
    }
}

/* :has() is the whole mechanism, and Firefox only shipped it in 121. Older
   engines get the plain surface with no spinner rather than a broken one -
   the render still happens and still fades in. */
@supports not selector(:has(*)) {
    .sa-loading::after { content: none; }
}

/* ============================================================================
   A PLATE, NOT A BARE RING  (2026-09-13)

   The ring above is correct and was matching - the operator still asked for "a
   loader spinner", which is the tell that a 2px hairline scaled from the
   container does not read as one on a large panel mid-render. Two changes:
   the spinner is a FIXED 28px rather than 18% of the box, so it looks the same
   on a 44px tile and a 576px panel; and on any surface that opts in with
   .sa-loading--labelled it sits on an opaque plate with a word, because a
   render here can genuinely take seconds and silence is indistinguishable from
   breakage.
   ============================================================================ */

.sa-loading:has([data-sa-profile]:not([src]))::after,
.sa-loading:has([data-sa-profile][src=""])::after {
    width: 28px;
    height: 28px;
    margin: -14px 0 0 -14px;
    border-width: 3px;
}

/* Opt-in label. Only the large panels take it; a 44px rail tile has no room and
   the spinner alone is unambiguous at that size. */
.sa-loading--labelled:has(canvas[data-profile]:not([data-sa-painted]))::before,
.sa-loading--labelled:has([data-sa-profile]:not([src]))::before,
.sa-loading--labelled:has([data-sa-profile][src=""])::before {
    content: "Rendering scent art";
    position: absolute;
    left: 50%;
    /* Below the spinner AND clear of the seam badge that sits dead centre on the
       dupes pair - at +26px the label rendered behind it. */
    top: calc(50% + 64px);
    transform: translateX(-50%);
    z-index: 1;
    white-space: nowrap;
    padding: 5px 11px;
    border-radius: var(--radius-full, 9999px);
    background: var(--color-card-bg);
    border: 1px solid var(--color-border-subtle);
    font: 600 0.75rem/1 var(--font-display, inherit);
    color: var(--color-text-secondary);
    pointer-events: none;
}

/* A render that failed is not a render that is still going. The driver sets this
   when it has exhausted WebGL and the Canvas2D fallback, so the spinner stops and
   the reader is told - rather than watching a ring turn forever. */
.sa-failed::after { content: none !important; }
.sa-failed::before {
    content: "Scent art unavailable";
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    text-align: center;
    padding: 8px;
    font: 600 0.75rem/1.3 var(--font-display, inherit);
    color: var(--color-muted);
    background: var(--color-surface-alt, var(--color-card-bg));
}
/* Too small to read a sentence in; the muted plate alone says "nothing here". */
.dupes-option__art.sa-failed::before { content: ""; }

@media (prefers-reduced-motion: reduce) {
    .sa-loading:has([data-sa-profile]:not([src]))::after,
    .sa-loading:has([data-sa-profile][src=""])::after { animation-duration: 2.4s; }
}


/* The labelled panels are SEEDED: the page copies the small rail bitmap into them so
   they are never blank, then the batch replaces it with a render at full size. That
   seed is a src, so the :not([src]) selectors above stop matching the moment it lands
   and the ring vanished while the real render - the slow one - was still to come. For
   the labelled surfaces the signal is the profile attribute alone: the driver strips it
   when the full-size render is painted, and not before. */
.sa-loading--labelled:has([data-sa-profile])::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 28px;
    height: 28px;
    margin: -14px 0 0 -14px;
    border-radius: var(--radius-full);
    border: 3px solid rgba(255, 255, 255, .55);
    border-top-color: var(--color-accent-text);
    animation: sa-spin 0.8s linear infinite;
    pointer-events: none;
    z-index: 2;
}
.sa-loading--labelled:has([data-sa-profile])::before {
    content: "Rendering scent art";
    position: absolute;
    left: 50%;
    top: calc(50% + 64px);
    transform: translateX(-50%);
    z-index: 2;
    white-space: nowrap;
    padding: 5px 11px;
    border-radius: var(--radius-full, 9999px);
    background: var(--color-card-bg);
    border: 1px solid var(--color-border-subtle);
    font: 600 0.75rem/1 var(--font-display, inherit);
    color: var(--color-text-secondary);
    pointer-events: none;
}

/* ============================================================================
   STILL / ANIMATED for a pair of panels  (scent-art-pair-tabs.mjs, 2026-09-13)

   Same control the perfume page's ScentArt panel has, under its own class so the
   dupes and compare pages do not have to load perfume-detail.css for it. The art
   behind it is high-contrast and, on Animated, moving - hence the scrim.
   ============================================================================ */
.sa-pair-tabs {
    display: inline-flex;
    border-radius: 999px;
    overflow: hidden;
    background: rgba(6, 6, 12, 0.55);
    border: 1px solid rgba(255, 255, 255, 0.12);
    backdrop-filter: blur(12px) saturate(0.9);
    -webkit-backdrop-filter: blur(12px) saturate(0.9);
}
.sa-pair-tab {
    appearance: none;
    border: 0;
    margin: 0;
    padding: 6px 14px;
    min-height: 2.25rem;
    cursor: pointer;
    background: transparent;
    /* Full white, not 0.72 alpha: over this bar's #767679 backdrop 72% composites to
       #d9d9d9 and measures 3.21:1. Nothing is lost - the selected tab is distinguished
       by its white BACKGROUND with dark text, not by text opacity. */
    color: #fff;
    font: 700 0.6875rem/1 var(--font-display, inherit);
    letter-spacing: .08em;
    text-transform: uppercase;
    transition: background .15s, color .15s;
}
.sa-pair-tab[aria-selected="true"] { background: rgba(255, 255, 255, 0.92); color: #101018; }
.sa-pair-tab:hover:not([aria-selected="true"]) { color: #fff; }
.sa-pair-tab:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.sa-pair-tab.is-loading { color: var(--color-accent-text); }
@media (hover: none), (max-width: 768px) { .sa-pair-tab { min-height: 2.75rem; padding: 8px 16px; } }

/* The live canvas fills its panel, over the still. The still is left in place
   beneath until the first frame paints, then hidden - never removed, so going
   back to Still costs nothing. */
.sa-pair-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    z-index: 1;
}
.sa-pair-live > img,
.sa-pair-live > canvas:not(.sa-pair-canvas) { visibility: hidden; }
