/* =============================================================================
   60-gallery.css — THE PRODUCT GALLERY (single product, left half)
   -----------------------------------------------------------------------------
   WHAT WAS WRONG

   1  A 5-across thumbnail GRID under the plate. This catalogue ships products
      with fifteen images, so the "gallery" was a 643px photograph sitting on a
      three-row, 15-cell block of identical squares — 470px of contact sheet
      that pushed the buy box's own content a screen further down and read as
      an upload folder, not a gallery.

   2  NO ZOOM, NO LIGHTBOX. `cursor: zoom-in` was declared on the main image
      and nothing happened when you clicked it. The only motion was a 1.04
      hover nudge, which is a hover state, not an inspection tool. These are
      hand-painted Yixing cups; the detail IS the product.

   3  A THIN ACTIVE STATE — `border-color: var(--color-primary)` on a 2px
      border, the same value used for :hover, so the frame you were looking at
      and the frame under the cursor were indistinguishable.

   4  NO GRADING. The scraped catalogue photography runs from a black studio
      table to a white lightbox, and the plate handed each one straight through
      on a flat --color-bg-subtle ground. The Bible's rule for any set of
      photographs is one common key.

   WHAT THIS DOES

   A RAIL AND A PLATE. The thumbnails become a vertical rail exactly as tall as
   the plate beside them, scrolling inside itself with a snap and a bottom fade,
   so fifteen frames cost the page nothing — the composition is identical at 3
   images and at 15. The rail is closed on its right by the hairline it shares
   with the plate, so the column resolves on all four edges whatever the count.

   THE PLATE IS GRADED. A radial ground, a vignette scrim and one shared
   saturation/contrast lift put every photograph into the same key, and carry
   the hero's own bottom HUD band — a tracked micro-label left, an affordance
   right — over the foot of the frame.

   THE ZOOM IS REAL. Hovering the plate drives the image to 2.05x and then
   drifts the origin slowly across the whole surface, so every part of the
   photograph is inspected at twice size without a lightbox, a plugin or a line
   of JavaScript. Press and hold freezes the drift on whatever you are looking
   at. Under prefers-reduced-motion it degrades to a static centre zoom.

   THE COUNT IS REAL TOO. `:has()` reads how many thumbnails the template
   product's own photography carry the frame with no printed metadata over it.

   This file loads after every media query in main.css, so it carries the whole
   responsive ladder for the layout it owns.
   ========================================================================== */


/* =============================================================================
   1 · THE TWO HALVES
   -----------------------------------------------------------------------------
   52% / 1fr with a 50px gutter gave the gallery the larger half and the buy
   box the smaller one, which is backwards: the plate is one square photograph
   and the right column carries the title, the price, the variant, the action,
   the trust row and a nine-row specification. They come to parity, with a
   gutter that scales.
   ========================================================================== */

/* COLUMN-GAP ONLY, and no page padding. 58-pdp-frame.css owns the page's
   vertical rhythm, the layout's rows and its row-gap; this file loads after it,
   so a `gap` shorthand or a `padding` here would silently reset them. The two
   files touch this selector for two different axes and neither overwrites the
   other. */
.single-product-page .single-product-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.14fr) minmax(0, 1fr);
  column-gap: clamp(36px, 3.8vw, 58px);

  /* The second half of that handshake. 58-pdp-frame.css wants row one to be
     "the gallery, sized to its own content and never taller" and row two to
     absorb the slack the summary column's two-row span creates — but with
     `max-content auto` BOTH rows are intrinsic, so the grid hands the slack to
     both and opens a ~320px hole under the plate. `1fr` on row two is what
     that comment actually asks for: the gallery row stays exactly the plate's
     height and every pixel of the difference lands in the maker's note. */
  grid-template-rows: max-content 1fr;
}


/* =============================================================================
   2 · THE GALLERY — a rail and a plate
   ========================================================================== */

.single-product-page .product-gallery {
  --rail: clamp(58px, 5.4vw, 80px);
  --rail-gap: clamp(12px, 1.2vw, 17px);

  /* THE RAIL IS OUT OF FLOW, and that is the whole trick. Laid out as a grid
     column it would size the row by its own content — fifteen 69px frames is
     1,100px — and a plate carrying `aspect-ratio: 1/1` would then be STRETCHED
     to that height and take its width from it, blowing a 528px plate out to
     1,023px straight over the summary column. Taken out of flow, the plate
     alone sizes the column and the rail is told to be exactly as tall as it. */
  position: relative;
  /* main.css sets `position: sticky; top: 100px` here. Switching to relative
     without releasing `top` would leave the offset in force and push the whole
     gallery 100px down its own column. */
  top: auto;
  display: block;
  padding-left: calc(var(--rail) + var(--rail-gap));

  /* The frame count, read off the DOM in §5 and printed by the plate's HUD. */
}

/* One-image products have no .product-thumbs at all; the plate takes the whole
   column rather than leaving an 80px pocket down its left edge. */
.single-product-page .product-gallery:not(:has(.product-thumbs)) {
  padding-left: 0;
}


/* =============================================================================
   3 · THE PLATE
   ========================================================================== */

.single-product-page .product-main-image {
  position: relative;
  width: 100%;
  margin: 0;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--ctl-hair);
  border-radius: var(--ctl-r);
  /* The common ground every photograph is seated on: a soft crown of surface
     light at the top, falling to the deepest neutral at the foot. */
  background:
    radial-gradient(118% 86% at 50% 4%,
      color-mix(in srgb, var(--color-surface-2) 74%, transparent) 0%,
      transparent 70%),
    var(--night-2);
  cursor: zoom-in;
}

.single-product-page .product-main-image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* The grading pass. Small numbers on purpose: enough to pull a washed-out
     lightbox shot and a black studio shot towards each other, not enough to
     misrepresent a glaze colour. */
  filter: saturate(1.05) contrast(1.04) brightness(1.01);
  transform-origin: 50% 50%;
  transition: transform 0.9s var(--ctl-ease), filter 0.6s var(--ctl-ease);
  will-change: transform;
}

/* --- the HUD band -----------------------------------------------------------
   The hero closes its frame with a tracked label on the left and a counter on
   the right, over a scrim. The plate does the same. ::before carries the scrim
   AND the right-hand affordance; ::after carries the left-hand count. Both
   step out of the way the moment the zoom starts. */

.single-product-page .product-main-image::before {
  content: "HOVER TO ZOOM";
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  padding: 14px 16px;
  /* vignette, then the foot scrim the HUD text sits on */
  background:
    radial-gradient(126% 108% at 50% 42%, transparent 54%, rgba(var(--ctl-night-rgb), 0.34) 100%),
    linear-gradient(180deg, transparent 58%, rgba(var(--ctl-night-rgb), 0.62) 100%);
  font-family: var(--font-primary);
  font-size: 0.7rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--color-text-light);
  pointer-events: none;
  transition: opacity 0.5s var(--ctl-ease);
}

/* --- the zoom ---------------------------------------------------------------
   16 seconds is deliberate. The first 1.6s is the push to 2.05x; the remaining
   14 walk the origin over the four quarters of the frame, slowly enough that
   it reads as a camera move rather than a carousel. Hold the mouse down and it
   stops where you stopped it. */

@keyframes sp-plate-inspect {
  0%          { transform: scale(1);    transform-origin: 50% 50%; }
  10%         { transform: scale(2.05); transform-origin: 50% 50%; }
  32%         { transform: scale(2.05); transform-origin: 15% 17%; }
  54%         { transform: scale(2.05); transform-origin: 85% 21%; }
  76%         { transform: scale(2.05); transform-origin: 18% 83%; }
  96%, 100%   { transform: scale(2.05); transform-origin: 83% 81%; }
}

/* Gated on a real pointer: a touch device has no hover to zoom with, and
   sticky-hover would otherwise strand the plate at 2x after a tap. */
@media (hover: hover) and (pointer: fine) {
  .single-product-page .product-main-image:hover img {
    animation: sp-plate-inspect 16s var(--ctl-ease) both;
    filter: saturate(1.06) contrast(1.06) brightness(1.03);
  }
  .single-product-page .product-main-image:active {
    cursor: zoom-out;
  }
  .single-product-page .product-main-image:active img {
    animation-play-state: paused;
  }
  .single-product-page .product-main-image:hover::before,
  .single-product-page .product-main-image:hover::after {
    opacity: 0;
  }
}

/* --- the sale badge ---------------------------------------------------------
   It used to be a solid chip in the corner of the photograph. It becomes HUD:
   the same tracked micro-voice as the rest of the frame, on smoked glass. */

.single-product-page .sp-gallery-badges {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 4;
  display: flex;
  flex-direction: column;
  gap: 6px;
  pointer-events: none;
}

.single-product-page .sp-gallery-badges .badge {
  padding: 7px 12px;
  border: 1px solid var(--ctl-hair-2);
  border-radius: var(--ctl-r);
  background: rgba(var(--ctl-night-rgb), 0.62);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-family: var(--font-primary);
  font-size: 0.7rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--paper);
}


/* =============================================================================
   4 · THE RAIL
   -----------------------------------------------------------------------------
   Exactly the plate's height, always — the grid row is sized by the plate's
   1:1 ratio and the rail stretches into it. Fifteen frames scroll inside the
   rail; three sit at its top and the shared hairline closes the column under
   them. The page height never changes with the image count.
   ========================================================================== */

.single-product-page .product-thumbs {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--rail);
  display: flex;
  flex-direction: column;
  align-content: flex-start;
  gap: 8px;
  min-height: 0;
  padding: 0 12px 0 0;
  /* the spine: with three frames it is what closes the column under them, so
     the composition is identical at any image count */
  border-right: 1px solid var(--ctl-hair);
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-snap-type: y proximity;
  /* The affordance that replaces a scrollbar nobody looks at: the rail dissolves
     at its foot exactly when there is more below it. */
  -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 44px), transparent 100%);
          mask-image: linear-gradient(180deg, #000 calc(100% - 44px), transparent 100%);

  scrollbar-width: thin;
  scrollbar-color: var(--ctl-hair-2) transparent;
}
.single-product-page .product-thumbs::-webkit-scrollbar { width: 3px; }
.single-product-page .product-thumbs::-webkit-scrollbar-track { background: transparent; }
.single-product-page .product-thumbs::-webkit-scrollbar-thumb {
  background: var(--ctl-hair-2);
  border-radius: 3px;
}

.single-product-page .product-thumb {
  position: relative;
  flex: 0 0 auto;
  width: 100%;
  aspect-ratio: 1 / 1;
  margin: 0;
  border: 1px solid transparent;
  border-radius: var(--ctl-r);
  background: var(--night-2);
  overflow: hidden;
  cursor: pointer;
  scroll-snap-align: start;
  /* Unselected frames are held back — this is what makes the selected one
     unmistakable without spending a colour on it. */
  opacity: 0.52;
  filter: grayscale(0.28) contrast(1.02);
  transition:
    opacity 0.45s var(--ctl-ease),
    filter 0.45s var(--ctl-ease),
    border-color 0.45s var(--ctl-ease),
    box-shadow 0.45s var(--ctl-ease);
}

.single-product-page .product-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.single-product-page .product-thumb:hover {
  opacity: 0.88;
  filter: none;
  border-color: var(--ctl-hair-2);
}
/* THE ACTIVE STATE — full opacity, full colour, a paper hairline with an inner
   ring. (It used to carry a printed index too; numbers burned onto product
   photography read as watermarks, so they are gone. Three signals is plenty.) */
.single-product-page .product-thumb.active {
  opacity: 1;
  filter: none;
  border-color: var(--ctl-hair-3);
  box-shadow:
    inset 0 0 0 1px rgba(var(--ctl-night-rgb), 0.55),
    0 0 0 1px var(--ctl-hair);
  cursor: default;
}
/* =============================================================================
   6 · THE LADDER
   -----------------------------------------------------------------------------
   Part files load after every media query in main.css, so main.css's steps for
   .single-product-layout (48%/1fr at 1024, one column at 768, 46%/1fr between
   620 and 768) cannot reach past this file. The whole ladder is restated.
   ========================================================================== */

@media (max-width: 1200px) {
  .single-product-page .single-product-layout {
    grid-template-columns: minmax(0, 1.03fr) minmax(0, 1fr);
    gap: clamp(32px, 3.4vw, 48px);
  }
}

@media (max-width: 1200px) {
  .single-product-page .product-gallery {
    --rail: clamp(56px, 5vw, 70px);
    --rail-gap: 13px;
  }
}

@media (max-width: 1024px) {
  .single-product-page .single-product-layout {
    grid-template-columns: minmax(0, 1.08fr) minmax(0, 1fr);
    column-gap: 30px;
  }
  .single-product-page .product-gallery {
    --rail: 54px;
    --rail-gap: 11px;
  }
  .single-product-page .product-thumbs { padding-right: 10px; }
}

/* 768 is 58-pdp-frame.css's own stacking step — it unwinds the layout's
   explicit rows and column-2 placement there, so this file has to hand over
   its columns on exactly the same line or .product-info would be left placed
   in a column that no longer exists. The rail turns on its side and becomes a
   filmstrip under the plate: same object, same active state, same count. */
@media (max-width: 768px) {
  .single-product-page .single-product-layout {
    grid-template-columns: minmax(0, 1fr);
    column-gap: 0;
    /* 58-pdp-frame.css unwinds its explicit rows here so DOM order becomes
       reading order. Its `grid-template-rows: none` cannot reach past this
       file, so the row track is handed back on the same line as the columns —
       otherwise the summary column would land in a 1fr row of its own. */
    grid-template-rows: none;
  }
  .single-product-page .product-gallery {
    position: static;
    display: block;
    padding-left: 0;
  }
  .single-product-page .product-main-image {
    margin-bottom: 12px;
  }
  .single-product-page .product-thumbs {
    position: static;
    width: auto;
    bottom: auto;
    display: flex;
    flex-direction: row;
    height: auto;
    padding: 0 0 8px;
    border-right: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 46px), transparent 100%);
            mask-image: linear-gradient(90deg, #000 calc(100% - 46px), transparent 100%);
  }
  .single-product-page .product-thumbs::-webkit-scrollbar { height: 3px; width: auto; }
  .single-product-page .product-thumb {
    width: 74px;
    flex: 0 0 74px;
  }
  /* Touch has no hover, so the plate's zoom is unreachable and its prompt is a
     lie. The count stays; the affordance goes. */
  .single-product-page .product-main-image {
    cursor: default;
  }
  .single-product-page .product-main-image::before { content: ""; }
}

@media (max-width: 480px) {
  .single-product-page .product-thumb {
    width: 64px;
    flex: 0 0 64px;
  }
  .single-product-page .product-thumbs { gap: 8px; }
  .single-product-page .sp-gallery-badges { top: 10px; left: 10px; }
}


/* =============================================================================
   7 · REDUCED MOTION
   The zoom is the feature, so it survives — as a static 1.85x centre push with
   no drift and no travel.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .single-product-page .product-main-image:hover img {
    animation: none;
    transform: scale(1.85);
  }
  .single-product-page .product-main-image img,
  .single-product-page .product-thumb,
  .single-product-page .product-main-image::before {
    transition-duration: 0.01ms;
  }
}
