/* ==========================================================================
   Bento gallery — scrubbed zoom hero
   --------------------------------------------------------------------------
   Companion to assets/js/bento.js. The effect is a GSAP Flip between two
   states of the SAME grid: the small mosaic (default) and the huge one
   (.is-final). JS applies .is-final for one frame, measures, removes it, then
   scrubs the delta against scroll. So the only thing this file has to get
   right is: both states must be fully declared at every breakpoint, and
   nothing may clip or contain the pinned wrapper.

   Every rule is scoped to body.bento-page — currently product.php — so it
   cannot leak to other pages.
   NOT wrapped in @layer — main.css is unlayered and would win. (Same
   constraint documented in apple.css §1.)

   Tokens come from custom.css only (--bg, --mist, --radius, --ink-soft,
   --glass-bg, --nav-font), never from apple.css: the bento page runs on the
   classic Bootstrap layout, so the .ap layer is not loaded. That also means
   both light and dark themes are covered for free, since custom.css redefines
   all of these under [data-bs-theme="dark"].
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Blocker: main.css:57 sets :root { scroll-behavior: smooth }.
   GSAP documents native smooth scrolling as incompatible with scrub — the
   browser's own easing fights the scrub and makes ScrollTrigger.refresh()
   restore the wrong position. custom.css only resets it under
   prefers-reduced-motion, i.e. exactly when the effect is switched off.
   bento.js sets this inline too, so the fix does not depend on :has().
   -------------------------------------------------------------------------- */
html:has(body.bento-page) { scroll-behavior: auto; }

/* --------------------------------------------------------------------------
   2. Pin hygiene. ScrollTrigger's pin switches the wrapper to position:fixed;
   any ancestor with overflow != visible, or with transform/filter/contain
   (which would establish a containing block), breaks that silently — the
   element simply scrolls away instead of sticking.
   <main class="main"> carries no CSS at all today, so it is already a clean pin
   parent; this rule is an assertion so a future edit to main.css cannot quietly
   kill the effect. Note there is deliberately NO blanket rule un-clipping
   <section> here — the stage sits outside every section, and main.css's
   overflow:clip is what keeps the Bootstrap rows from spilling sideways.
   -------------------------------------------------------------------------- */
body.bento-page main.main { overflow: visible; }

/* --------------------------------------------------------------------------
   3. The pinned stage.
   overflow:hidden HERE is correct and necessary — it clips the ~300vw final
   grid to the viewport. It sits on the pinned element itself, never on an
   ancestor of it, so it does not trip rule 2.
   svh rather than vh: on mobile the URL bar collapsing changes vh mid-scroll,
   which would shift the pin distance under the user.
   -------------------------------------------------------------------------- */
body.bento-page .bento-wrap {
  position: relative;
  width: 100%;
  height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--bg);
  /* The grid sizes its middle column in cqw against this element. The wrap
     excludes the scrollbar where 100vw does not, and that 15px is the
     difference between a banner cell that is 16:9 and one that is 1.76:1. */
  container-type: inline-size;
}

/* Fallback for browsers without svh units. */
@supports not (height: 100svh) {
  body.bento-page .bento-wrap { height: 100vh; }
}

/* --------------------------------------------------------------------------
   4. The grid. Both states are driven from the custom properties below, so a
   breakpoint only has to restate numbers — and .is-final MUST be restated
   alongside them. Flip measures real computed geometry, so a --final left on
   desktop numbers at 480px produces a wrong, jumping zoom rather than a
   graceful one.

   The two track sizes are derived from the stage rather than hand-tuned, so
   the mosaic fills .bento-wrap exactly and every tile is as large as the
   pinned viewport allows. Percentages, not vw/vh: the grid is 100% x 100% of a
   wrap that is 100svh, so both axes are definite and percentages resolve —
   and unlike 100vw they exclude the scrollbar, so the grid never overflows
   into the wrap's overflow:hidden.
   -------------------------------------------------------------------------- */
body.bento-page .bento {
  --bgap: 1vh;

  /* The BANNER'S HEIGHT drives the whole layout, and everything else is derived
     from it. The middle column is sized from the banner rather than the banner
     from its column, which is the only way a cell comes out 16:9 at every
     viewport: a percentage track resolves against the stage, so a cell's shape
     otherwise follows the viewport's shape. Before this, the banner's cell was
     1.05:1 at 1440x900 holding 16:9 art, and object-fit: contain drew it at 59%
     of the cell height between two bands of --mist.

     cqw, not vw: .bento-wrap is the container, and its width excludes the
     scrollbar. vw includes it, which here is 15px -> 3px of letterbox left over.

     The min() is the guard. On a tall viewport (iPad Pro portrait) 38svh asks
     for a middle column wider than the stage, and the 1fr columns either side
     would collapse to nothing. Capping the ASK rather than the result keeps the
     cell exactly 16:9 instead of clamping it back out of ratio. */
  --bbanner: min(38svh, calc(44cqw * 9 / 16));
  --bmid:    calc(var(--bbanner) * 16 / 9);
  --brow-b:  calc((var(--bbanner) - var(--bgap)) / 2);
  --brow-a:  calc((100% - 2 * var(--bgap) - var(--bbanner)) / 2);

  position: relative;
  width: 100%;
  height: 100%;
  flex: none;
  display: grid;
  gap: var(--bgap);
  /* Rows are [a, b, b, a] — not four equal tracks. The banner spans the two b
     rows, so a and b together still fill the stage exactly, and the outer
     columns keep two half-height tiles each. */
  grid-template-columns: minmax(0, 1fr) var(--bmid) minmax(0, 1fr);
  grid-template-rows: var(--brow-a) var(--brow-b) var(--brow-b) var(--brow-a);
  justify-content: center;
  align-content: center;
}

/* The zoomed state. Applied by JS for a single frame to be measured, and again
   by Flip as the scrub target — never left on the element between frames.

   Every column goes to 100% and justify-content:center then centres the middle
   one on the stage. The b rows take (stage - gap) / 2 each, so the banner —
   rows 2-3 of the middle column — measures exactly 100% tall.

   --brow-a is deliberately NOT restated. All that matters is that rows 1 and 4
   stay equal to each other: the banner is then symmetric about the grid's
   centre line, and align-content:center lands it flush whatever the outer rows
   measure. The old formula needed all four rows equal and so had to be
   re-derived at every breakpoint; this one cannot drift. */
body.bento-page .bento.is-final {
  grid-template-columns: repeat(3, 100%);
  --brow-b: calc((100% - var(--bgap)) / 2);
}

/* Desktop mosaic: 7 tiles — 2 in the left column, 3 in the middle, 2 in the
   right. The middle column's centre cell is the banner, and the four outer
   tiles are each half the stage tall. */
body.bento-page .bento > :nth-child(1) { grid-area: 1 / 1 / 3 / 2; }
body.bento-page .bento > :nth-child(2) { grid-area: 1 / 2 / 2 / 3; }
body.bento-page .bento > :nth-child(3) { grid-area: 2 / 2 / 4 / 3; }
body.bento-page .bento > :nth-child(4) { grid-area: 1 / 3 / 3 / 4; }
body.bento-page .bento > :nth-child(5) { grid-area: 3 / 1 / 5 / 2; }
body.bento-page .bento > :nth-child(6) { grid-area: 3 / 3 / 5 / 4; }
body.bento-page .bento > :nth-child(7) { grid-area: 4 / 2 / 5 / 3; }

/* --------------------------------------------------------------------------
   5. Tiles. Every tile is filled edge to edge — the mosaic reads as a collage,
   not as art floating on panels, and a render that used to draw at ~24% of its
   cell (contain, plus up to 28px of inset) now draws at 100% of it.

   That crops. The art in these cells is poster-style, drawn at roughly 16:9
   with headlines baked into the pixels (the same fact section 9 relies on
   below), and the satellite cells are not 16:9 — they are 2.25:1 or 0.9:1 — so
   a headline can lose its edge. The deliberate trade, and it is now made only
   by the satellites: section 4 sizes the BANNER's cell to 16:9 exactly, which
   is the one tile whose text the page is built around. includes/bento.php
   still emits .is-contain for exactly those tiles, so restoring uncropped art
   is one rule here, not a markup change; the per-product fix is a re-crop
   under admin -> Hero mosaic -> Tiles.

   .is-banner is the one exception — see below. The tinted panel and 14px
   radius are the site's own --mist / --radius, so the mosaic reads as part of
   the Bootstrap layer rather than a transplant; --mist still shows through
   wherever a tile's own pixels are transparent.
   -------------------------------------------------------------------------- */
body.bento-page .bento__item {
  position: relative;
  overflow: hidden;
  margin: 0;
  background: var(--mist);
  border-radius: var(--radius);
}

body.bento-page .bento__item img {
  display: block;
  width: 100%;
  height: 100%;
}

body.bento-page .bento__item.is-cover img,
body.bento-page .bento__item.is-contain img {
  object-fit: cover;
  object-position: 50% 50%;
}

/* Poster art: 16:9, with the logo, product name, strapline and callouts baked
   into the pixels. The one tile still fitted rather than filled — this is the
   case section 9 spells out below, only it applies at every width, because
   cropping this art cuts off the text it exists to show, and unlike the
   satellites this is the tile the page is built around. It also rides the
   centre cell, which finishes the zoom flush to the viewport, where a 16:9
   banner fills a 16:9 screen exactly; covering it instead would shave the
   callouts on anything wider or narrower than 16:9.
   contain is now free of its old cost: section 4 derives the middle column's
   width from this cell's height, so the cell IS 16:9 and there is nothing left
   to letterbox. It stays contain rather than cover because the cell is only
   16:9 to within the min() guard — on a very tall viewport the cap bites, and
   cover would crop the callouts there rather than show a hairline of --mist. */
body.bento-page .bento__item.is-banner img {
  object-fit: contain;
  object-position: 50% 50%;
}

/* --------------------------------------------------------------------------
   6. Scroll cue. Sits above the grid; fades out as the zoom starts (JS drives
   the opacity so it tracks the scrub rather than guessing at a duration).
   -------------------------------------------------------------------------- */
body.bento-page .bento-cue {
  position: absolute;
  left: 50%;
  bottom: clamp(16px, 3vh, 34px);
  transform: translateX(-50%);
  z-index: 2;
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: 999px;
  background: var(--glass-bg);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  box-shadow: var(--shadow-sm);
  color: var(--ink-soft);
  font-family: var(--nav-font);
  font-size: .8125rem;
  letter-spacing: .02em;
  text-decoration: none;
  cursor: pointer;
}

body.bento-page .bento-cue:hover { color: var(--ink); }

body.bento-page .bento-cue:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

body.bento-page .bento-cue::after {
  content: "";
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-1px) rotate(45deg);
}

/* Two blocks have been deleted from this file rather than renumbered, both for
   the same reason: a media query that can never match is a trap, because it is
   the obvious place to go and edit when the thing it names misbehaves.

   - A 2 x 6 mobile re-cut of the mosaic with its own eight :nth-child rules. It
     shared the phone breakpoint with the block below, which has equal
     specificity, comes later, and sets grid-area: auto !important — so every
     one of its rules was already overridden. Eight stale tile positions behind
     a seven-tile grid is exactly the silent mismatch includes/bento.php warns
     about.
   - A 1023.98px "gentler zoom" that landed .is-final at 72% x 81% instead of
     filling the stage. The mosaic now stops at 1200px and .is-final only exists
     while the zoom is running, so nothing below 1200 can ever be in that state. */

/* --------------------------------------------------------------------------
   8. Reduced motion. bento.js creates no ScrollTrigger at all here, so the
   stage must stand on its own: release the viewport lock and let the mosaic
   scroll past as an ordinary block. This is also the no-JS / blocked-CDN
   presentation, since the grid is CSS-first and the zoom is enhancement.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  body.bento-page .bento-wrap {
    height: auto;
    overflow: visible;
    padding: 12px 0;
  }
  /* Must stay a length. The wrap is height:auto here, so section 4's
     percentage row would resolve to auto and collapse to the image height.
     Only --brow-a needs it: --brow-b is derived from --bbanner, which is already
     absolute (svh and cqw), so the banner keeps its 16:9 cell here too. */
  body.bento-page .bento     { --brow-a: 24vh; }
  body.bento-page .bento-cue { display: none; }
}

/* --------------------------------------------------------------------------
   9. Tablet and below — the mosaic stops being a mosaic.

   One tile only: the product's own 16:9 hero banner, the single piece of art
   that is both product-specific and legible without a desktop's width.

   The 1200px line is a device boundary, not a size one, and it is shared with
   bento.js — THE TWO MUST MOVE TOGETHER. The mosaic's whole point is the pinned
   zoom that ends on the banner; the mosaic without it is just seven pictures
   scrolling past. So the width at which the JS stops pinning has to be the
   width at which the CSS stops laying out a mosaic, or one of two broken states
   appears: a static mosaic no one can zoom, or a single tile that still
   scroll-jacks a full viewport to magnify nothing.

   1200 rather than 992 because every iPad is below it in BOTH orientations —
   mini 1133, 10.2" 1080, Air/Pro 11" 1194 — and a pinned scroll-jack is the
   interaction a touch screen suits worst. It also keeps the poster legible:
   the art carries the product name, spec callouts and strapline as pixels, and
   in a 3-column mosaic at 1024px each tile is ~330px wide, where none of that
   survives. The gallery further down the page carries the rest of the imagery.
   -------------------------------------------------------------------------- */
@media (max-width: 1199.98px) {
  /* The wrap no longer has to box the mosaic into one pinned screen. The side
     gutter grows with the viewport rather than staying at the phone's 16px: at
     1199px a flush-to-the-edge banner reads as a stray full-bleed band, since
     nothing else on the page runs edge to edge. clamp() rather than a second
     breakpoint, because this is a smooth quantity and not a change of layout. */
  body.bento-page .bento-wrap {
    height: auto;
    overflow: visible;
    padding: clamp(18px, 2.4vw, 32px) clamp(16px, 3.2vw, 40px) 6px;
  }

  body.bento-page .bento {
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: none;
    grid-auto-rows: auto;
    width: 100%;
    gap: 12px;
  }

  /* Section 4 hard-codes a grid-area per child for the 3-column mosaic; clear
     them or the tiles keep trying to occupy a grid that no longer exists here.
     !important because those are plain :nth-child rules of equal specificity. */
  body.bento-page .bento > * { grid-area: auto !important; }

  /* The banner is pinned to cell 3 by product.php, but only for products whose
     art has shipped — so select it by class, not by position. Four products
     (valt-dental-laser, as-200e, air-bridge, perfit-zr) have no renders at all
     and fill every cell from $heroPool; :has() picks the first pool photo for
     those rather than leaving an empty hero. */
  body.bento-page .bento > *          { display: none; }
  body.bento-page .bento > .is-banner { display: block; }
  body.bento-page .bento:not(:has(.is-banner)) > :first-child { display: block; }

  body.bento-page .bento__item { aspect-ratio: 4 / 3; }

  /* The hero banner is the one tile whose own ratio is known, so give it that
     instead of boxing 16:9 art into a 4:3 card and letterboxing what is already
     the widest thing on the page. */
  /* .is-banner keeps section 5's contain, for the same reason it has it there:
     cropping a poster cuts off the text it exists to show. In a 16:9 card that
     costs nothing anyway. Everything else fills its card, as on desktop — and
     only .is-banner or the :first-child fallback above is ever displayed here,
     so no other fit mode needs an override. */
  body.bento-page .bento__item.is-banner { aspect-ratio: 16 / 9; }
}
