/* gd-photo-tile: the loading ladder states (skeleton -> blur-up -> sharp)
   plus picked / marked / arrived. The blurred placeholder only ever
   transitions its own opacity: nothing animates beneath the blur. */
gd-photo-tile { display: block; position: relative; border-radius: var(--radius-photo); overflow: hidden;
  background: var(--photo-well); break-inside: avoid; margin-bottom: var(--gap-grid);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .35), inset 0 0 0 1px var(--accent-soft-border);
  opacity: 0; transform: translateY(12px);
  transition: opacity var(--dur-tile) var(--ease-out), transform var(--dur-tile) var(--ease-out),
    box-shadow .2s ease; cursor: pointer; }
gd-photo-tile.show { opacity: 1; transform: none; }
gd-photo-tile.picked { box-shadow: 0 0 0 2px var(--accent); }

gd-photo-tile .shimmer { position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%, var(--shimmer-wash) 50%, transparent 70%);
  background-size: 220% 100%; animation: gd-shimmer 1.8s ease-in-out infinite; }
@keyframes gd-shimmer { from { background-position: 130% 0; } to { background-position: -90% 0; } }

/* The reserved box. --layout-aspect (set by a packer that needs a specific
   aspect) wins over --photo-aspect (the tile's own, from the listing); the
   fallback is 2:3 portrait, matching the skeleton before dimensions land.
   ONE number sizes the box, and in the grid it is the same number that
   sizes the flex width, so a row cannot resolve to two heights. */
gd-photo-tile .frame { position: relative; width: 100%;
  padding-bottom: calc(100% / var(--layout-aspect, var(--photo-aspect, 0.6667))); }
gd-photo-tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* Only when the packer asked for an aspect the photo does NOT have: fit the
   whole photo inside the cell instead of filling it. These are photos OF THE
   GUEST — a crop can cut someone out of their own group shot, which is the
   reason justified rows were chosen over a fixed-ratio grid. Letterboxing a
   rare panorama keeps that promise; cover would quietly break it. */
gd-photo-tile.fitted img { object-fit: contain; }
gd-photo-tile img.ph { filter: blur(var(--blur-placeholder)) saturate(1.15); transform: scale(1.12); opacity: 0;
  transition: opacity var(--dur-quick) ease; }
gd-photo-tile img.ph.on { opacity: 1; }
gd-photo-tile img.full { opacity: 0; transform: scale(1.03);
  transition: opacity var(--dur-reveal) ease, transform var(--dur-settle) var(--ease-out); }
gd-photo-tile img.full.on { opacity: 1; transform: scale(1); }

gd-photo-tile .flag { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: var(--ink-3); font: var(--type-fine)/1.4 var(--font-ui); text-align: center; padding: var(--space-xs); }

gd-photo-tile.arrived { animation: gd-pulse var(--dur-pulse) ease; }
@keyframes gd-pulse { 0% { box-shadow: 0 0 0 0 var(--pulse-ring); } 100% { box-shadow: 0 0 0 14px transparent; } }

/* A saved photo is DONE, and done things get out of the way. The state lives
   in the photo itself, not in a badge: desaturated and dimmed, it drops out of
   the scan so the ones still to save are what the eye lands on. A tick alone
   could not do this -- on a wall of neon it was a speck the operator had to
   hunt for, which is the opposite of a status. The filter transitions, so
   saving a photo visibly settles it. */
gd-photo-tile.saved .frame { filter: saturate(.18) brightness(.5) contrast(.95);
  transition: filter var(--dur-reveal, .35s) var(--ease-out, ease-out); }
gd-photo-tile.saved { box-shadow: 0 1px 4px rgba(0, 0, 0, .35), inset 0 0 0 1px var(--accent-soft-border); }

/* The label names the state the colour is already showing. Gold on a drained
   photo, so it is the one thing left with any saturation in it. */
gd-photo-tile .savedmark { position: absolute; z-index: 2; bottom: var(--space-xs); left: var(--space-xs);
  display: inline-flex; align-items: center; gap: 5px; padding: 5px 9px 5px 7px;
  border-radius: var(--radius-pill); background: var(--glass-strong, rgba(0, 0, 0, .72));
  border: 1px solid var(--accent-soft-border); color: var(--accent);
  font: 700 var(--type-note)/1 var(--font-ui); letter-spacing: .02em;
  backdrop-filter: blur(var(--blur-glass, 8px)); }
gd-photo-tile .savedmark i { font-style: normal; font-size: 1.05em; line-height: 1; }

@media (prefers-reduced-motion: reduce) {
  gd-photo-tile.saved .frame { transition: none; }
}

gd-photo-tile .pickbadge { position: absolute; z-index: 2; top: var(--space-xs); right: var(--space-xs);
  min-width: 26px; height: 26px; border-radius: 50%; background: var(--accent); color: var(--accent-ink);
  font: 700 var(--type-note)/26px var(--font-ui); text-align: center; padding: 0 var(--space-2xs);
  box-shadow: var(--shadow-badge); }

@media (prefers-reduced-motion: reduce) {
  gd-photo-tile { transition-duration: 1ms; }
  gd-photo-tile .shimmer { animation: none; opacity: .4; }
  gd-photo-tile img.ph, gd-photo-tile img.full { transition-duration: 1ms; }
  gd-photo-tile.arrived { animation: none; }
}
