/* ==========================================================================
   bottle-viewer.css — Apex Holding / Greppo Propria
   PURPOSE: presentation for the drag-to-rotate 3D bottle that
   assets/js/cinema/bottle-viewer.js mounts on any [data-bottle-viewer] element.

   LOAD ORDER: after commerce.css and after the world theme, i.e. LAST or nearly.
   Like every file in the cinema layer this is PURELY ADDITIVE: it introduces its
   own .bv__* namespace plus the [data-bottle-viewer] / [data-bv-state] hooks,
   and it does not override a single existing rule by name. Nothing outside the
   Greppo order surface is touched by anything in this file.

   Add it to a page with:
     <link rel="stylesheet" href="../assets/css/bottle-viewer.css">

   THE THREE STATES, AND WHY THE FALLBACK IS THE DEFAULT
     no attribute         bottle-viewer.js has not run, or refused to. Whatever
                          the page author wrote inside the element — the wine
                          name, the description, a silhouette — is what shows,
                          inside a designed frame. This is what a visitor with
                          JavaScript off, without WebGL2, or behind a failed CDN
                          gets, and it is a designed state, not a broken one.
     data-bv-state=loading  three and the model are being fetched.
     data-bv-state=ready    the canvas is live.

   Because the enhanced presentation is gated on an attribute the SCRIPT adds,
   the fallback needs no JavaScript to appear and can never be left half
   applied. If anything at all goes wrong, the authored copy is what stands.

   NO PRICE, NO NAME AND NO COMMERCE COPY IS SET HERE. This file styles a
   viewport onto a piece of geometry and nothing else. The product name, the
   price and the Stripe button are the card's, and they live in the catalogue
   markup and in commerce.css where the client can find them.

   COLOUR comes only from the --w-* semantic tokens, so the viewer takes on the
   palette of whichever world hosts it with no per-world rule in this file.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. Local tokens
   Namespaced --bv-* so tokens.css keeps sole ownership of its own names.
   Override any of these on the element itself to retune a single viewer:
     <div data-bottle-viewer="…" style="--bv-ratio: 3 / 4;">
   -------------------------------------------------------------------------- */

[data-bottle-viewer] {
  /* Portrait by default. A bottle is much taller than it is wide, and the fit
     in bottle-viewer.js is a BOUNDING SPHERE fit, so this ratio decides how
     much air surrounds the bottle rather than whether it fits. */
  --bv-ratio: 3 / 4;
  --bv-min-height: 240px;
  --bv-max-height: 60vh;
  --bv-radius: var(--radius-md, 10px);
  --bv-surface: var(--w-surface, #16171a);
  --bv-surface-2: var(--w-surface-2, #1d1f23);
  --bv-base: var(--w-base-2, #101114);
  --bv-line: var(--w-line, rgba(255, 255, 255, 0.14));
  --bv-accent: var(--w-accent, #c9a24b);
  --bv-text-dim: var(--w-text-dim, #9aa0a6);

  display: block;
  width: 100%;
  margin-inline: auto;
}


/* --------------------------------------------------------------------------
   2. The fallback

   Present until bottle-viewer.js sets data-bv-state, which means it is also
   what shows when bottle-viewer.js never runs at all. The authored children —
   typically a .bv__fallback paragraph naming the wine — are laid out inside a
   frame that matches the live stage, so the card never changes shape when the
   3D arrives and there is no layout shift to see.
   -------------------------------------------------------------------------- */

[data-bottle-viewer]:not([data-bv-state]) {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2, 0.5rem);
  aspect-ratio: var(--bv-ratio);
  min-height: var(--bv-min-height);
  max-height: var(--bv-max-height);
  padding: var(--space-5, 1.5rem);
  overflow: hidden;
  border: 1px solid var(--bv-line);
  border-radius: var(--bv-radius);
  background:
    radial-gradient(
      110% 80% at 50% 8%,
      color-mix(in srgb, var(--bv-accent) 10%, transparent) 0%,
      transparent 68%
    ),
    linear-gradient(
      to bottom,
      var(--bv-surface-2) 0%,
      var(--bv-surface) 62%,
      var(--bv-base) 100%
    );
}

/* The script appends these two. Until it has also set data-bv-state they are
   not part of the picture, and after a dispose() they must not be either — so
   the state attribute, not the element's existence, is what reveals them. */
[data-bottle-viewer]:not([data-bv-state]) > .bv__stage {
  display: none;
}

.bv__fallback {
  margin: 0;
  max-width: 26ch;
  text-align: center;
  text-wrap: pretty;
  font-family: var(--font-body, inherit);
  font-size: var(--step--1, 0.9rem);
  line-height: 1.55;
  color: var(--bv-text-dim);
}

/* A caption written as an attribute still shows, for markup that carries no
   child content at all. Purely a safety net: real markup is always better,
   because generated content is not reliably announced by every screen reader. */
[data-bottle-viewer]:not([data-bv-state]):empty::before {
  content: attr(data-bottle-name);
  font-family: var(--font-body, inherit);
  font-size: var(--step--1, 0.9rem);
  line-height: 1.5;
  color: var(--bv-text-dim);
}

/* Once enhanced, the authored fallback steps aside for the stage. Nothing is
   removed from the DOM — the base site's content is never destroyed, only
   presented differently. */
[data-bottle-viewer][data-bv-state] > :not(.bv__stage):not(.bv__sr) {
  display: none;
}


/* --------------------------------------------------------------------------
   3. The stage — the control itself
   -------------------------------------------------------------------------- */

.bv[data-bv-state] {
  display: block;
}

.bv__stage {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: var(--bv-ratio);
  /* Comfortably past the 44px minimum target on every axis, at every width. */
  min-height: var(--bv-min-height);
  max-height: var(--bv-max-height);
  overflow: hidden;
  border: 1px solid var(--bv-line);
  border-radius: var(--bv-radius);
  background:
    radial-gradient(
      110% 80% at 50% 8%,
      color-mix(in srgb, var(--bv-accent) 10%, transparent) 0%,
      transparent 68%
    ),
    linear-gradient(
      to bottom,
      var(--bv-surface-2) 0%,
      var(--bv-surface) 62%,
      var(--bv-base) 100%
    );

  /* THE SCROLL GUARANTEE.
     pan-y hands every vertical gesture to the page and keeps it there: a swipe
     that begins as a scroll IS a scroll, the browser never offers it to us, and
     bottle-viewer.js does not try to take it. Only the horizontal component is
     ours, which is enough to spin the bottle. Once the visitor has deliberately
     engaged — a tap, or focus — .is-engaged below takes the whole gesture,
     because at that point turning the bottle is what they asked for. */
  touch-action: pan-y;

  cursor: grab;
  outline: none;
  -webkit-user-select: none;
  user-select: none;
  /* iOS long-press callout on a canvas the visitor is dragging. */
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

.bv__stage.is-engaged {
  touch-action: none;
  border-color: color-mix(in srgb, var(--bv-accent) 42%, var(--bv-line));
}

.bv__stage.is-dragging {
  cursor: grabbing;
}

/* The focus ring is on the stage itself, because the stage IS the control. */
.bv__stage:focus-visible {
  outline: 2px solid var(--w-focus, #7cc0ff);
  outline-offset: 3px;
}

/* Fallback for engines without :focus-visible support. */
.bv__stage:focus:not(:focus-visible) {
  outline: none;
}

/* The blit target. bottle-viewer.js renders every bottle on the page through
   ONE WebGL context and copies each one into its own canvas here, which is why
   this element can sit inside a tilted, rounded, clipped card and simply
   inherit all three. */
.bv__canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* Already aria-hidden, and never a pointer target: every listener is on the
     stage, so the canvas can be replaced on each remount without a single
     listener having to move with it. */
  pointer-events: none;
}


/* --------------------------------------------------------------------------
   4. The hint
   Decoration only. It is aria-hidden; the spoken instructions live in the
   .bv__sr description the stage is described by.
   -------------------------------------------------------------------------- */

.bv__hint {
  position: absolute;
  inset-inline: 0;
  bottom: var(--space-3, 0.75rem);
  z-index: 1;
  margin: 0;
  padding-inline: var(--space-4, 1rem);
  text-align: center;
  font-family: var(--font-body, inherit);
  font-size: var(--step--1, 0.85rem);
  letter-spacing: 0.05em;
  color: var(--bv-text-dim);
  opacity: 0;
  transition: opacity var(--dur-micro, 200ms) var(--ease-out-soft, ease);
  pointer-events: none;
}

.bv__stage:hover .bv__hint,
.bv__stage:focus-visible .bv__hint,
.bv__stage.is-engaged .bv__hint {
  opacity: 0.85;
}

.bv__stage.is-engaged .bv__hint {
  color: var(--w-text, #e8e8e8);
  opacity: 1;
}

/* A pointer that cannot hover has no way to reveal the hint, so it is simply
   always there — quietly. */
@media (hover: none) {
  .bv__hint {
    opacity: 0.7;
  }
}


/* --------------------------------------------------------------------------
   5. The spoken description
   Visually hidden, still announced. Its own rule rather than a shared utility,
   so this file depends on nothing outside itself.
   -------------------------------------------------------------------------- */

.bv__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* --------------------------------------------------------------------------
   6. Loading
   A slow breath on the stage while three and the model are fetched. It is gone
   the instant the first frame is drawn.
   -------------------------------------------------------------------------- */

[data-bv-state="loading"] .bv__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    color-mix(in srgb, var(--bv-accent) 6%, transparent) 50%,
    transparent 100%
  );
  animation: bv-breathe 2.4s var(--ease-in-out-cinema, ease-in-out) infinite;
  pointer-events: none;
}

@keyframes bv-breathe {
  0%, 100% { opacity: 0.25; }
  50%      { opacity: 0.75; }
}


/* --------------------------------------------------------------------------
   7. Layout at width and at pointer
   -------------------------------------------------------------------------- */

/* A coarse pointer gets a shorter stage. A tall canvas on a phone pushes the
   name, the price and the order button off screen, and those are the parts of
   the card that carry the meaning. */
@media (pointer: coarse) {
  [data-bottle-viewer] {
    --bv-max-height: 46vh;
    --bv-min-height: 220px;
  }
}

/* Very narrow: keep the frame from eating the whole viewport height. */
@media (max-width: 30rem) {
  [data-bottle-viewer] {
    --bv-ratio: 4 / 5;
  }
}


/* --------------------------------------------------------------------------
   8. prefers-reduced-motion — the complete alternate cut, with NO content loss

   bottle-viewer.js has already switched auto-rotate, inertia and the hover lean
   off and settled to a single static rendered frame. All this stylesheet has to
   do is stop its own two animations.

   THE VIEWER STAYS FULLY INTERACTIVE. A visitor who chooses to turn the bottle
   is asking for that motion, which is a completely different thing from motion
   the page imposes on them. Nothing is hidden, nothing is replaced, and every
   word of the description is still there.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  [data-bv-state="loading"] .bv__stage::after {
    animation: none;
    opacity: 0.4;
  }

  .bv__hint {
    transition: none;
  }
}


/* --------------------------------------------------------------------------
   9. Forced colours
   Keep the frame, the engaged state and the focus ring legible when the OS is
   drawing the palette. The rendered bottle is a bitmap and is left alone.
   -------------------------------------------------------------------------- */

@media (forced-colors: active) {
  .bv__stage,
  [data-bottle-viewer]:not([data-bv-state]) {
    border-color: CanvasText;
  }

  .bv__stage.is-engaged {
    border-color: Highlight;
  }

  .bv__stage:focus-visible {
    outline-color: Highlight;
  }

  .bv__hint,
  .bv__fallback {
    color: CanvasText;
  }
}
