/* ============================================================
   SNS Valley View — Gallery
   New in build 1j (docs/builds/1j-gallery-page.md). Not ported from the
   design handoff — no gallery page exists there — but built entirely from
   tokens, breakpoints, and component patterns the handoff and prior builds
   already established: --pad-x, the 680px/1024px/1600px edges, .overline,
   .btn, and rooms.css's own grid/figure mechanism. See
   docs/builds/1j-gallery-page.md for the sizes derivation and
   docs/DECISIONS.md for the crop check on the one non-3:2 source.
   ============================================================ */

/* 80px top matches .rooms's own top padding (rooms.css) — both are normal-
   flow sections below a normal-flow header (no overlay pull like .hero/
   .room-hero use, so there's no header-clearing math to do here, just the
   same section rhythm the rest of the site already uses). */
.gallery-page {
  padding: 80px var(--pad-x) 96px;
}

.gallery-page__head {
  margin-bottom: 48px;
}
.gallery-page__title {
  margin: 12px 0 0;
  font-family: var(--f-display);
  font-size: 44px;
  font-weight: 300;
  line-height: 1.1;
  color: var(--c-cream);
}

/* ------------------------------------------------------------
   Grid
   Four column tiers reusing this codebase's three existing breakpoint
   edges (680/1024/1600px) rather than inventing new ones — see
   gallerySizes (gallery.templ) for the full per-tier derivation this grid
   must stay in sync with. 20px gap matches rooms.css/room-detail.css's own
   grids. 13 photos don't divide evenly by any of 2/3/4 (13 is prime) —
   unlike rooms.css's grid, which specifically avoids orphaning a lone card
   because it always shows exactly 4 rooms, a photo gallery is expected to
   just flow and end wherever it ends; no column count was chosen to avoid
   a partial last row.
   ------------------------------------------------------------ */
.gallery-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
}

/* 3/2 matches rooms.css's own card ratio — 11 of the 13 source photos are
   already native 3:2, so object-fit: cover only has real cropping work to
   do on the two 4:3 aerials and the one ultra-wide (2.17:1) firepit-
   gathering photo. That last one was checked directly, not assumed safe:
   a centred 3:2 crop removes ~15% of frame width off each side (background
   wall/trellis on the left, a sliver of building on the right) and leaves
   the whole seated circle — the only photo on the site with people in it —
   fully inside the kept region. See docs/builds/1j-gallery-page.md. */
.gallery-grid__figure {
  position: relative;
  aspect-ratio: 3 / 2;
  margin: 0;
  overflow: hidden;
}
.gallery-grid__figure .responsive-image,
.gallery-grid__figure .responsive-image__real,
.gallery-grid__figure .responsive-image__blur {
  width: 100%;
  height: 100%;
}
.gallery-grid__figure .responsive-image__real,
.gallery-grid__figure .responsive-image__blur {
  object-fit: cover;
}

/* ------------------------------------------------------------
   Closing link back to the rooms
   Same .btn--ghost already used for Header's Reserve link — no new button
   variant for one more outbound link.
   ------------------------------------------------------------ */
.gallery-page__link {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  margin-top: 64px;
  padding-top: 48px;
  border-top: 1px solid var(--c-hair);
  text-align: center;
}
/* cream-55 ("body" tier), not cream-45 ("sub") — same fix room-detail.css's
   .room-detail__cta-copy already needed: cream-45 measures 4.02:1 here
   (Lighthouse color-contrast, confirmed against the real rendered page),
   short of WCAG AA's 4.5:1 for body text. cream-30/20 are reserved for
   micro-labels and legal text only (CLAUDE.md); this is a full sentence
   meant to be read, so it gets the body tier. See docs/DECISIONS.md. */
.gallery-page__link-copy {
  margin: 0;
  font-size: 14px;
  color: var(--c-cream-55);
}

/* ============================================================
   Responsive
   ============================================================ */
@media (min-width: 680px) {
  .gallery-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .gallery-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1600px) {
  .gallery-grid { grid-template-columns: repeat(4, 1fr); }
}

@media (max-width: 1024px) {
  .gallery-page { padding: 88px var(--pad-x) 72px; }
}
@media (max-width: 680px) {
  .gallery-page { padding: 72px var(--pad-x) 56px; }
  .gallery-page__title { font-size: 34px; }
  .gallery-page__link { margin-top: 48px; padding-top: 32px; }
}

/* ============================================================
   Lightbox
   Build 1k (docs/builds/1k-lightbox.md). One <dialog> per tile, opened via
   the HTML Invoker Commands API (gallery.templ) — no JS, see
   docs/DECISIONS.md for the mechanism decision and why no :target fallback
   is layered under it.
   ------------------------------------------------------------ */

/* Resets the button back to a plain, unstyled hit target the size of its
   figure — <button> ships browser chrome (padding, border, background)
   that a <figure><img> never had, and the trigger needs to look exactly
   like the image it wraps, not like a button. */
.gallery-grid__trigger {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

/* [open] on every rule below, not bare .gallery-lightbox: the UA
   stylesheet only hides a closed dialog via `dialog:not([open]) { display:
   none }`, and author CSS beats UA CSS regardless of specificity — an
   unscoped `display: flex` here would override that and force every one
   of the 13 dialogs into layout from first paint, which is exactly what
   defeats the loading="lazy" deferral this build depends on (a
   `display: none` element has no box, so the browser's lazy-load
   intersection check can never fire; a `display: flex` one does). Caught
   by the network waterfall showing all 13 full-size images fetching on
   page load before this scoping was added — see
   docs/builds/1k-lightbox.md's Gate 1 report.

   The dialog's own box is now the full viewport (not shrink-wrapped to
   the image) — it needs to be, since .gallery-lightbox__scrim-close
   (below) fills that same box to make backdrop-click-to-close work, and a
   button can only cover the area its containing block actually occupies.

   No open/close transition — a plain instant show/hide, same as native
   <dialog> gives you for free. Nothing here to kill under
   prefers-reduced-motion; deliberately not adding one keeps that true. */
.gallery-lightbox[open] {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  max-width: 100vw;
  max-height: 100vh;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  display: grid;
  place-items: center;
}
/* The visible scrim now comes from .gallery-lightbox__scrim-close's own
   background, not ::backdrop — a ::backdrop pseudo-element can't host a
   button, and backdrop-click-to-close needs something clickable there.
   Zeroed out rather than removed, so the UA's own default modal backdrop
   tint (a light dim in most browsers) doesn't show through underneath. */
.gallery-lightbox::backdrop {
  background: transparent;
}

/* Fills the entire dialog box, sitting BEHIND the image in paint order
   (plain DOM order — it's written before .gallery-lightbox__image, no
   z-index needed): a click landing on the image hits the image and stops
   there; a click anywhere else in the dialog falls through to this
   button, which closes it. This is what makes "click outside the photo to
   dismiss" work without a script — verified directly, not assumed, at
   1280px and 390px: clicks up to 2px inside the image's edge never close
   it, clicks from 10px outside it always do (docs/DECISIONS.md).
   command="close" is a real control, but a pointer-only one — tabindex
   and aria-hidden below (gallery.templ) keep it out of the tab order and
   the accessibility tree, so a keyboard user's first Tab still lands on
   the visible ✕, not an invisible full-viewport stop. --c-bg at high
   opacity, same flat-scrim family as the testimonial scrim
   (testimonials.css) — but darker: that scrim exists to keep quote text
   readable over a still-visible video, this one exists to isolate a
   single photograph, so it leans much closer to opaque. */
.gallery-lightbox__scrim-close {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  background: rgba(9, 9, 9, 0.92);
  cursor: pointer;
}

/* Pinned to the viewport corner rather than sitting in flow above the
   image (the dialog's own box no longer shrink-wraps to make room for it)
   — position: absolute resolves against the dialog itself, which is the
   full viewport now. Stacks above the scrim button by DOM order (written
   after it) without needing an explicit z-index. */
.gallery-lightbox__close {
  position: absolute;
  top: 16px;
  right: 16px;
  padding: 8px;
  margin: 0;
  border: 0;
  background: none;
  font-family: var(--f-ui);
  font-size: 20px;
  line-height: 1;
  color: var(--c-cream-55);
  cursor: pointer;
}
.gallery-lightbox__close:hover { color: var(--c-cream); }

/* Overrides responsive-image.css's grid-figure-shaped defaults (width/
   height: 100%, object-fit: cover) — the lightbox has no fixed box to
   fill and crop into; the image sizes itself, uncropped, up to the
   viewport cap below. .responsive-image needs display: inline-block so it
   shrink-wraps to the real <img>'s rendered box: both the blur placeholder
   and the cached-variant layer (gallery.templ) are absolutely positioned
   to fill that same box (responsive-image.css's technique, extended here
   with one more layer), and a block div would otherwise stretch to the
   dialog's full width instead of matching the image's actual
   (aspect-ratio-constrained) size. Positioned relative + stacks above the
   scrim-close button by DOM order, same as .gallery-lightbox__close. */
.gallery-lightbox .responsive-image {
  position: relative;
  display: inline-block;
  max-width: min(1600px, 92vw);
  max-height: 92vh;
}
.gallery-lightbox .responsive-image__real,
.gallery-lightbox .responsive-image__blur,
.gallery-lightbox__cached {
  width: auto;
  height: auto;
  max-width: min(1600px, 92vw);
  max-height: 92vh;
  object-fit: contain;
}
.gallery-lightbox .responsive-image__real {
  display: block;
}
/* Same absolute-fill treatment as .responsive-image__blur, sitting between
   it and .responsive-image__real in paint order (written after the blur,
   before the real picture, in gallery.templ) — resolves to the grid
   tile's own srcset candidate (sizes="galleryGridSizes" on its <source>s),
   so wherever that's smaller than the real layer's target it appears
   instantly from the tile's own already-fetched bytes, then the real
   layer paints over it once its own, larger fetch completes. */
.gallery-lightbox__cached {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
