.gallery-filters {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 3rem;
}
.gf-btn {
  padding: 0.55em 1.25em;
  border-radius: var(--r-pill);
  font-family: var(--font-b);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  border: 1.5px solid var(--pink-mid);
  background: transparent;
  color: var(--ink-lt);
  cursor: pointer;
  transition: all 0.25s;
}
.gf-btn:hover,
.gf-btn.active {
  background: var(--pink);
  border-color: var(--pink);
  color: white;
}
.gallery-masonry {
  columns: 3;
  column-gap: 1.25rem;
}
.gallery-item {
  break-inside: avoid;
  margin-bottom: 1.25rem;
  border-radius: var(--r-md);
  overflow: hidden;
  cursor: pointer;
  position: relative;
}
.gallery-item:hover .gi-overlay {
  opacity: 1;
}
.gi-ph {
  width: 100%;
  background-image:
    repeating-linear-gradient(45deg, transparent, transparent 10px, oklch(0.72 0.09 350 / 0.05) 10px, oklch(0.72 0.09 350 / 0.05) 20px),
    linear-gradient(160deg, var(--pink-mid) 0%, var(--pink-bg) 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  padding: 1.5rem;
  color: var(--ink-lt);
  font-size: 0.65rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-family: var(--font-b);
  font-weight: 500;
  text-align: center;
}
.gi-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, oklch(0.52 0.098 350 / 0.6) 0%, transparent 60%);
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  display: flex;
  align-items: flex-end;
  padding: 1.25rem;
}
.gi-label {
  color: white;
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* Varying heights for masonry effect.
   `img.` prefix is required: the generic `.gallery-item img { height:100% }`
   rule below is more specific than a bare `.gi-hN`, and since .gallery-item
   has no intrinsic height, 100% resolves against a missing image's zero
   height and collapses the slot. These must win. */
img.gi-h1, .gi-h1 { height: 280px; }
img.gi-h2, .gi-h2 { height: 200px; }
img.gi-h3, .gi-h3 { height: 360px; }
img.gi-h4, .gi-h4 { height: 240px; }
img.gi-h5, .gi-h5 { height: 320px; }
img.gi-hero, .gi-hero { height: 340px; }

/* Real photo item.
   No `height` here on purpose — the .gi-hN classes above own the slot height
   and share this rule's specificity, so setting height:100% would win by
   source order and collapse every slot whose image hasn't loaded. */
.gallery-item img {
  width: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.5s var(--ease);
}
/* The one non-masonry image (.gi-hero sets its own height) still fills. */
.gallery-item > img:not([class*="gi-h"]) { height: 100%; }
/* Masonry photos fill the gi-hN height set above — object-fit crops the
   photo to that box. The height must stay authoritative (not `auto`), or a
   not-yet-added image collapses the slot to ~16px and breaks the masonry.
   The gradient shows through until the real file lands; color:transparent
   + font-size:0 suppress the broken-image glyph and alt text meanwhile. */
.gallery-item img.gi-img {
  background-image:
    repeating-linear-gradient(45deg, transparent, transparent 10px, oklch(0.72 0.09 350 / 0.05) 10px, oklch(0.72 0.09 350 / 0.05) 20px),
    linear-gradient(160deg, var(--pink-mid) 0%, var(--pink-bg) 100%);
  color: transparent;
  font-size: 0;
  overflow: hidden;
}
/* Hide the browser's broken-image glyph while a file is still missing.
   `img:not([src])` won't match (the src exists but 404s), so target the
   loaded state instead: an image with no intrinsic size gets its content
   box hidden, while the CSS background gradient still paints. Removed
   automatically once the real photo loads. */
.gallery-item img.gi-img:not(.is-loaded) {
  content-visibility: hidden;
}
.gallery-item:hover img { transform: scale(1.04); }

/* Treatment label: always faintly visible over an empty slot so the grid
   reads as intentional, and lifts to full white on hover once a photo is in.
   Centred rather than bottom-aligned — with no photo behind it, a low-left
   label looks like a caption for something that isn't there. */
.gallery-item .gi-overlay {
  opacity: 1;
  background: none;
  align-items: center;
  justify-content: center;
  text-align: center;
  transition: opacity 0.35s var(--ease), background 0.35s var(--ease);
}
.gallery-item .gi-label {
  color: var(--ink-lt);
  transition: color 0.35s var(--ease), text-shadow 0.35s var(--ease);
}
/* Once the image loads it paints over the gradient, so restore the dark
   scrim + white label for legibility on top of a photo. */
.gallery-item:hover .gi-overlay {
  background: linear-gradient(to top, oklch(0.52 0.098 350 / 0.6) 0%, transparent 60%);
}
.gallery-item:hover .gi-label {
  color: white;
  text-shadow: 0 1px 6px oklch(0.3 0.05 350 / 0.5);
}

/* When the real photos arrive, add class="has-photos" to .gallery-masonry
   (or drop this block) to return the labels to bottom-left captions that
   only appear on hover — the intended design for a populated gallery. */
.gallery-masonry.has-photos .gi-overlay {
  opacity: 0;
  align-items: flex-end;
  justify-content: flex-start;
  text-align: left;
  background: linear-gradient(to top, oklch(0.52 0.098 350 / 0.6) 0%, transparent 60%);
}
.gallery-masonry.has-photos .gi-label { color: white; }
.gallery-masonry.has-photos .gallery-item:hover .gi-overlay { opacity: 1; }

@media (max-width: 900px) { .gallery-masonry { columns: 2; } }
@media (max-width: 560px) {
  .gallery-masonry { columns: 1; }
  /* Single column: cap the tallest slots so one image doesn't dominate
     the whole viewport on a phone. */
  img.gi-h1, img.gi-h2, img.gi-h3, img.gi-h4, img.gi-h5, img.gi-hero,
  .gi-h1, .gi-h2, .gi-h3, .gi-h4, .gi-h5, .gi-hero { height: 260px; }
}
