* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #f2f1ee;
  font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
}

#gallery {
  position: fixed;
  inset: 0;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

#gallery.dragging {
  cursor: grabbing;
}

#canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
}

.tile {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
  transform: translate3d(0, 0, 0);
}

.tile-inner {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #ddd;
}

.tile img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
  user-select: none;
  pointer-events: none;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.tile:hover img {
  transform: scale(1.06);
}

.tile-caption {
  position: absolute;
  left: 8px;
  bottom: 8px;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #fff;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.3s ease, transform 0.3s ease;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

.tile:hover .tile-caption {
  opacity: 1;
  transform: translateY(0);
}

/* Click-to-rotate photo stack -- one tile in the wallpaper shows a stack of
   extra images instead of the normal cycling set. Cards peek out past the
   front photo's edges, so tile-inner can't clip to bounds here the way it
   does for every other tile. */
.tile.stack .tile-inner {
  overflow: visible;
  perspective: 900px;
  /* Rotating .stack-3d foreshortens it, exposing tile-inner's own flat
     background (normally a #ddd loading-state fallback) around it. */
  background: none;
}

/* Cards are aligned on the same X/Y footprint (no 2D offset/rotation of
   their own) and separated only along Z -- tilting this whole group is what
   turns that depth stack into a visible 3D cascade. At rotateY(0) the back
   cards sit fully hidden behind the front one, so the tile reads as one
   ordinary flat photo until it's focused. script.js kicks it to the base
   tilt in enterFocus() and drives it continuously from there via the
   viewport-wide cursor-follow tilt, then drops it back to this flat resting
   state in exitFocus(). */
.stack-3d {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: rotateY(0deg);
  transition: transform 0.2s ease-out;
  will-change: transform;
}

/* Real upcoming photos in the stack, not solid color fills -- their
   background-image is set in script.js so a peek of the next couple frames
   is always visible behind the front one. */
.stack-card {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-color: #ddd;
  background-size: cover;
  background-position: center;
  box-shadow: 0 14px 28px rgba(0, 0, 0, 0.22);
}

.stack-card.back-1 {
  transform: translateZ(-18px);
}

.stack-card.back-2 {
  transform: translateZ(-36px);
}

.tile.stack img {
  transition: transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.42s ease;
  /* Each card in the stack casts its own soft shadow onto the one behind it. */
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.22);
}

.tile.stack:hover img {
  transform: none;
}

/* The front card recedes past back-2's resting depth (-36px) -- it's
   visually going to the back of the deck, not flying away and vanishing. */
.tile.stack img.stack-exit {
  transform: translateZ(-54px) translateY(20px) scale(0.9);
  opacity: 0;
}

/* Starts at back-1's resting depth (-18px), i.e. exactly where this photo
   was already sitting a moment ago, and animates forward from there --
   reads as "the next card in line steps up to the front". */
.tile.stack img.stack-enter {
  transition: none;
  transform: translateZ(-18px) scale(0.94);
  opacity: 0.6;
}

/* Fisheye video -- the <video> is only ever a decode source for the WebGL
   canvas (drawFisheyeFrame() in script.js copies its frames through the
   lens-bulge shader every frame), so it stays invisible in the DOM. */
.video-fisheye-source {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}

.video-fisheye-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.tile.video:hover .video-fisheye-canvas {
  transform: scale(1.06);
}

#hint {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #000;
  opacity: 0.5;
  pointer-events: none;
  transition: opacity 0.6s ease;
  z-index: 10;
}

#hint.hidden {
  opacity: 0;
}

#coords {
  position: fixed;
  top: 20px;
  left: 24px;
  font-size: 11px;
  letter-spacing: 0.05em;
  color: #000;
  opacity: 0.4;
  pointer-events: none;
  z-index: 10;
  font-variant-numeric: tabular-nums;
}
