/* ─────────────────────────────────────────────────────────────────────────
   gallery.css
   Styles for gallery.html — the wedding photo/video archive browser.
   Inherits all CSS custom properties and font families from styles.css,
   which must be linked first.
   ───────────────────────────────────────────────────────────────────────── */

/* The subpage shell (body.subpage, .subpage-nav, .subpage-back/.subpage-link)
   is defined once in styles.css so /upload and /gallery share one header.
   Do not redefine it here — a second definition silently diverges. */

/* The dark page header moved into the shared .subpage-masthead in styles.css,
   which now also encloses the nav. The four rules that used to live here
   (.gallery-header, .gallery-pre-title, .gallery-header .gallery-title,
   .gallery-header .divider) are gone rather than kept "harmlessly": gallery.css
   loads AFTER styles.css, so at equal specificity they would have won —
   `background: var(--color-primary)` would paint over the masthead photograph,
   and `opacity: 0.6` would put the divider back at 2.24:1 against the lit
   lanterns, under the 3:1 floor. */

/* ── Controls band ─────────────────────────────────────────────────────────
   Full-bleed champagne band between the dark header and the light grid —
   the same treatment as the landing page's .details-panel (background-color
   + --texture-noise), scaled down: this is a working control strip, not a
   hero panel. It is a sibling of .gallery-header and .gallery-main in the
   markup (not nested in either), so it is full-bleed for free, the same way
   .gallery-header already is — no 100vw/negative-margin breakout needed.

   The old 1px .gallery-main-border seam (dark header -> light controls) is
   removed: the band now carries its own colour, so the dark -> champagne
   -> light transition reads as a band change on its own, exactly like the
   landing page's panels, which never use a seam line between bands. */
.gallery-controls-band {
  background-color: var(--color-champagne);
  background-image: var(--texture-noise);
}

/* styles.css caps .subpage-nav at 900px because that matches /upload's content
   column. The gallery's own columns are 1400px with 1.5rem gutters, so the
   shared cap left "← Home" indented ~180px from the champagne band and the
   photo grid below it at desktop widths — the one element on the page that
   didn't line up. gallery.css loads after styles.css, so this equal-specificity
   override wins on source order. */
/* padding-top is here for a second, unrelated reason: gallery.html is the only
   page that sets viewport-fit=cover (line 17), which extends the layout viewport
   up behind the status bar. styles.css gives .subpage-nav a flat 1.5rem = 24px
   of top padding, but env(safe-area-inset-top) is 44px on every Face ID iPhone
   in portrait — so "← Home", "↑ Upload" and the EN/ES toggle started 20px INSIDE
   the status bar and were neither visible nor tappable until the masthead
   scrolled away. max() keeps the 1.5rem everywhere the inset is 0. */
.subpage-nav {
  max-width: 1400px;
  padding-top: max(1.5rem, env(safe-area-inset-top));
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

/* styles.css tightens .subpage-nav's padding at 420px, but the override above
   has the same specificity and gallery.css loads second — so on this page the
   1.5rem was winning at every width. Re-state the narrow-screen padding here,
   after the base rule in the same file. */
@media (max-width: 420px) {
  .subpage-nav {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}

.gallery-controls-band-inner {
  max-width: 1400px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.gallery-main {
  max-width: 1400px;
  margin: 0 auto;
  padding: 1.5rem 1.5rem 4rem;
}

/* Upload nudge — a quiet single-line strip, not a slab. Flex-nowrap plus
   ellipsis on the text keeps it from wrapping even at a 390px viewport.
   The fill is the same --color-champagne-at-22% tint the strip always used;
   on the new champagne band that reads as a faint emboss rather than a card,
   so the border carries the definition here (--color-accent, not the
   --color-champagne border this used pre-band, which would now be
   invisible — border and background the same colour as their surroundings). */
.gallery-nudge {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  /* The text wraps to a second line on narrow screens (see
     .gallery-nudge-text); keep the row's own items on one line. */
  gap: 0.75rem;
  /* color-mix() is Safari 16.2+ / Chrome 111+. Guests arrive on whatever phone
     they own, so the literal comes first as the fallback declaration and the
     tokenised version overrides it wherever it parses. The literal is
     --color-champagne's own channels, not an approximation. */
  background: rgba(212, 196, 168, 0.22);
  background: color-mix(in srgb, var(--color-champagne) 22%, transparent);
  border: 1px solid var(--color-accent);
  padding: 0.6rem 0.5rem 0.6rem 1rem;
  font-family: var(--font-body);
  font-size: 0.8rem;
  color: var(--color-primary);
}

.gallery-nudge-text {
  flex: 1 1 auto;
  min-width: 0;
  /* This used to be nowrap + ellipsis, which at 390px rendered
     "Were you there? Add your pho…" — the invitation to upload, the one thing
     this banner exists to say, was the part cut off. Let it wrap. */
  overflow-wrap: break-word;
}

/* Underlined, so the link affordance survives on colour alone (SC 1.4.1) —
   which matters here because --color-accent, this link's colour pre-band,
   is only 3.27:1 on --color-champagne (fails the 4.5:1 text minimum the band
   now requires). --color-muted-deep clears it at 5.75:1. */
.gallery-nudge-link {
  flex-shrink: 0;
  color: var(--color-muted-deep);
  text-decoration: underline;
  font-weight: 500;
  white-space: nowrap;
  /* Measured 47x20 at 320px — a thumb-sized miss on the one control that
     turns a browsing guest into an uploading one. The negative margin keeps
     the banner the same height while the hit area grows to 44px. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  /* min-width too: with padding alone the English label "Upload" measured
     42x44 — the height was fixed but the width still missed by 2px. */
  min-width: 44px;
  padding-inline: 0.25rem;
  margin-block: -0.75rem;
}

.gallery-nudge-link:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.gallery-nudge-dismiss {
  flex-shrink: 0;
  margin-left: 0.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 1.35rem;
  line-height: 1;
  /* --color-muted fails AA on the champagne band (3.11:1); -deep clears it. */
  color: var(--color-muted-deep);
  min-width: 44px;
  min-height: 44px;
  padding: 0;
}

.gallery-nudge-dismiss:hover { color: var(--color-primary); }
.gallery-nudge-dismiss:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* "My photos" pill — an opaque dark-primary fill, so it reads the same
   regardless of the band colour underneath it. */
.gallery-mine-pill {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.5rem 1.25rem;
  min-height: 44px;
  background: var(--color-primary);
  color: white;
  border: 2px solid var(--color-primary);
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
  border-radius: 4px;
}

.gallery-mine-pill:hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
}

.gallery-mine-pill:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
}

.gallery-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: flex-end;
}

.gallery-control-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* Echoes .pre-title's treatment (thin weight, wide uppercase tracking) so the
   control labels read as the same interface-text register as the rest of the
   site instead of a cramped admin-panel caption. */
.gallery-control-label {
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 300;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--color-muted-deep);
}

.gallery-select {
  font-family: var(--font-body);
  /* H2: 1rem (16px) prevents iOS Safari auto-zoom on focus (threshold is <16px) */
  font-size: 1rem;
  color: var(--color-primary);
  background: white;
  border: 2px solid var(--color-accent);
  padding: 0.6rem 2.1rem 0.6rem 0.9rem;
  min-height: 44px;
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236b6b6b'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  transition: border-color 0.2s;
  border-radius: 4px;
}

/* --color-secondary (gold) was the hover/focus border pre-band; on the new
   champagne band it measures 1.30:1 against the background — effectively
   invisible. --color-primary gives a crisp, unambiguous "activated" edge
   regardless of what the control sits on. */
.gallery-select:hover,
.gallery-select:focus { border-color: var(--color-primary); outline: none; }

.gallery-select:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Type-filter pills */
.gallery-type-filter {
  border: none;
  padding: 0;
  margin: 0;
  display: flex;
  gap: 0;
}

/* Resting border/text was --color-champagne / --color-accent, tuned for the
   old light background. On the champagne band --color-champagne is invisible
   (border == background) and --color-accent text drops to 3.27:1, under the
   4.5:1 text minimum. --color-muted-deep (5.75:1) carries the label; the
   border switches to --color-accent, which still clears the 3:1 non-text
   minimum (3.27:1) so the resting pill keeps a visible edge. */
.gallery-type-pill {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.5rem 1rem;
  min-height: 44px;
  min-width: 44px;
  background: transparent;
  border: 2px solid var(--color-accent);
  color: var(--color-muted-deep);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
  /* Collapse adjacent borders */
  margin-left: -2px;
  border-radius: 4px;
}

.gallery-type-pill:first-child { margin-left: 0; }

/* --color-secondary was the hover colour pre-band; 1.30:1 on champagne makes
   it unusable for both border and text here. --color-primary is the same
   colour the active/pressed state already fills with, so hover now reads as
   a step toward that state instead of an unrelated colour. */
.gallery-type-pill:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
  z-index: 1;
  position: relative;
}

.gallery-type-pill--active,
.gallery-type-pill[aria-pressed="true"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: white;
  z-index: 2;
  position: relative;
}

.gallery-type-pill:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  z-index: 3;
  position: relative;
}

/* ── Result count ──────────────────────────────────────────────────────── */

.gallery-count {
  font-size: 0.8rem;
  /* --color-muted fails AA on the champagne band (3.11:1); -deep clears it. */
  color: var(--color-muted-deep);
  letter-spacing: 0.04em;
  min-height: 1.2em;
}

/* ── Thumbnail grid ────────────────────────────────────────────────────── */

/* Justified rows — the Google Photos / Flickr model.

   Every tile is a flex item with `flex-grow: var(--ar)` and
   `flex-basis: calc(var(--ar) * var(--gallery-row-h))`. Because BOTH the basis
   and the growth factor are proportional to the aspect ratio, the tiles on a
   line resolve to widths of `ar_i * (W - gaps) / Σar` — so every tile in that
   line lands on exactly the SAME height and the line fills the width exactly.
   No dead space, no ragged bottoms, and DOM order is untouched, so the
   chronological sort still reads left-to-right, top-to-bottom. (A fixed column
   grid can't do this: it forces one width on every photo, so a portrait and a
   landscape in the same row differ in height by 100px or more.)

   --gallery-row-h is only a TARGET; the real row height floats around it as
   each line justifies. Keeping it in CSS rather than measuring in JS is what
   lets a phone rotation or a window resize re-justify the entire grid with no
   script, no ResizeObserver, and no layout-thrash pass. */
.gallery-grid {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  align-content: flex-start;
  gap: 0.25rem;
  --gallery-row-h: 215px;
}

@media (min-width: 1400px) {
  .gallery-grid { --gallery-row-h: 240px; }
}

/* Soaks up the slack on the last row of each date group and of the grid, so a
   short final row keeps its natural height instead of stretching edge to edge.
   Zero basis means it never forces a wrap; the huge grow factor means the real
   tiles beside it keep their basis. */
.gallery-row-filler {
  flex: 1000000 1 0;
  min-width: 0;
  height: 0;
}

/* ── Group headers ─────────────────────────────────────────────────────── */

.gallery-group-header {
  flex: 0 0 100%;
  width: 100%;
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--color-muted);
  padding: 1rem 0.25rem 0.5rem;
  border-top: 1px solid var(--color-champagne);
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  /* Historically this header held TWO spans (a date and an inferred phase) and
     the flex line had to wrap to keep them from tearing mid-phrase. Guest-chosen
     sections replaced that with a single span carrying the whole label, so the
     wrap is now only a safety net — but it costs nothing and keeps the rule
     correct if a second span ever comes back. */
  flex-wrap: wrap;
  position: sticky;
  top: 0;
  background: var(--color-light);
  z-index: 2;
}

.gallery-group-header > span {
  /* Must NOT be nowrap. That was correct when the header held two short spans;
     the single combined label ("AGOSTO 2025 · FOTOS DE COMPROMISO", uppercased
     at 0.15em tracking) is 322px wide and overflowed the 288px content box at
     320px viewports — the page scrolled sideways. Measured on a real 320x568
     iPhone context in both languages; Spanish is the wider of the two. */
  white-space: normal;
  overflow-wrap: break-word;
}

.gallery-group-header:first-child {
  border-top: none;
}

/* No opacity here: --color-muted is 5.06:1 on --color-light at full strength,
   but 0.75 opacity composited it to #8f8f8e and dropped it to 3.09:1. The
   lighter weight alone carries the visual de-emphasis. */
.gallery-group-phase {
  font-weight: 300;
}

/* ── Gallery tile wrapper (grid cell) ──────────────────────────────────── */

/* One wrapper per photo; --ar is set inline by gallery.js from the stored
   width/height. flex-shrink stays at 1 so a very wide panorama whose basis
   exceeds a narrow phone's content width still fits instead of overflowing. */
.gallery-tile-wrap {
  flex-grow: var(--ar, 1);
  flex-shrink: 1;
  flex-basis: calc(var(--ar, 1) * var(--gallery-row-h));
  min-width: 0;
}

/* ── Gallery tile ──────────────────────────────────────────────────────── */

.gallery-tile {
  position: relative;
  aspect-ratio: var(--ar, 1);
  overflow: hidden;
  background: var(--color-champagne);
  border: none;
  padding: 0;
  cursor: pointer;
  display: block;
  width: 100%;
  outline: none;
}

.gallery-tile:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: -3px;
  /* The ring lands on an unpredictable photo, so a single colour can always be
     swallowed by the image. The inset dark halo guarantees one of the two
     edges separates whatever the thumbnail happens to be. */
  box-shadow: inset 0 0 0 6px rgba(26, 26, 26, 0.55);
  z-index: 1;
}

.gallery-tile-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.3s ease;
}

.gallery-tile:hover .gallery-tile-img,
.gallery-tile:focus-visible .gallery-tile-img {
  transform: scale(1.04);
}

@media (prefers-reduced-motion: reduce) {
  .gallery-tile:hover .gallery-tile-img,
  .gallery-tile:focus-visible .gallery-tile-img {
    transform: none;
  }
}

/* Placeholder when thumbnailDataUrl is null */
.gallery-tile-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.5rem;
  /* A bare glyph on flat champagne read as a rendering failure rather than a
     deliberate state — and now that the controls band above is champagne too,
     an empty champagne square looked like a tile that simply never painted.
     The inset dashed rule and the caption make it legible as "we know, this
     one has no preview". */
  text-align: center;
  color: var(--color-muted-deep);
  background: var(--color-champagne);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}

.gallery-tile-placeholder-glyph {
  font-size: 1.6rem;
  line-height: 1;
  opacity: 0.7;
}

.gallery-tile-placeholder-label {
  font-family: var(--font-body);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
}

/* Video badge (play triangle in bottom-left). Font-size raised from 0.6rem
   (9.6px, below the site's 0.68rem floor) to 0.7rem; padding/gap trimmed
   slightly so the larger text still fits the small corner badge. */
.gallery-tile-badge--video {
  position: absolute;
  bottom: 0.4rem;
  left: 0.4rem;
  background: rgba(0, 0, 0, 0.55);
  color: white;
  font-size: 0.7rem;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.35rem;
  border-radius: 4px;
  pointer-events: none;
  font-family: var(--font-body);
  font-weight: 400;
}

.gallery-tile-badge-play {
  padding-left: 0.1em; /* optical centering */
}

/* ── Bulk selection ────────────────────────────────────────────────────── */

/* Selection badge — built into every tile, but only shown while the body
   carries .gallery-selecting. pointer-events:none keeps the whole tile (not
   this small corner) as the tap target. Its own opaque-ish dark ground plus
   a white ring is legible whether it lands on a bright sky or a dark suit. */
.gallery-tile-select-badge {
  display: none;
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  background: rgba(26, 26, 26, 0.6);
  border: 2px solid rgba(255, 255, 255, 0.9);
  pointer-events: none;
  z-index: 2;
  transition: background 0.15s, border-color 0.15s;
}

.gallery-selecting .gallery-tile-select-badge {
  display: flex;
}

.gallery-tile-select-badge::after {
  content: '';
  width: 11px;
  height: 6px;
  border-left: 2px solid white;
  border-bottom: 2px solid white;
  transform: rotate(-45deg) translate(1px, -1px);
  opacity: 0;
  transition: opacity 0.15s;
}

.gallery-tile[aria-pressed="true"] .gallery-tile-select-badge {
  background: var(--color-sage);
  border-color: var(--color-sage);
}

.gallery-tile[aria-pressed="true"] .gallery-tile-select-badge::after {
  opacity: 1;
}

/* Selected-tile ring + tint. The tint sits in its own absolutely-positioned
   pseudo-element (not just a box-shadow) so it composites over the thumbnail
   image regardless of the image's own stacking — a static <img> always
   paints below a positioned sibling/pseudo in the same tile. */
.gallery-tile[aria-pressed="true"] {
  box-shadow: inset 0 0 0 4px var(--color-sage);
}

.gallery-tile[aria-pressed="true"]::before {
  content: '';
  position: absolute;
  inset: 0;
  /* Same color-mix() fallback as .gallery-nudge — and it matters more here:
     this tint IS the "selected" affordance, so on a browser without
     color-mix() an unguarded declaration would leave selection invisible.
     Literal is --color-sage's own channels. */
  background: rgba(58, 90, 64, 0.2);
  background: color-mix(in srgb, var(--color-sage) 20%, transparent);
  pointer-events: none;
  z-index: 1;
}

.gallery-selecting .gallery-tile {
  cursor: pointer;
}

/* ── Bulk-selection toolbar ────────────────────────────────────────────── */

/* Fixed to the viewport bottom. No fixed backdrop exists behind it — it would
   scroll over the dark header, the light controls, and photo tiles — so a
   translucent fill can never be contrast-safe here. It gets its own fully
   opaque background instead (--color-primary, the same dark ground the item
   view and item-bar already use), which is what keeps every label on it
   legible no matter what is happening underneath. gallery.js adds matching
   bottom padding to .gallery-main while this is open so it never covers the
   Load-more button or the footer. */
.gallery-select-toolbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: var(--color-primary);
  color: white;
  box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.25);
  padding:
    0.75rem
    max(1rem, env(safe-area-inset-right))
    max(0.75rem, env(safe-area-inset-bottom))
    max(1rem, env(safe-area-inset-left));
}

.gallery-select-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  max-width: 1400px;
  margin: 0 auto;
}

.gallery-select-count {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: white;
}

.gallery-select-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* Buttons wrap onto extra lines rather than forcing horizontal scroll or
   pushing the page wide at a 320px viewport. */
.gallery-select-btn {
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  padding: 0.5rem 0.9rem;
  min-height: 44px;
  min-width: 44px;
  background: transparent;
  border: 2px solid rgba(255, 255, 255, 0.35);
  color: white;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}

.gallery-select-btn:hover {
  border-color: rgba(255, 255, 255, 0.7);
}

.gallery-select-btn:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.gallery-select-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.gallery-select-btn--accent {
  background: var(--color-sage);
  border-color: var(--color-sage);
}

.gallery-select-btn--accent:hover:not(:disabled) {
  background: var(--color-sage-light);
  border-color: var(--color-sage-light);
  color: var(--color-primary);
}

.gallery-select-btn--done {
  background: var(--color-secondary);
  border-color: var(--color-secondary);
  color: var(--color-primary);
}

.gallery-select-status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  max-width: 1400px;
  margin: 0 auto;
  font-family: var(--font-body);
  font-size: 0.8rem;
  color: white;
}

.gallery-select-message {
  max-width: 1400px;
  margin: 0.4rem auto 0;
  font-family: var(--font-body);
  font-size: 0.78rem;
  color: var(--color-champagne);
}

/* Uploader name overlay */
.gallery-tile-overlay {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 2rem 0.5rem 0.45rem;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.35) 45%, transparent 100%);
  font-family: var(--font-body);
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.2s, transform 0.2s;
  pointer-events: none;
}

.gallery-tile:hover .gallery-tile-overlay,
.gallery-tile:focus-visible .gallery-tile-overlay {
  opacity: 1;
  transform: translateY(0);
}

/* On touch devices there is no hover — always show the overlay */
@media (hover: none) {
  .gallery-tile-overlay {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .gallery-tile-overlay {
    transition: none;
  }
}

/* Caption + uploader inside the overlay. Both clamp to one ellipsised line so
   a long caption can never change the tile's height and break the row. */
.gallery-tile-cap,
.gallery-tile-by {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

.gallery-tile-cap {
  font-size: 0.72rem;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.96);
}

/* Raised from 0.6rem (9.6px), below the site's smallest type. */
.gallery-tile-by {
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.72);
}

/* Loading skeleton tiles */
.gallery-tile-skeleton {
  background: var(--color-champagne);
  flex: 1 1 var(--gallery-row-h);
  aspect-ratio: 1 / 1;
  position: relative;
  overflow: hidden;
}

.gallery-tile-skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255,255,255,0.4) 50%,
    transparent 100%
  );
  animation: gallery-shimmer 1.2s infinite;
  transform: translateX(-100%);
}

@keyframes gallery-shimmer {
  to { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
  .gallery-tile-skeleton::after {
    animation: none;
    background: none;
  }
}

/* Narrow viewports get a uniform grid instead of justified rows.

   Justification needs enough width to choose from. Measured against the real
   corpus, the height swing within a page (tallest row / shortest row) runs
   2.1x at a 600px viewport and only 1.6x at 1024px, and the number of rows
   left holding a single stretched photo collapses from many to one over the
   same span. Below ~900px there simply isn't room: at 351px of content the row
   capacity is ~2 aspect-units, so one landscape (1.78) leaves nowhere for a
   second tile and stretches to fill the row alone. Every target height tried
   there either produced 30-of-40 single-tile rows swinging 160px->351px, or
   packed 2-3 tiles at a cramped ~112px — aspect ratios quantise too coarsely
   at that width for anything in between.

   So below 900px the tiles drop to a fixed square grid — three-up on small
   tablets, two-up on phones at a generous ~173px a side. Perfectly even, edge
   to edge, and still zero dead space. The true aspect ratio is never lost: the
   item view always shows the photo uncropped. This is the same split Google
   Photos makes, and for the same reason. */
@media (max-width: 899px) {
  .gallery-tile-wrap {
    flex: 0 1 calc(33.3333% - 0.1667rem);
  }
  .gallery-tile {
    aspect-ratio: 1;
  }
}

@media (max-width: 599px) {
  .gallery-tile-wrap { flex-basis: calc(50% - 0.125rem); }
}

@media (max-width: 359px) {
  .gallery-grid { gap: 0.15rem; }
  .gallery-tile-wrap { flex-basis: calc(50% - 0.075rem); }
}

/* ── Empty / error states ──────────────────────────────────────────────── */

.gallery-empty,
.gallery-filter-empty,
.gallery-error-state {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 40vh;
  text-align: center;
  padding: 4rem 2rem;
}

.gallery-empty-inner {
  max-width: 400px;
}

.gallery-empty-icon {
  font-size: 2rem;
  color: var(--color-champagne);
  margin-bottom: 1.5rem;
  font-family: var(--font-display);
}

.gallery-empty-msg {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 300;
  color: var(--color-primary);
  line-height: 1.5;
  margin-bottom: 2rem;
}

.gallery-empty-cta {
  display: inline-block;
  padding: 0.9rem 2.5rem;
  background: var(--color-sage);
  color: white;
  text-decoration: none;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  transition: background 0.2s, color 0.2s;
  border: 2px solid var(--color-sage);
  cursor: pointer;
  min-height: 44px;
}

.gallery-empty-cta:hover {
  color: var(--color-champagne);
  text-shadow: 0 0 10px rgba(212, 196, 168, 0.8), 0 0 20px rgba(212, 196, 168, 0.5);
}

.gallery-empty-cta:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
}

/* ── Loading indicator ─────────────────────────────────────────────────── */

.gallery-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 2.5rem;
  color: var(--color-muted);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
}

.gallery-loading-spinner {
  width: 20px;
  height: 20px;
  border: 2px solid var(--color-champagne);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: gallery-spin 0.7s linear infinite;
  flex-shrink: 0;
}

@keyframes gallery-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .gallery-loading-spinner { animation: none; }
}

/* ── Load more button ──────────────────────────────────────────────────── */

.gallery-load-more-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  margin-top: 2.5rem;
}

.gallery-load-more {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  padding: 0.75rem 2.5rem;
  min-height: 44px;
  background: var(--color-primary);
  border: 2px solid var(--color-primary);
  color: white;
  cursor: pointer;
  transition: all 0.3s ease;
}

.gallery-load-more:hover {
  background: var(--color-primary);
  color: var(--color-champagne);
  text-shadow: 0 0 10px rgba(212, 196, 168, 0.8), 0 0 20px rgba(212, 196, 168, 0.5);
}

.gallery-load-more:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
}

.gallery-load-more:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.gallery-load-more-error {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.8rem;
  color: var(--color-muted);
}

.gallery-load-more-retry {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent);
  background: none;
  border: 2px solid var(--color-champagne);
  padding: 0.35rem 0.75rem;
  min-height: 44px;
  cursor: pointer;
}

.gallery-load-more-retry:hover { color: var(--color-secondary); border-color: var(--color-secondary); }

.gallery-load-more-retry:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* ── Item view ─────────────────────────────────────────────────────────── */

.item-view {
  min-height: 100dvh;
  background: var(--color-primary);
  display: flex;
  flex-direction: column;
  touch-action: pan-y;
  color: rgba(255, 255, 255, 0.92);
}

/* On wide screens, arrange stage + meta side by side */
@media (min-width: 900px) {
  .item-view {
    display: grid;
    grid-template-rows: auto 1fr;
    grid-template-columns: 1fr 20rem;
  }
  .item-bar  { grid-column: 1 / -1; }
  .item-stage { grid-column: 1; grid-row: 2; overflow: hidden; }
  .item-meta { grid-column: 2; grid-row: 2; overflow-y: auto; }
}

/* Item bar */
.item-bar {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgba(26, 26, 26, 0.9);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  gap: 0.5rem 1.5rem;
  /* The lang-toggle is genuinely hidden while this view is open (styles.css now
     carries [hidden]{display:none!important}, without which its display:flex
     outranked the UA rule and left it covering the Share button). Reserving a
     6.5rem gutter for it here wrapped Share/Download onto a second row and cost
     120px of vertical space above the photo on a phone. */
  padding: max(0.75rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) 0.75rem max(1rem, env(safe-area-inset-left));
  flex-wrap: wrap;
}

.item-back {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-champagne);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-height: 44px;
  padding: 0 0.5rem;
  transition: color 0.15s;
}

/* Below 560px the bar cannot fit "BACK TO GALLERY" (longer still in Spanish),
   "4 of 46" and both actions on one line — measured at 466px of content in a
   390px viewport, which wrapped the actions onto a second row and cost 120px of
   vertical space above the photo. Drop to the arrow alone; the anchor carries a
   translated aria-label, and a lone back-arrow is the standard affordance in
   every phone photo viewer. */
@media (max-width: 560px) {
  .item-back-label { display: none; }
  .item-back { min-width: 44px; justify-content: center; }
  .item-back-arrow { font-size: 1.25rem; line-height: 1; }
}

.item-back:hover { color: white; }
.item-back:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.item-position {
  font-family: var(--font-body);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  /* 0.4 alpha composited to #767676 on the item view's --color-primary ground:
     3.81:1, under AA. 0.62 keeps it clearly secondary at 7.9:1. */
  color: rgba(255, 255, 255, 0.62);
}

.item-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.item-share,
.item-download {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-champagne);
  background: none;
  border: none;
  text-decoration: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.5rem;
  transition: color 0.15s;
  white-space: nowrap;
}

.item-share:hover, .item-download:hover { color: white; }

.item-share:focus-visible,
.item-download:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Item stage — contains the media and prev/next */
.item-stage {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  min-height: 200px;
}

.item-media-wrap {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-height: calc(100dvh - 9rem);
  overflow: hidden;
}

@media (min-width: 900px) {
  .item-media-wrap {
    max-height: calc(100dvh - 5rem);
  }
}

/* Blur-up background */
/* Derivative not ready: the blur-up backdrop is the only thing in the wrap,
   and it is out of flow, so without this the wrap is 0px tall and the
   "showing a preview" note sits over nothing. --item-aspect is set from the
   document's stored width/height in gallery.js. */
.item-media-wrap.is-preview-only {
  aspect-ratio: var(--item-aspect, 4 / 3);
  max-width: 100%;
}

.item-blur-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  filter: blur(12px);
  transform: scale(1.04);
  transition: opacity 0.3s;
}

@media (prefers-reduced-motion: reduce) {
  .item-blur-bg { transition: none; }
}

.item-media {
  position: relative;
  max-width: 100%;
  max-height: calc(100dvh - 9rem);
  object-fit: contain;
  display: block;
  transition: opacity 0.3s;
}

@media (min-width: 900px) {
  .item-media {
    max-height: calc(100dvh - 5rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .item-media { transition: none; }
}

/* Prev / Next buttons */
.item-prev,
.item-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  min-width: 44px;
  min-height: 44px;
  background: rgba(0, 0, 0, 0.35);
  border: none;
  color: rgba(255, 255, 255, 0.8);
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  transition: background 0.15s, color 0.15s;
  border-radius: 4px;
}

.item-prev {
  left: max(0.5rem, env(safe-area-inset-left));
}

.item-next {
  right: max(0.5rem, env(safe-area-inset-right));
}

.item-prev:hover,
.item-next:hover {
  background: rgba(0, 0, 0, 0.6);
  color: white;
}

.item-prev:focus-visible,
.item-next:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Item meta footer */
.item-meta {
  padding: 1.25rem 1.25rem max(1.25rem, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

@media (min-width: 900px) {
  .item-meta {
    padding: 2rem 1.5rem;
    border-left: 1px solid rgba(255, 255, 255, 0.08);
    justify-content: center;
  }
}

.item-caption-text {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 300;
  color: rgba(255, 255, 255, 0.92);
  line-height: 1.55;
  max-height: 6.5rem;
  overflow-y: auto;
}

.item-caption-meta {
  font-family: var(--font-body);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  color: rgba(255, 255, 255, 0.5);
  text-transform: uppercase;
}

.item-processing-note {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-style: italic;
  /* was 0.45 -> #818181 -> 4.47:1, a hair under AA */
  color: rgba(255, 255, 255, 0.62);
}

.item-error-state {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.5);
}

.item-error-retry {
  font-family: var(--font-body);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-champagne);
  background: none;
  border: 1px solid rgba(255, 255, 255, 0.2);
  padding: 0.35rem 0.75rem;
  min-height: 44px;
  cursor: pointer;
  transition: border-color 0.15s;
}

.item-error-retry:hover { border-color: rgba(255, 255, 255, 0.4); }
.item-error-retry:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Item-view next-loading inline spinner */
.item-next-loading {
  width: 18px;
  height: 18px;
  border: 2px solid rgba(255, 255, 255, 0.15);
  border-top-color: var(--color-champagne);
  border-radius: 50%;
  animation: gallery-spin 0.7s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .item-next-loading { animation: none; }
}

/* ── Screen-reader only ────────────────────────────────────────────────── */
/* (gallery.css defines its own in case styles.css doesn't — harmless duplicate) */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Skip link — visible on focus */
.gallery-skip-link:focus {
  position: fixed;
  top: 0.5rem;
  left: 50%;
  transform: translateX(-50%);
  width: auto;
  height: auto;
  clip: auto;
  clip-path: none;
  margin: 0;
  z-index: 9999;
  background: var(--color-primary);
  color: white;
  padding: 0.5rem 1rem;
  font-family: var(--font-body);
  font-size: 0.85rem;
  white-space: nowrap;
}

/* ── Responsive adjustments ────────────────────────────────────────────── */

@media (max-width: 480px) {
  .gallery-main {
    padding: 1.5rem 0.75rem 3rem;
  }

  .gallery-controls-band-inner {
    padding: 1.5rem 0.75rem;
    gap: 0.75rem;
  }

  .gallery-controls {
    gap: 0.75rem 1.25rem;
  }

  .gallery-empty-msg {
    font-size: 1.2rem;
  }
}

/* ── Compact controls on phones ────────────────────────────────────────────
   Stacked one-per-row, Sort / From / Type / Select filled 440px of an 844px
   iPhone screen before a single photograph appeared. Two columns for the two
   selects halves that; Type keeps a full row because the three pills need
   more than half the width, and Select follows on its own. Placed after the
   480px block above deliberately — at 390px both queries match, and an
   equal-specificity rule only wins if it comes later in source order. */
@media (max-width: 560px) {
  /* One column, not two. A half-width select here truncates its own selected
     value, which is the one string that has to stay readable: at 390px the
     two-column select measured 170px against the 207px that "Latest moment
     first" needs (154-159px of text plus 48px of the control's own padding),
     and the Spanish labels are longer still. Full width gives 352px at 390px
     and 282px at 320px, so every option reads in both languages. */
  .gallery-controls {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.85rem;
    align-items: end;
  }

  /* Grid items don't shrink below their content width without this. */
  .gallery-control-group { min-width: 0; }
  .gallery-select { width: 100%; }

  /* Not stretched: the pill row and the SELECT button size to their content. */
  /* The button is wrapped in .gallery-control-group--select now, so the old
     direct-child selector on #gallery-select-mode no longer matches. */
  .gallery-controls > .gallery-control-group--select { justify-self: stretch; }
  .gallery-select-mode-btn { width: 100%; }
}

/* ── Bulk-select trigger ──────────────────────────────────────────────────────
   Was styled as a filter pill and sat among the filter pills, which is exactly
   why guests did not recognise it as the way to download several files. Three
   things separate it now: its own wrapper (pushed to the trailing edge of the
   controls row on desktop), a FILLED sage ground rather than the pills' hairline
   border, and an explicit two-word label instead of "Select".

   --color-sage is the site's action colour, already used by .calendar-btn and
   the load-more button, so this reads as an action on first glance. */
.gallery-control-group--select {
  margin-left: auto;
  align-self: flex-end;
}

.gallery-select-mode-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0 1.25rem;
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #fff;
  background: var(--color-sage);
  border: 1px solid var(--color-sage);
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}

.gallery-select-icon {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
}

.gallery-select-mode-btn:hover {
  background: var(--color-primary);
  border-color: var(--color-primary);
}

.gallery-select-mode-btn:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* setSelecting() toggles .gallery-type-pill--active on this button (gallery.js).
   Matching the compound selector rather than renaming the class keeps that JS
   untouched — one less thing to get out of step. */
.gallery-select-mode-btn.gallery-type-pill--active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

.gallery-select-hint {
  margin: 0.75rem 0 0;
  font-family: var(--font-body);
  font-size: 0.8rem;
  line-height: 1.5;
  letter-spacing: 0.04em;
  /* On --color-champagne: --color-muted is 3.11:1, --color-muted-deep 5.75:1. */
  color: var(--color-muted-deep);
}

/* Selection mode is on and the selection toolbar is saying the same thing. */
body.gallery-selecting .gallery-select-hint {
  display: none;
}

@media (max-width: 560px) {
  .gallery-control-group--select {
    margin-left: 0;
  }
  .gallery-select-hint {
    margin-top: 0.5rem;
  }
}
