/* photod — "Darkroom"
   The photographs are the content. Everything here is built to get out of
   their way: warm tones rather than clinical grey, generous gallery-wall
   spacing, and chrome that fades out the moment it isn't needed. */

@font-face {
  font-family: 'Fraunces';
  src: url('/static/fonts/fraunces.woff2') format('woff2-variations');
  font-weight: 300 700;
  font-display: swap;
}
@font-face {
  font-family: 'Archivo';
  src: url('/static/fonts/archivo.woff2') format('woff2-variations');
  font-weight: 400 600;
  font-display: swap;
}

/* ---------- theme tokens ----------
   Dark is the default. Light applies either when the OS asks for it and the
   visitor hasn't chosen otherwise, or when they've explicitly picked it.
   The light block is written twice because plain CSS has no way to alias a
   declaration block; keeping the token list short is what makes that fine. */

:root {
  --serif: 'Fraunces', 'Iowan Old Style', Palatino, Georgia, serif;
  --sans: 'Archivo', 'Helvetica Neue', Helvetica, sans-serif;
  --gap: 10px;
  --pad: clamp(20px, 5vw, 72px);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* Warm blacks. Pure #000 reads as a hole in the screen; a little warmth
     makes prints sit on the page instead of floating in a void. */
  --bg: #0c0b0a;
  --bg-lift: #161412;
  --cell-bg: #161412;
  --ink: #f2ede6;
  --ink-dim: #9a9188;
  --ink-faint: #5f584f;
  --accent: #d8a26a;
  --line: rgba(242, 237, 230, 0.11);
  --grain: 0.028;
  --glow: rgba(216, 162, 106, 0.20);

  --lb-bg: #080706;
  --lb-ink: #f2ede6;
  --lb-chrome: rgba(255, 255, 255, 0.07);
  --lb-chrome-hover: rgba(255, 255, 255, 0.18);
  --lb-scrim: rgba(0, 0, 0, 0.55);
  --lb-pill-bg: rgba(0, 0, 0, 0.55);
  --lb-pill-ink: rgba(255, 255, 255, 0.9);
  --lb-shadow: rgba(0, 0, 0, 0.6);
}

:root[data-theme="light"] {
  --bg: #f7f4ef;
  --bg-lift: #ffffff;
  --cell-bg: #e8e2d8;
  --ink: #17140f;
  --ink-dim: #6d6459;
  --ink-faint: #a79c8d;
  --accent: #9a5f22;
  --line: rgba(23, 20, 15, 0.14);
  --grain: 0.020;
  --glow: rgba(154, 95, 34, 0.16);

  --lb-bg: #efeae2;
  --lb-ink: #17140f;
  --lb-chrome: rgba(23, 20, 15, 0.08);
  --lb-chrome-hover: rgba(23, 20, 15, 0.18);
  --lb-scrim: rgba(255, 255, 255, 0.6);
  --lb-pill-bg: rgba(255, 255, 255, 0.8);
  --lb-pill-ink: rgba(23, 20, 15, 0.85);
  --lb-shadow: rgba(23, 20, 15, 0.22);
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --bg: #f7f4ef;
    --bg-lift: #ffffff;
    --cell-bg: #e8e2d8;
    --ink: #17140f;
    --ink-dim: #6d6459;
    --ink-faint: #a79c8d;
    --accent: #9a5f22;
    --line: rgba(23, 20, 15, 0.14);
    --grain: 0.020;
    --glow: rgba(154, 95, 34, 0.16);

    --lb-bg: #efeae2;
    --lb-ink: #17140f;
    --lb-chrome: rgba(23, 20, 15, 0.08);
    --lb-chrome-hover: rgba(23, 20, 15, 0.18);
    --lb-scrim: rgba(255, 255, 255, 0.6);
    --lb-pill-bg: rgba(255, 255, 255, 0.8);
    --lb-pill-ink: rgba(23, 20, 15, 0.85);
    --lb-shadow: rgba(23, 20, 15, 0.22);
  }
}

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Otherwise every tap on a photo flashes a blue rectangle. */
  -webkit-tap-highlight-color: transparent;
}

/* A whisper of grain. Photographic paper is never perfectly smooth, and this
   keeps large flat areas from banding on cheap displays. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; }

/* ---------- shared type ---------- */

.eyebrow {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.display {
  font-family: var(--serif);
  font-weight: 300;
  font-variation-settings: 'opsz' 120, 'SOFT' 20;
  letter-spacing: -0.015em;
  line-height: 1.02;
  margin: 0;
}

/* ---------- gallery header ---------- */

.wrap {
  position: relative;
  z-index: 2;
  padding-left: max(var(--pad), env(safe-area-inset-left));
  padding-right: max(var(--pad), env(safe-area-inset-right));
}

.masthead {
  padding: clamp(48px, 11vh, 132px) 0 clamp(28px, 5vh, 56px);
  max-width: 1100px;
}
.masthead h1 { font-size: clamp(2.6rem, 8vw, 5.4rem); }
.masthead .meta {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 22px;
  flex-wrap: wrap;
}
.dot { width: 3px; height: 3px; border-radius: 50%; background: var(--ink-faint); }

.topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: max(18px, env(safe-area-inset-top));
}
.backlink { font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-dim); }
.backlink:hover { color: var(--ink); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 11px 20px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: transparent;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .35s var(--ease), border-color .35s var(--ease), color .35s var(--ease);
}
.btn svg { width: 14px; height: 14px; }

/* Theme toggle: shows the icon of the mode you'd switch *to*. */
.theme-toggle {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  flex: none;
  transition: background .3s var(--ease), border-color .3s var(--ease);
}
.theme-toggle svg { width: 17px; height: 17px; }
.theme-toggle .i-sun { display: none; }
.theme-toggle .i-moon { display: block; }
:root[data-theme="light"] .theme-toggle .i-sun { display: block; }
:root[data-theme="light"] .theme-toggle .i-moon { display: none; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .theme-toggle .i-sun { display: block; }
  :root:not([data-theme="dark"]) .theme-toggle .i-moon { display: none; }
}

/* ---------- justified photo grid ---------- */

.grid { display: flex; flex-direction: column; gap: var(--gap); padding-bottom: clamp(60px, 12vh, 140px); }
/* Centred, because a row whose height was capped won't span the full width. */
.row { display: flex; gap: var(--gap); justify-content: center; }

.cell {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--cell-bg);
  cursor: zoom-in;
  animation: rise .7s var(--ease) both;
}
.cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .6s var(--ease), transform .9s var(--ease);
}
.cell img.ready { opacity: 1; }

.cell::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 90% at 50% 110%, var(--glow), transparent 62%);
  opacity: 0;
  transition: opacity .45s var(--ease);
  pointer-events: none;
}

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

.empty {
  padding: 12vh 0 20vh;
  color: var(--ink-dim);
  font-family: var(--serif);
  font-size: 1.3rem;
  font-weight: 300;
}

/* ---------- lightbox ---------- */

.lb {
  position: fixed;
  /* inset:0 alone. An explicit height would override bottom:0 and size the
     overlay to that height instead of stretching to the viewport — with
     100svh (the *smallest* viewport height) that left a gap on phones with
     the grid showing through underneath. Fixed + inset:0 always fills. */
  inset: 0;
  z-index: 60;
  background: var(--lb-bg);
  opacity: 0;
  visibility: hidden;
  transition: opacity .38s var(--ease), visibility .38s;
  /* Stops a swipe in the viewer triggering pull-to-refresh, and stops touch
     gestures scrolling the grid underneath without having to freeze the page. */
  overscroll-behavior: contain;
  touch-action: none;
}
.lb.open { opacity: 1; visibility: visible; }
.lb.chrome-hidden { cursor: none; }

/* Deliberately NOT pinning the body with position:fixed while the viewer is
   open. That does lock scrolling, but it also makes the page unscrollable —
   and iOS Safari only collapses its toolbar in response to scrolling, so
   pinning guarantees the browser chrome stays on screen for as long as you
   are looking at a photo. Background scrolling is blocked by touch-action
   and preventDefault on the overlay instead, which leaves Safari's own
   auto-hide behaviour intact. */

.lb-stage {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  padding: 3vmin;
  overflow: hidden;
  /* The viewer implements its own pinch/pan, so the browser must not also
     scroll or zoom in response to the same gestures. */
  touch-action: none;
}
.lb-stage img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  opacity: 0;
  transition: opacity .3s var(--ease);
  box-shadow: 0 40px 120px var(--lb-shadow);
  transform-origin: center center;
  will-change: transform;
  -webkit-user-select: none; user-select: none;
  -webkit-user-drag: none;
}
.lb-stage img.ready { opacity: 1; }
/* Animate only deliberate zoom steps, never live pinch or drag. */
.lb-stage img.zoom-anim { transition: opacity .3s var(--ease), transform .28s var(--ease); }
.lb.zoomed { cursor: zoom-out; }

@media (max-width: 640px) {
  /* Edge to edge on a phone: 3vmin of padding is ~12px of screen given away
     on the one axis where a landscape photo has none to spare. */
  .lb-stage { padding: 0; }
  .lb-stage img { box-shadow: none; }
}

.lb-chrome {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 1;
  transition: opacity .4s var(--ease);
}
.lb.chrome-hidden .lb-chrome { opacity: 0; }
.lb-chrome > * { pointer-events: auto; }

.lb-top {
  position: absolute;
  top: 0; left: 0; right: 0;
  /* Above .nav. The nav zones come later in the DOM, so without this they
     paint over these buttons and every click lands on "next" instead. */
  z-index: 2;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: clamp(14px, 2.6vw, 26px);
  padding-top: max(clamp(14px, 2.6vw, 26px), env(safe-area-inset-top));
  padding-left: max(clamp(14px, 2.6vw, 26px), env(safe-area-inset-left));
  padding-right: max(clamp(14px, 2.6vw, 26px), env(safe-area-inset-right));
  background: linear-gradient(to bottom, var(--lb-scrim), transparent);
}
.lb-title {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 15px;
  color: var(--lb-ink);
  opacity: .7;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lb-actions { display: flex; gap: 6px; flex: none; }

.icon {
  display: grid;
  place-items: center;
  /* 44px is the documented minimum comfortable touch target. */
  width: 44px; height: 44px;
  border: 0; border-radius: 50%;
  background: var(--lb-chrome);
  color: var(--lb-ink);
  cursor: pointer;
  transition: background .3s var(--ease), transform .3s var(--ease);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
.icon:active { transform: scale(.92); }
.icon svg { width: 17px; height: 17px; }

/* Generous invisible hit zones: on a phone the whole left/right third
   advances, which is far easier than aiming for a small chevron.
   Inset vertically so they never sit under the top bar or the counter —
   z-index alone would fix the clicks, but overlapping hit areas are still
   a trap waiting to be re-broken. */
.nav {
  position: absolute;
  top: 76px; bottom: 76px;
  z-index: 1;
  width: 32%;
  max-width: 220px;
  border: 0;
  background: transparent;
  color: var(--lb-ink);
  cursor: pointer;
  display: grid;
  place-items: center;
  opacity: .55;
  transition: opacity .3s var(--ease);
}
.nav.prev { left: 0; justify-items: start; padding-left: clamp(10px, 2vw, 26px); }
.nav.next { right: 0; justify-items: end; padding-right: clamp(10px, 2vw, 26px); }
.nav svg { width: 30px; height: 30px; filter: drop-shadow(0 2px 10px var(--lb-shadow)); }
.nav[disabled] { opacity: 0; pointer-events: none; }

.lb-count {
  position: absolute;
  left: 50%;
  bottom: max(clamp(16px, 3vh, 30px), env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 2;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.22em;
  color: var(--lb-pill-ink);
  background: var(--lb-pill-bg);
  padding: 7px 15px;
  border-radius: 100px;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  white-space: nowrap;
}

/* Shown only if a photo takes more than ~180 ms — preloaded neighbours never
   trigger it. The outgoing photo stays visible underneath rather than
   blanking. */
.lb.loading .lb-stage img { opacity: .4; transition: opacity .2s var(--ease); }
.lb.loading .lb-stage::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 26px; height: 26px;
  margin: -13px 0 0 -13px;
  border: 1.5px solid var(--lb-chrome);
  border-top-color: var(--lb-ink);
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- password gate ---------- */

.gate {
  position: relative;
  z-index: 2;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: var(--pad);
}
.gate-inner { width: 100%; max-width: 420px; text-align: center; }
.gate h1 { font-size: clamp(2rem, 6vw, 3rem); margin-bottom: 10px; }
.gate p { color: var(--ink-dim); font-size: 14px; margin: 0 0 40px; }

.field { position: relative; margin-bottom: 22px; }
.field input {
  width: 100%;
  padding: 14px 4px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1.25rem;
  font-weight: 300;
  text-align: center;
  letter-spacing: .08em;
  outline: none;
  transition: border-color .35s var(--ease);
}
.field input::placeholder { color: var(--ink-faint); letter-spacing: .16em; font-size: .85rem; font-family: var(--sans); text-transform: uppercase; }
.field input:focus { border-bottom-color: var(--accent); }

.gate .btn { width: 100%; justify-content: center; padding: 14px; }
.gate-corner { position: fixed; top: max(16px, env(safe-area-inset-top)); right: max(16px, env(safe-area-inset-right)); z-index: 3; }

.alert {
  margin-bottom: 26px;
  padding: 11px 16px;
  border-left: 2px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--ink);
  font-size: 13px;
  text-align: left;
}

/* ---------- album / group cards ---------- */

.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; padding-bottom: 12vh; }
.card {
  border: 1px solid var(--line);
  border-radius: 3px;
  overflow: hidden;
  background: var(--bg-lift);
  transition: transform .35s var(--ease), box-shadow .35s var(--ease);
}
/* Cover uses a real <img> rather than an inline background-image, because the
   URL is per-album and a dynamic style="" attribute cannot exist under a
   strict CSP. An <img> is better semantics anyway and is covered by img-src. */
.card .cover {
  position: relative;
  aspect-ratio: 4/3;
  background: var(--cell-bg);
  display: grid; place-items: center;
  color: var(--ink-faint); font-size: 12px;
  overflow: hidden;
}
.card .cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.card .body { padding: 15px 16px 17px; }
.card h3 { font-family: var(--serif); font-weight: 400; font-size: 1.1rem; margin: 0 0 5px; }

/* ---------- admin ---------- */

.admin-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: max(22px, env(safe-area-inset-top)) 0 22px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 44px;
}
.admin-bar .right { display: flex; align-items: center; gap: 10px; }
.brand { font-family: var(--serif); font-size: 19px; font-weight: 600; letter-spacing: -.01em; }
.brand span { color: var(--accent); }

.admin-main { max-width: 1080px; margin: 0 auto; padding-bottom: 15vh; }
.admin-main h1 { font-size: clamp(2rem, 5vw, 3rem); margin-bottom: 30px; }

.panel {
  background: var(--bg-lift);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: clamp(20px, 3vw, 30px);
  margin-bottom: 30px;
}
.panel h2 { font-family: var(--serif); font-weight: 400; font-size: 1.3rem; margin: 0 0 18px; }

.form-row { display: flex; gap: 10px; flex-wrap: wrap; }
.form-row input {
  flex: 1 1 190px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 14px;
  outline: none;
  transition: border-color .3s var(--ease);
}
.form-row input:focus { border-color: var(--accent); }

.checks { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 18px; }
.check {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 15px;
  border: 1px solid var(--line);
  border-radius: 100px;
  cursor: pointer;
  font-size: 13px;
  transition: border-color .3s var(--ease), background .3s var(--ease);
}
.check:has(input:checked) { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.check input { accent-color: var(--accent); }

.dropzone {
  border: 1px dashed var(--line);
  border-radius: 3px;
  padding: clamp(34px, 7vw, 64px) 20px;
  text-align: center;
  color: var(--ink-dim);
  transition: border-color .3s var(--ease), background .3s var(--ease);
  cursor: pointer;
}
.dropzone.over { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, transparent); }
.dropzone strong { display: block; font-family: var(--serif); font-size: 1.25rem; font-weight: 400; color: var(--ink); margin-bottom: 6px; }

.progress { height: 2px; background: var(--line); border-radius: 2px; overflow: hidden; margin-top: 18px; display: none; }
.progress.on { display: block; }
.progress i { display: block; height: 100%; width: 0; background: var(--accent); transition: width .3s var(--ease); }

.share {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  padding: 13px 15px; border: 1px solid var(--line); border-radius: 2px;
  background: var(--bg); font-size: 13px; margin-top: 6px;
}
.share code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--accent); word-break: break-all; }

.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; margin-top: 22px; }
.thumbs img { aspect-ratio: 1; object-fit: cover; border-radius: 2px; width: 100%; }

/* Spacing utilities.
   These exist so no template needs a style="" attribute: a strict
   Content-Security-Policy of style-src 'self' blocks inline styles outright,
   and carrying 'unsafe-inline' just to nudge a few margins would be a poor
   trade. */
.m-0      { margin: 0; }
.mb-8     { margin: 0 0 8px; }
.mb-16    { margin: 0 0 16px; }
.mt-12    { margin: 12px 0 0; }
.mt-14    { margin: 14px 0 0; }
.mt-18    { margin: 18px 0 10px; }
.mt-20    { margin-top: 20px; }
.subtitle { margin: -18px 0 30px; }
.contents { display: contents; }
.link-accent { color: var(--accent); }
.check.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }

/* ---------- modal ---------- */
.modal-back {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0,0,0,.5);
  display: grid; place-items: center;
  padding: 20px;
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.modal {
  width: 100%; max-width: 480px;
  background: var(--bg-lift);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: clamp(20px, 4vw, 30px);
  box-shadow: 0 24px 70px rgba(0,0,0,.35);
}
.modal h2 { font-family: var(--serif); font-weight: 400; font-size: 1.35rem; margin: 0 0 12px; }
.modal p { font-size: 14px; margin: 0 0 14px; color: var(--ink-dim); }
.modal .names {
  max-height: 130px; overflow-y: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px; color: var(--ink-dim);
  background: var(--bg); border: 1px solid var(--line); border-radius: 2px;
  padding: 10px 12px; margin-bottom: 20px; line-height: 1.7;
}
.modal-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.modal-actions .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--bg-lift); }

.muted { color: var(--ink-dim); font-size: 13px; }
.danger { color: #c2452a; }
.warn {
  padding: 12px 16px; border-left: 2px solid #c2452a;
  background: rgba(194, 69, 42, .08); font-size: 13px; margin-bottom: 18px;
}
.tag {
  display: inline-block; padding: 3px 10px; border-radius: 100px;
  border: 1px solid var(--line); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-dim); margin-right: 5px;
}

/* ---------- interaction: pointer-only ----------
   Hover effects latch on touch devices — tap a photo and it stays scaled and
   glowing until you tap elsewhere. Gate them behind a real pointer. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { background: var(--ink); border-color: var(--ink); color: var(--bg); }
  .theme-toggle:hover { background: var(--lb-chrome); border-color: var(--ink-dim); }
  .cell:hover img { transform: scale(1.035); }
  .cell:hover::after { opacity: 1; }
  .icon:hover { background: var(--lb-chrome-hover); }
  .nav:hover { opacity: 1; }
  .card:hover { transform: translateY(-3px); box-shadow: 0 12px 32px var(--lb-shadow); }
  .dropzone:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 5%, transparent); }
}

@media (max-width: 640px) {
  .masthead { padding-top: 34px; }
  .nav { width: 40%; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
