/* ─── LQIP Progressive Image (.hzo-img) ────────────────────────────────────
   W-023 · closes audit gap G51 (missing aspect/dimensions → CLS). The fleet's
   photo primitive: an aspect-ratio box that reserves EXACT layout space (zero
   CLS) with a tiny LQIP placeholder that blurs up to the full image on decode.

   FAIL-VISIBLE BY CONSTRUCTION (mirrors the reveal orchestrator, W-020): the
   armed leg that hides the full image (opacity:0) applies ONLY under the
   JS-set `html.hzo-img-ready` flag. No JS, a thrown init, reduced-motion,
   forced-colors, or print → the flag is absent → the FULL image is fully
   visible and the whole widget degrades to a plain <img> in a reserved box.
   The box + placeholder are server-rendered and paint on the first frame with
   zero layout dependency on JS — the reserved box IS the sub-100ms guarantee.

   Single-cell grid overlay stacks the LQIP under the full image WITHOUT a
   z-index (§9.3): grid-area:1/1 + DOM order (LQIP first, full second) is the
   paint order. No new z-index token is introduced. */

.hzo-img {
    display: grid;
    width: 100%;
    max-width: 100%;
    /* Reserved box → zero CLS. `auto` (unset aspect) falls back to the intrinsic
       ratio the width/height attrs on .hzo-img__full provide. */
    aspect-ratio: var(--hzo-img-aspect, auto);
    overflow: hidden;
    /* A neutral frame shows through until the LQIP/full paints (and stays as the
       error frame). Token only — no hardcoded color. */
    background-color: var(--surface-2);
}

/* Both layers occupy the one grid cell and fill it — object-fit does the crop. */
.hzo-img > .hzo-img__lqip,
.hzo-img > .hzo-img__full {
    grid-area: 1 / 1;
    display: block;
    width: 100%;
    height: 100%;
    margin: 0;
    object-fit: var(--hzo-img-fit, cover);
    object-position: var(--hzo-img-pos, center);
}

/* ── The full image. Default opacity:1 so NO-JS / reduced-motion / forced-colors
      / print all show it. The fade is declared here so the armed→revealed
      transition (opacity 0→1) animates once the armed leg stops matching. ── */
.hzo-img__full {
    opacity: 1;
    transition: opacity var(--hzo-img-fade, var(--dur-3)) var(--ease-out);
}

/* ── The LQIP placeholder (decorative, aria-hidden in markup). ── */
.hzo-img__lqip {
    opacity: 1;
    transition: opacity var(--hzo-img-fade, var(--dur-3)) var(--ease-out);
}
/* base64 / blurhash: a tiny bitmap scaled up + blurred. The slight scale hides
   the blur's soft edges against the box. */
.hzo-img__lqip--blur {
    filter: blur(var(--hzo-img-blur, 12px));
    transform: scale(1.04);
}
/* color: a flat dominant-color wash (no bitmap). */
.hzo-img__lqip--color {
    background-color: var(--hzo-img-lqip-color, var(--surface-2));
}

/* ═══ Armed (hidden) leg — ONLY under the JS-ready flag ════════════════════
   Before decode, hide the full image so the LQIP shows through; img.js flips
   .hzo-img--loading → .hzo-img--loaded on decode, this selector stops matching,
   and the full image fades in over --hzo-img-fade. */
.hzo-img-ready .hzo-img--loading .hzo-img__full {
    opacity: 0;
}

/* Decoded: the full image is visible (default opacity:1 via the rule above) and
   the LQIP fades out beneath it, then is inert. */
.hzo-img--loaded .hzo-img__lqip {
    opacity: 0;
}

/* ═══ Error state — reconcile the optimistic blur to a real fallback ════════
   img.js sets .hzo-img--error on the <img> `error` event (or a never-resolving
   decode watchdog): drop the frozen blur, keep the neutral frame, and show the
   alt as visible text so a broken source is never a silent empty box. */
.hzo-img--error .hzo-img__lqip {
    display: none;
}
.hzo-img--error .hzo-img__full {
    opacity: 0; /* the broken <img> icon is suppressed; the fallback text carries meaning */
}
.hzo-img__fallback {
    grid-area: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-100);
    line-height: var(--lh-snug);
}

/* ═══ Fail-visible environment branches (spec §12) ═════════════════════════ */

/* reduced-motion [PRESERVE-5 · G13/G60]: the blur-up cross-fade is AMBIENT, so
   it fully STOPS — the full image appears instantly on decode, never stranded
   at opacity:0. (img.js also skips the ready flag under reduce; this is the CSS
   belt to its suspenders.) */
@media (prefers-reduced-motion: reduce) {
    .hzo-img__full,
    .hzo-img__lqip {
        transition: none !important;
    }
    .hzo-img--loading .hzo-img__full {
        opacity: 1 !important;
    }
}

/* forced-colors / High Contrast [§12.2]: opacity/blur intermediates are
   unreliable, so show the full image instantly and drop the decorative blur.
   The error frame uses SYSTEM colors (not opacity) so it stays visible. */
@media (forced-colors: active) {
    .hzo-img--loading .hzo-img__full {
        opacity: 1 !important;
    }
    .hzo-img__lqip--blur {
        display: none;
    }
    .hzo-img--error {
        background-color: Canvas;
        border: var(--border-1) solid GrayText;
    }
    .hzo-img--error .hzo-img__fallback {
        color: CanvasText;
    }
}

/* @media print [G13 — the reveal-hidden-content culprit class, applied to the
      decode-fade]: a printed estimate/service page must NEVER show a blurred
      thumbnail. Force the full image visible and hide the placeholder. The
      beforeprint JS handler additionally flushes any still-loading instance. */
@media print {
    .hzo-img__full {
        opacity: 1 !important;
    }
    .hzo-img__lqip {
        display: none !important;
    }
}
