/* ---------- Fonts (self-hosted, SIL Open Font License; see vendor/*-LICENSE.txt) ---------- */

@font-face {
  font-family: "Geist";
  src: url("vendor/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("vendor/GeistMono-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
}

/* Pixel font for the wordmark and small labels, matching the pixel "i" logo. It is drawn on an
   11 px grid, so use it at 11 px or multiples of 5.5 px. */
@font-face {
  font-family: "Departure Mono";
  src: url("vendor/DepartureMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

/* ---------- Tokens ---------- */

:root {
  --bg: #e9f0ee;
  --bg-glow: rgba(25, 68, 138, 0.08);
  --surface: rgba(255, 255, 255, 0.72);
  --card: #ffffff;
  --card-hover: #fbfbfa;
  --stage: #dfe7e4;
  --text: #17171a;
  --muted: #55555d;
  --faint: #9d9da6;
  --line: rgba(23, 23, 26, 0.08);
  --line-strong: rgba(23, 23, 26, 0.16);
  --accent: #19448a;
  --accent-soft: rgba(25, 68, 138, 0.08);
  --accent-line: rgba(25, 68, 138, 0.35);
  --grid: rgba(25, 68, 138, 0.07);
  --ticker: rgba(25, 68, 138, 0.2);
  --danger: #9a3412;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 10px 30px -12px rgba(0, 0, 0, 0.12);
  --shadow-photo: 0 2px 6px rgba(0, 0, 0, 0.06), 0 30px 60px -24px rgba(0, 0, 0, 0.35);

  /* Square corners, like the logo. */
  --radius: 0;
  --radius-sm: 0;
  --topbar-h: 64px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  --font: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: var(--font);
  --mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --pixel: "Departure Mono", var(--mono);

  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0b0b0d;
    --bg-glow: rgba(137, 173, 230, 0.10);
    --surface: rgba(18, 18, 21, 0.72);
    --card: rgba(255, 255, 255, 0.035);
    --card-hover: rgba(255, 255, 255, 0.055);
    --stage: #070708;
    --text: #f3f3f5;
    --muted: #a3a3ad;
    --faint: #6f6f79;
    --line: rgba(255, 255, 255, 0.07);
    --line-strong: rgba(255, 255, 255, 0.14);
    --accent: #89ade6;
    --accent-soft: rgba(137, 173, 230, 0.13);
    --accent-line: rgba(137, 173, 230, 0.45);
    --grid: rgba(137, 173, 230, 0.06);
    --ticker: rgba(137, 173, 230, 0.17);
    --danger: #fb923c;
    --shadow-sm: none;
    --shadow: 0 0 0 1px rgba(255, 255, 255, 0.02), 0 10px 30px -12px rgba(0, 0, 0, 0.6);
    --shadow-photo: 0 30px 70px -20px rgba(0, 0, 0, 0.9);
  }
}

/* ---------- Base ---------- */

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; font-synthesis: none; }

body {
  margin: 0;
  min-height: 100dvh;
  display: grid;
  grid-template-rows: var(--topbar-h) 1fr;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  background:
    radial-gradient(1200px 600px at 50% -200px, var(--bg-glow), transparent 70%),
    var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

[hidden] { display: none !important; }

button { font: inherit; color: inherit; }

svg { display: block; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ---------- Top bar ---------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 clamp(16px, 3vw, 28px);
  background: var(--surface);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  border-bottom: 2px solid var(--accent);
}

.local-badge { view-transition-name: badge; }
#open-another { view-transition-name: new-photo; }

.brand {
  display: inline-flex;
  align-items: center;
  gap: 11.5px;
  color: var(--text);
  text-decoration: none;
  font-family: var(--pixel);
  font-weight: 400;
  font-size: 19px;
  letter-spacing: 0;
}

.brand-mark {
  width: 25.3px;
  height: 25.3px;
  color: var(--accent);
  overflow: visible;
}

/* On hover the dot of the "i" hops further out of its frame. Units are the logo's 120-unit grid. */
.brand-dot { transition: transform 0.45s var(--ease); }
.brand:hover .brand-dot { transform: translateY(-12px); }

.topbar-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* "Stays on your device" echoes the logo: a square frame with a gap in its top edge and a pixel dot
   breaking out of it, above a pixel lock. The frame is drawn with background gradients so the gap
   stays see-through over the blurred top bar. --gap-l and --gap-w place the gap over the lock. */
.local-badge {
  --b: 2px;
  --gap-l: 9px;
  --gap-w: 12px;
  --edge: linear-gradient(var(--accent) 0 0);
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  box-sizing: border-box;
  height: 30px;
  margin: 0;
  padding: 0 11px 0 10px;
  font-family: var(--pixel);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  background:
    var(--edge) left top / var(--b) 100%,
    var(--edge) right top / var(--b) 100%,
    var(--edge) left bottom / 100% var(--b),
    var(--edge) left top / var(--gap-l) var(--b),
    var(--edge) right top / calc(100% - var(--gap-l) - var(--gap-w)) var(--b);
  background-repeat: no-repeat;
}

.local-badge::after {
  content: "";
  position: absolute;
  top: -2px;
  left: calc(var(--gap-l) + (var(--gap-w) - 6px) / 2);
  width: 6px;
  height: 6px;
  background: var(--accent);
  transition: transform 0.45s var(--ease);
}

.local-badge:hover::after { transform: translateY(-3px); }

.local-badge svg { flex: none; width: 10px; height: 12px; color: var(--accent); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 30px;
  padding: 0 12px 0 9px;
  border: 2px solid var(--accent);
  border-radius: 0;
  background: var(--bg);
  font-family: var(--pixel);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
  transition: transform 0.2s var(--ease), background-color 0.2s, color 0.2s;
}

.btn svg { width: 13px; height: 13px; }
.btn:hover { background: var(--accent); color: var(--bg); }
.btn:active { transform: scale(0.97); }

/* ---------- Empty state ---------- */

/* The empty page follows the logo: a faint pixel grid behind everything, Departure Mono for the
   headline, and a square frame with a gap in its top edge and a dot breaking out of it. */

main { min-height: 0; }

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  min-height: calc(100dvh - var(--topbar-h));
  padding: 48px 16px 24px;
  text-align: center;
  background-image:
    linear-gradient(var(--grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid) 1px, transparent 1px);
  background-size: 22px 22px;
  background-position: center -1px;
}

/* overflow-x: clip keeps the full-width ticker from adding a horizontal scrollbar. */
.empty { overflow-x: clip; }

.hero-copy { position: relative; isolation: isolate; }

/* Ambient ticker: faint rows of made-up tags sliding behind the headline, fading out at every edge.
   app.js builds the rows, sets each row's --dur and --delay, and sets --ticker-top so the rows
   sit between the 22 px grid lines. */
.ticker {
  position: absolute;
  z-index: -1;
  top: var(--ticker-top, 0);
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  font-family: var(--pixel);
  font-size: 11px;
  line-height: 22px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: pre;
  color: var(--ticker);
  mask-image:
    linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent),
    linear-gradient(transparent, #000 28%, #000 72%, transparent);
  mask-composite: intersect;
  animation: fade-in 2s 0.4s ease both;
}

.ticker-row {
  display: flex;
  width: max-content;
  animation: ticker var(--dur, 240s) var(--delay, 0s) linear infinite;
}

.ticker-row:nth-child(even) { animation-direction: reverse; }

@keyframes ticker { to { transform: translateX(-50%); } }

@keyframes fade-in { from { opacity: 0; } }

.hero-copy h1 {
  position: relative;
  width: fit-content;
  margin: 0 auto;
  font-family: var(--pixel);
  font-size: 44px;
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: 0;
  text-transform: uppercase;
  animation: rise 0.8s var(--ease) both;
}

/* A soft blur behind the headline separates it from the ticker and grid. The mask feathers the
   blur out so it has no visible edge. */
.hero-copy h1::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -22px -44px;
  backdrop-filter: blur(4px);
  mask-image:
    linear-gradient(90deg, transparent, #000 44px, #000 calc(100% - 44px), transparent),
    linear-gradient(transparent, #000 22px, #000 calc(100% - 22px), transparent);
  mask-composite: intersect;
  pointer-events: none;
}

.hero-copy p {
  margin: 14px auto 0;
  max-width: 460px;
  color: var(--muted);
  font-size: 15.5px;
  animation: rise 0.8s 0.06s var(--ease) both;
}

@property --fill {
  syntax: "<color>";
  inherits: false;
  initial-value: transparent;
}

/* The frame is drawn with background layers: four edges, the top edge split around a gap, and a
   solid fill so the grid does not show through. --fill changes on hover and while dragging. */
.dropzone {
  --stroke: 5px;
  --gap: 76px;
  --edge: linear-gradient(var(--accent) 0 0);
  --fill: var(--bg);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: min(484px, 100%);
  aspect-ratio: 11 / 7;
  padding: 24px;
  border: 0;
  border-radius: 0;
  background:
    var(--edge) left top / var(--stroke) 100%,
    var(--edge) right top / var(--stroke) 100%,
    var(--edge) left bottom / 100% var(--stroke),
    var(--edge) left top / calc(50% - var(--gap) / 2) var(--stroke),
    var(--edge) right top / calc(50% - var(--gap) / 2) var(--stroke),
    linear-gradient(var(--fill) 0 0);
  background-repeat: no-repeat;
  cursor: pointer;
  view-transition-name: hero;
  animation: rise 0.8s 0.12s var(--ease) both;
  transition: transform 0.35s var(--ease), --fill 0.25s;
}

/* The dot of the "i", half above the top edge. */
.dropzone::after {
  content: "";
  position: absolute;
  top: -9px;
  left: calc(50% - 11px);
  width: 22px;
  height: 22px;
  background: var(--accent);
  transition: transform 0.45s var(--ease);
}

.dropzone:hover,
body.dragging .dropzone { --fill: color-mix(in srgb, var(--accent) 5%, var(--bg)); }

.dropzone:hover::after { transform: translateY(-10px); }
body.dragging .dropzone::after { transform: translateY(-16px); }

.dropzone:active { transform: scale(0.99); }

/* Three photos stacked diagonally, each one pixel (6 px) up and to the right of the one in front.
   On hover, or while a file is dragged over the page, they spread out 20 px apart along the same
   diagonal (whole pixels, so the edges stay sharp) and data scrambles rise from them (app.js). */
.dropzone-icon {
  position: relative;
  display: block;
  width: 66px;
  height: 54px;
  margin-bottom: 10px;
  color: var(--accent);
}

.stack-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  transition: transform 0.5s var(--ease);
}

/* The inside of each photo is filled so the ones behind are hidden where they overlap. */
.stack-fill { fill: var(--bg); transition: fill 0.25s; }
.dropzone:hover .stack-fill,
body.dragging .stack-fill { fill: color-mix(in srgb, var(--accent) 5%, var(--bg)); }

.stack-back { color: color-mix(in srgb, var(--accent) 45%, var(--bg)); transform: translate(12px, -12px); }
.stack-mid { color: color-mix(in srgb, var(--accent) 65%, var(--bg)); transform: translate(6px, -6px); }

.dropzone:hover .stack-back,
body.dragging .stack-back { transform: translate(20px, -20px); }
.dropzone:hover .stack-mid,
body.dragging .stack-mid { transform: translate(0, -10px); transition-delay: 0.03s; }
.dropzone:hover .stack-front,
body.dragging .stack-front { transform: translate(-20px, 0); transition-delay: 0.06s; }

/* Data scrambles rising from the photos (spawned by app.js). */
.data-bit {
  position: absolute;
  top: 0;
  left: 50%;
  font-family: var(--pixel);
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0.08em;
  white-space: pre;
  color: var(--accent);
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
}

.dropzone-title {
  font-family: var(--pixel);
  font-size: 22px;
  font-weight: 400;
  text-transform: uppercase;
}

.dropzone-sub { color: var(--muted); font-size: 14px; }

.formats {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
  animation: rise 0.8s 0.18s var(--ease) both;
}

.formats li {
  padding: 6px 9px;
  border: 2px solid var(--accent);
  background: var(--bg);
  font-family: var(--pixel);
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0.08em;
  color: var(--accent);
}

body.busy .dropzone { pointer-events: none; }
body.busy .dropzone-icon { animation: pulse 1.1s var(--ease) infinite; }

/* "Nothing is uploaded · Privacy · Terms · Contact" on one pixel-font line. */
.empty-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px 0;
  margin-top: 32px;
  padding: 2px 6px;
  background: var(--bg);
  font-family: var(--pixel);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  animation: rise 0.8s 0.24s var(--ease) both;
}

.empty-note { margin: 0; }

.empty-foot .legal-links { display: contents; font-size: inherit; }

.empty-foot .empty-note::after,
.empty-foot .legal-links a:not(:last-child)::after {
  content: "·";
  margin: 0 10px;
  color: var(--faint);
}

.empty-foot .legal-links a { color: inherit; }
.empty-foot .legal-links a:hover { color: var(--accent); }

/* ---------- Legal links and pages ---------- */

.legal-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px 18px;
  font-size: 12.5px;
  animation: rise 0.8s 0.24s var(--ease) both;
}

.legal-links a {
  color: var(--muted);
  text-decoration: none;
}

.legal-links a:hover { color: var(--accent); text-decoration: underline; }

.doc {
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  padding: 48px 16px 40px;
}

.doc h1 {
  margin: 0 0 10px;
  font-family: var(--pixel);
  font-size: 33px;
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: 0;
  text-transform: uppercase;
}

.doc h2 {
  margin: 32px 0 8px;
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.doc p { margin: 0 0 12px; color: var(--muted); }
.doc .doc-lead { margin: 20px 0 8px; font-size: 16.5px; color: var(--text); }
.doc .doc-date { font-size: 13px; color: var(--muted); }
.doc a { color: var(--accent); }
.doc ul { margin: 0 0 12px; padding-left: 20px; color: var(--muted); }
.doc li { margin-bottom: 8px; }
.doc li strong { color: var(--text); font-weight: 600; }
.doc-links { margin-top: 48px; padding-top: 20px; border-top: 1px solid var(--line); }

.error {
  margin: -8px 0 0;
  max-width: 520px;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 9%, transparent);
  animation: rise 0.4s var(--ease) both;
}

/* ---------- Drag overlay ---------- */

.drag-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: 16px;
  pointer-events: none;
  opacity: 0;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: opacity 0.2s ease;
}

.drag-overlay-inner {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  --edge: linear-gradient(var(--accent) 0 0);
  position: relative;
  background:
    var(--edge) left top / 5px 100%,
    var(--edge) right top / 5px 100%,
    var(--edge) left bottom / 100% 5px,
    var(--edge) left top / calc(50% - 38px) 5px,
    var(--edge) right top / calc(50% - 38px) 5px;
  background-repeat: no-repeat;
  font-family: var(--pixel);
  font-size: 22px;
  font-weight: 400;
  text-transform: uppercase;
  color: var(--accent);
  transform: scale(0.98);
  transition: transform 0.3s var(--ease);
}

.drag-overlay-inner::after {
  content: "";
  position: absolute;
  top: -9px;
  left: calc(50% - 11px);
  width: 22px;
  height: 22px;
  background: var(--accent);
}

/* Only show the full-screen overlay once a photo is open; the drop zone handles the empty state. */
body.dragging[data-state="viewer"] .drag-overlay { opacity: 1; }
body.dragging[data-state="viewer"] .drag-overlay-inner { transform: scale(1); }

/* Loading: shown when a file takes a moment, e.g. Safari waiting for Photos to export a drag. */
.overlay-loading { display: none; align-items: center; gap: 14px; font-size: 16.5px; }
body.loading .drag-overlay { opacity: 1; }
body.loading .drag-overlay-inner { transform: scale(1); background: none; }
body.loading .drag-overlay-inner::after { content: none; }
body.loading .overlay-drop { display: none; }
body.loading .overlay-loading { display: inline-flex; }

.spinner {
  width: 18px;
  height: 18px;
  border: 3px solid var(--accent-soft);
  border-top-color: var(--accent);
  animation: spin 0.8s steps(4) infinite;
}

/* ---------- Viewer ---------- */

.viewer {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 340px);
  height: calc(100dvh - var(--topbar-h));
}

.panel {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 22px 18px 40px;
  border-left: 1px solid var(--line);
  background: var(--surface);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  view-transition-name: panel;
}

.stage {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 0;
  padding: clamp(12px, 1.2vw, 16px);
  background:
    radial-gradient(circle at 50% 40%, color-mix(in srgb, var(--stage) 60%, transparent), var(--stage));
  overflow: hidden;
}

/* The figure fills the stage and is a size container, so the frame can fit the photo exactly:
   as wide as possible without passing the stage's width or height, or the photo's own size
   (upscaling a small file only makes it blurrier). The bottom padding reserves a strip for the
   caption, so the photo always ends above it with a gap; container units measure the content box,
   so the photo fits in the space above that strip. */
.photo {
  --fit-h: 100cqh;
  --caption-strip: 52px;
  display: grid;
  place-items: center;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  margin: 0;
  padding-bottom: var(--caption-strip);
  container-type: size;
  view-transition-name: hero;
}

/* The frame clips the zoomed photo. Its entrance animation only fills "backwards" so that, once it
   ends, nothing keeps the photo on a scaled or blurred layer (which can leave it looking soft). */
.zoom-frame {
  position: relative;
  width: min(100cqw, var(--fit-h) * var(--ar), var(--nat-w));
  aspect-ratio: var(--ar);
  overflow: hidden;
  box-shadow: var(--shadow-photo);
  cursor: zoom-in;
  touch-action: pan-y;
  overscroll-behavior: contain;
  animation: frame-in 0.6s var(--ease) backwards;
}

/* Pixel reveal: while the photo opens, a canvas over it shows the photo as big square pixels that
   get smaller in steps (pixelReveal in app.js). The real photo is hidden under it until the last
   step, then the canvas fades out and is removed. */
.pixel-reveal {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  image-rendering: pixelated;
  transition: opacity 0.2s ease;
}

.pixel-reveal.is-done { opacity: 0; }
.zoom-frame.is-revealing .zoom-img { visibility: hidden; }

.zoom-frame.is-zoomed { cursor: grab; touch-action: none; }
.zoom-frame.is-dragging { cursor: grabbing; }
.zoom-frame:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Zoom changes this image's real width and height (see attachZoom), so it is positioned absolutely
   in the frame and only ever translated, never scaled. */
.zoom-img {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}

.zoom-img.is-animating { transition: width 0.35s var(--ease), height 0.35s var(--ease), transform 0.35s var(--ease); }
.zoom-img.is-gliding { transition: width 0.14s ease-out, height 0.14s ease-out, transform 0.14s ease-out; }

/* The caption floats over the bottom of the stage instead of taking a row of its own, which leaves
   that height to the photo. */
.photo figcaption {
  position: absolute;
  left: 50%;
  bottom: 10px;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100% - 20px);
  padding: 3px 3px 3px 12px;
  border: 2px solid var(--accent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(160%) blur(14px);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  font-family: var(--pixel);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  transform: translateX(-50%);
  animation: caption-in 0.6s 0.15s var(--ease) both;
}

.photo figcaption > span { overflow: hidden; text-overflow: ellipsis; }

@keyframes caption-in {
  from { opacity: 0; transform: translate(-50%, 10px); }
  to { opacity: 1; transform: translateX(-50%); }
}

.zoom-controls {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border-left: 2px solid var(--accent);
}

.zoom-btn,
.zoom-level {
  height: 26px;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s;
}

.zoom-btn { width: 26px; font-size: 16.5px; line-height: 1; }
.zoom-level { min-width: 52px; padding: 0 8px; color: var(--muted); }
.zoom-btn:hover:not(:disabled),
.zoom-level:hover { background: var(--accent-soft); color: var(--accent); }
.zoom-btn:disabled { color: var(--faint); cursor: default; }
.zoom-btn:focus-visible,
.zoom-level:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.preview-fallback {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: min(440px, 100%);
  aspect-ratio: 4 / 3;
  padding: 28px;
  border: 2px solid var(--accent);
  background: var(--card);
  text-align: center;
  animation: photo-in 0.7s var(--ease) both;
}

.preview-fallback svg { width: 40px; height: 40px; color: var(--faint); margin-bottom: 6px; }
.preview-fallback strong { font-size: 15.5px; font-weight: 600; }
.preview-fallback span { font-size: 13.5px; color: var(--muted); max-width: 320px; }

/* ---------- Panel content ---------- */

.summary {
  padding: 4px 6px 18px;
  animation: rise 0.6s var(--ease) both;
}

.summary-eyebrow {
  margin: 0 0 4px;
  overflow: hidden;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 450;
  letter-spacing: 0;
  color: var(--faint);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.summary-title {
  margin: 0;
  font-family: var(--pixel);
  font-size: 22px;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

.summary-sub {
  margin: 4px 0 0;
  font-size: 14px;
  color: var(--muted);
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}

.chips li {
  padding: 4px 7px;
  border: 2px solid var(--accent);
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.notice {
  display: flex;
  gap: 12px;
  margin: 0 0 12px;
  padding: 14px;
  border: 1px solid var(--line-strong);
  border-left: 3px solid var(--accent);
  background: var(--card);
  animation: rise 0.6s 0.05s var(--ease) both;
}

.notice svg { flex: none; width: 20px; height: 20px; margin-top: 1px; color: var(--accent); }
.notice strong { display: block; font-size: 14px; font-weight: 600; margin-bottom: 2px; }
.notice p { margin: 0; font-size: 13px; color: var(--muted); }

.group {
  margin: 0 0 12px;
  padding: 0;
  border: 1px solid var(--line-strong);
  background: var(--card);
  animation: rise 0.6s calc(0.05s + var(--i, 0) * 0.045s) var(--ease) both;
}

.group-head {
  margin: 0;
  font-family: var(--pixel);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
}

/* The whole header is the collapse button. */
.group-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 14px 16px 6px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: left;
  cursor: pointer;
  transition: padding-bottom 0.32s var(--ease), color 0.2s;
}

.group-toggle:hover { color: var(--muted); }
.group-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.group-head svg { width: 14px; height: 14px; color: var(--accent); }

.group-head .group-chevron {
  margin-left: auto;
  color: var(--faint);
  transition: transform 0.32s var(--ease), color 0.2s;
}

.group-toggle:hover .group-chevron { color: var(--muted); }

/* Smooth collapse: the body's single grid row animates between its full height and zero. */
.group-body {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 0.32s var(--ease), opacity 0.24s ease;
}

.group-inner {
  min-height: 0;
  overflow: hidden;
  padding: 0 16px;
}

.group-inner > :last-child { margin-bottom: 8px; }

.group.is-collapsed .group-toggle { padding-bottom: 14px; }
.group.is-collapsed .group-chevron { transform: rotate(-90deg); }
.group.is-collapsed .group-body { grid-template-rows: 0fr; opacity: 0; }

.rows { margin: 0; }

.row {
  display: grid;
  grid-template-columns: minmax(96px, 38%) 1fr;
  gap: 12px;
  padding: 7px 0;
  border-top: 1px solid var(--line);
}

.row:first-child { border-top: 0; }

.row dt {
  font-size: 13px;
  color: var(--muted);
}

.row dd {
  margin: 0;
  font-size: 13.5px;
  font-weight: 500;
  text-align: right;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* Numbers (1/250 s, f/2.8, coordinates, sizes) read best in the mono face. */
.row dd.num { font-family: var(--mono); font-size: 13px; }

/* Decode reveal: the not-yet-resolved end of a value, in the pixel face (decodeValues in app.js). */
.scramble {
  font-family: var(--pixel);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--accent);
}

.row dd small {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--faint);
}

.copy-value {
  display: block;
  width: 100%;
  min-height: 2.6em;
  margin: -3px -6px;
  padding: 3px 6px;
  box-sizing: content-box;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  cursor: copy;
  transition: background-color 0.2s;
}

.copy-value:hover { background: var(--accent-soft); }
.copy-value:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.copy-value.is-copied { display: grid; place-items: center end; }

.copy-done {
  color: var(--accent);
  font-weight: 600;
  animation: rise 0.3s var(--ease) both;
}

.map-link {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin: 6px 0 8px;
  padding: 8px 12px;
  border: 2px solid var(--accent);
  font-family: var(--pixel);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
  background: none;
  transition: background-color 0.2s, color 0.2s, transform 0.2s var(--ease);
}

.map-link svg { width: 13px; height: 13px; }
button.map-link { width: 100%; cursor: pointer; }
button.map-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.notice-action { margin: 10px 0 0; }

.map-box { margin: 6px 0 2px; }
.map-box .map-link { margin: 0; }

.map-note {
  margin: 6px 2px 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted);
}

.map-error { margin: 0 2px; color: var(--danger); }

.map-frame {
  overflow: hidden;
  aspect-ratio: 600 / 360;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--stage);
}

.map-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}

.map-box.is-loaded .map-img { opacity: 1; }

.map-credit {
  margin: 4px 2px 0;
  font-size: 11.5px;
  text-align: right;
  color: var(--muted);
}

.map-credit a { color: var(--muted); }
.map-credit a:hover { color: var(--accent); }
.map-link:hover { background: var(--accent); color: var(--bg); }
.map-link:active { transform: scale(0.98); }

/* Raw tags */

.raw { padding-bottom: 6px; }

.raw > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: -14px -16px -8px;
  padding: 14px 16px;
  list-style: none;
  cursor: pointer;
  font-family: var(--pixel);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
  border-radius: var(--radius);
}

.raw > summary::-webkit-details-marker { display: none; }
.raw > summary > svg { flex: none; width: 14px; height: 14px; color: var(--accent); }
.raw > summary:hover { color: var(--muted); }
.raw > summary .count {
  padding: 1px 6px;
  font-size: 11px;
  letter-spacing: 0;
  color: var(--muted);
  background: var(--line);
}

.raw > summary .chev {
  width: 14px;
  height: 14px;
  margin-left: auto;
  transition: transform 0.3s var(--ease);
}

.raw[open] > summary { margin-bottom: 6px; }
.raw[open] > summary .chev { transform: rotate(180deg); }
.raw[open] .raw-body { animation: rise 0.4s var(--ease) both; }

.raw-segment {
  margin: 14px 0 4px;
  font-family: var(--pixel);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
}

.raw-segment:first-child { margin-top: 4px; }

/* Raw tags stack the name above the value: long tag names and XMP strings
   get the full panel width instead of wrapping in two narrow columns. */
.raw .row { grid-template-columns: 1fr; gap: 2px; }
.raw .row dt { font-family: var(--mono); font-size: 11.5px; overflow-wrap: anywhere; }
.raw .row dd { font-family: var(--mono); font-size: 11.5px; font-weight: 400; text-align: left; }

/* ---------- Motion ---------- */

@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

/* The photo frame fades and grows in without blur, so the pixel reveal stays sharp. */
@keyframes frame-in {
  from { opacity: 0; transform: scale(0.97); }
  to { opacity: 1; transform: none; }
}

@keyframes photo-in {
  from { opacity: 0; transform: scale(0.97); filter: blur(6px); }
  to { opacity: 1; transform: none; filter: none; }
}

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

@keyframes pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(0.92); opacity: 0.6; }
}

@keyframes panel-in {
  from { opacity: 0; transform: translateX(24px); }
  to { opacity: 1; transform: none; }
}

::view-transition-group(*) { animation-duration: 0.55s; animation-timing-function: var(--ease); }
::view-transition-old(hero),
::view-transition-new(hero) { height: 100%; object-fit: contain; }
::view-transition-new(panel):only-child { animation: panel-in 0.55s var(--ease) both; }
::view-transition-old(panel):only-child { animation: panel-in 0.3s var(--ease) reverse both; }

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0s !important;
    transition-duration: 0.001ms !important;
  }

  /* An endless loop at near-zero duration would flicker, so the ticker rows stand still. */
  .ticker-row { animation: none !important; }
}

/* ---------- Narrow screens ---------- */

@media (max-width: 860px) {
  .viewer {
    grid-template-columns: 1fr;
    height: auto;
  }

  .stage {
    min-height: 0;
    padding: 16px;
  }

  /* The stage has no fixed height here, so fit to the width and 66% of the screen height, with the
     caption back in the flow below the photo. */
  .photo {
    --fit-h: 66svh;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    height: auto;
    padding-bottom: 0;
    container-type: inline-size;
  }

  .photo figcaption {
    position: static;
    transform: none;
    animation: rise 0.6s 0.15s var(--ease) both;
  }

  .panel {
    overflow: visible;
    border-left: 0;
    border-top: 1px solid var(--line);
    padding: 20px 16px 48px;
  }
}

@media (max-width: 520px) {
  .hero-copy h1 { font-size: 33px; }
  .dropzone { --gap: 60px; }
  .formats { gap: 6px; }
  .formats li { padding: 5px 6px; letter-spacing: 0.04em; }

  /* The note on its own line, the links below it. */
  .empty-foot .empty-note { flex-basis: 100%; }
  .empty-foot .empty-note::after { content: none; }

  /* Icon only, but the words stay for screen readers. */
  .local-badge span {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .local-badge { width: 30px; padding: 0 10px; }
  .btn span { display: none; }
  .btn { padding: 0 9px; }
}
