/* ------------------------------------------------------------------
   256 — a camera roll, mostly
   Visual language: one serif voice (Libre Baskerville) for everything, a
   spaced-out scattered library on a warm, faintly grained ground, and
   a single interaction accent (#F5BD33) reserved for hover states —
   tying back to the idea of a highlighter dragged across something
   worth a second look.
------------------------------------------------------------------- */

/* Self-hosted (SIL Open Font License) rather than loaded from Google
   Fonts — one small same-origin file, preloaded in index.html, instead
   of a render-blocking third-party stylesheet plus a second font host.
   A single variable file covers weights 400–700; latin subset only. */
@font-face {
  font-family: "Libre Baskerville";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/libre-baskerville-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --paper: #ffffff;
  --ink: #000000;
  --ink-soft: #555555;
  --ink-faint: #9a9a9a;
  --line: #e8e8e8;
  --card-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.28), 0 2px 5px rgba(0, 0, 0, 0.08);

  /* type — one family throughout; hierarchy comes from size/weight/
     spacing, not a second typeface */
  --font: "Libre Baskerville", Baskerville, Georgia, "Times New Roman", serif;
  --font-display: var(--font); /* legacy alias, kept so nothing else needs renaming */
  --font-body: var(--font);
  --weight-regular: 400;
  --weight-medium: 500;

  /* the one accent color, used only for hover/interaction states */
  --yellow: #f5bd33;
  --yellow-hover: rgba(245, 189, 51, 0.4);

  /* background grain — independent, low-opacity, easy to tune later */
  --grain-opacity: 0.05;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: var(--weight-regular);
  font-size: 14px;
  letter-spacing: -0.01em;
  -webkit-font-smoothing: antialiased;
}

/* Warm, faint grain across the page background. Fixed + pointer-events:
   none so it never intercepts input, and painted before .app in the box
   order (both positioned, z-index:auto) so it always sits behind the
   interface without needing a z-index fight. Turn it up/down with
   --grain-opacity above; the sepia/saturate pair is what gives the
   (naturally greyscale) noise its warm cast. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  filter: sepia(0.65) saturate(2) brightness(1.08);
}

button {
  font-family: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  border-radius: 3px;
  transition: background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease, transform 0.15s ease;
}

/* ---------------- layout shell ---------------- */

.app {
  display: flex;
  height: 100vh;
  overflow: hidden;
  position: relative;
}

/* ---------------- icon nav (category symbols) ----------------
   Flattened on purpose: all six symbols and the brand block are direct
   siblings in one space-between row, so the gaps between all seven
   items come out approximately equal — evenly spread across the full
   header width rather than clustered into two tight trios at the
   edges. */

.icon-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  width: 100%;
}

.nav-symbol {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  flex: 0 0 52px;
  /* the rest of the toolbar is pointer-events:none so its text can't
     block the canvas below; these specific boxes opt back in so they
     can be hovered/focused — the box itself doubles as the (generous)
     click target, so it grows along with the visual symbol size */
  pointer-events: auto;
  cursor: pointer;
  outline: none;
}

.nav-symbol img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  position: relative;
  z-index: 1;
  /* Sizing hierarchy: unselected symbols sit at a smaller default size
     and grow to their full (current/original) size once active — the
     inverse of shrinking on activation, so the active symbol reads as
     the one that's "stepped forward." --nav-scale is what the hover
     wiggle below also multiplies against, so a hover never resets
     whichever size (default or active) is currently showing. */
  --nav-scale: 0.82;
  transform: scale(var(--nav-scale));
  transition: transform 0.18s ease;
}

/* Sized larger than the 52x52 box via an absolutely positioned
   pseudo-element, so it can breathe without changing the box's own
   dimensions (no layout shift). Blurred so it reads as a soft, diffuse
   glow rather than a hard-edged colored circle; shown on hover/focus
   only (see below), not tied to the active-mode state. */
.nav-symbol::before {
  content: "";
  position: absolute;
  inset: -12px;
  border-radius: 50%;
  background: var(--yellow-hover);
  filter: blur(7px);
  opacity: 0;
  transform: scale(0.85);
  transition: opacity 0.15s ease, transform 0.15s ease;
  pointer-events: none;
}

/* Active mode indicator — the symbol grows to its full original size;
   box/hit-area dimensions never shift, and transform scales from the
   img's own center, which the flex-centered .nav-symbol box already
   keeps aligned. */
.nav-symbol.active img {
  --nav-scale: 1;
}

/* The soft glow itself is a hover/focus cue only, independent of
   whether the symbol's mode is active. */
.nav-symbol:hover::before,
.nav-symbol:focus-visible::before {
  opacity: 1;
  transform: scale(1);
}

/* A very small, quick wobble on hover/focus — reuses symbolWiggle
   (also used by the info button below) at whatever size the symbol is
   currently showing (default or active), via --nav-scale, so hovering
   never changes the symbol's size and never blocks the click target
   underneath it (the wiggle only ever touches the img, never
   .nav-symbol's own box/hit area). */
.nav-symbol:hover img,
.nav-symbol:focus-visible img {
  animation: symbolWiggle 0.4s ease-in-out 1;
}

@keyframes symbolWiggle {
  0%   { transform: scale(var(--nav-scale, 1)) rotate(0deg); }
  25%  { transform: scale(var(--nav-scale, 1)) rotate(-7deg); }
  50%  { transform: scale(var(--nav-scale, 1)) rotate(6deg); }
  75%  { transform: scale(var(--nav-scale, 1)) rotate(-3deg); }
  100% { transform: scale(var(--nav-scale, 1)) rotate(0deg); }
}

.brand {
  text-align: center;
  flex: 0 0 auto;
  /* Doubles as the reset-to-default control — opts back into pointer
     events (the rest of the toolbar stays click-through) without
     touching the title/subtitle's own look. */
  pointer-events: auto;
  cursor: pointer;
  outline: none;
}

.brand-name {
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: 28px;
  letter-spacing: -0.08em;
  line-height: 1.1;
  color: var(--ink);
}

.brand-sub {
  font-family: var(--font-body);
  font-size: 13px;
  letter-spacing: -0.05em;
  color: var(--ink);
  margin-top: 3px;
}

/* ---------------- main ---------------- */

.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  position: relative;
}

.toolbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  /* Comfortably above a photo card's max base z-index (10 + 40 random +
     20 large-bonus = 70) — with genuinely open-ended scroll, some card
     sits in the header's band at nearly every scroll position, and it
     must fade under the header rather than paint over its text/icons. */
  z-index: 80;
  padding: 22px 32px 0;
  height: 132px;
  /* Several stops on an easing curve (rather than one linear fade) so
     the dissolve into the field below reads as gradual throughout,
     without the single visible "edge" a two-stop fade tends to have. */
  background: linear-gradient(
    to bottom,
    var(--paper) 0%,
    var(--paper) 25%,
    rgba(255, 255, 255, 0.94) 40%,
    rgba(255, 255, 255, 0.8) 53%,
    rgba(255, 255, 255, 0.58) 65%,
    rgba(255, 255, 255, 0.36) 77%,
    rgba(255, 255, 255, 0.16) 89%,
    rgba(255, 255, 255, 0) 100%
  );
  pointer-events: none;
}

/* The viewport onto the infinite field. No native scrollbars — panning
   is done by hand (drag + wheel, see app.js) so the field can move in
   all four directions instead of just down. */
.canvas-wrap {
  flex: 1;
  position: relative;
  overflow: hidden;
  touch-action: none;
  cursor: grab;
  -webkit-user-select: none;
  user-select: none;
}

.canvas-wrap.dragging {
  cursor: grabbing;
}

/* The "world" — chunks are absolutely positioned within it at their
   world coordinates (which can be negative in any direction). Moved
   entirely via transform (translate for pan, scale for zoom) rather
   than by setting an explicit size, so it has no edges of its own. */
.canvas {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

/* ---------------- photo card ---------------- */

.photo-card {
  transition: transform 0.15s ease;
  cursor: pointer;
}

.photo-frame {
  position: relative;
  overflow: hidden;
  background: var(--line);
  box-shadow: rgba(255, 255, 255, 0.4);
  outline: 1px solid rgba(255, 255, 255, 0.4);
  outline-offset: -1px;
}

.photo-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.photo-card:hover {
  /* Above every other (non-hovered) card — those top out at 70 — but
     still safely under the header/nav's z-index:80, so a hovered card
     never jumps above the fixed toolbar and its gradient. */
  z-index: 79 !important;
}

.photo-card:hover .photo-frame {
  box-shadow: 0 14px 30px -12px rgba(0, 0, 0, 0.4), 0 3px 8px rgba(0, 0, 0, 0.12), 0 0 16px 1px var(--yellow-hover);
}

.photo-caption {
  margin-top: 6px;
  display: block;
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.015em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
}

.photo-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------------- isolation view ---------------- */

.isolation {
  position: fixed;
  inset: 0;
  background: rgba(255, 255, 255, 0.97);
  z-index: 1500;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 26px 20px;
}

.isolation[hidden] {
  display: none;
}

.isolation-close {
  position: absolute;
  top: 18px;
  right: 22px;
  background-color: transparent;
  border: none;
  color: var(--ink);
  font-size: 20px;
  width: 38px;
  height: 38px;
  border-radius: 50%;
}

/* No yellow at all in the default state — the glow only appears on
   hover (below), via a blurred pseudo-element behind the ✕ glyph
   rather than a box-shadow, so it reads as a soft diffuse haze
   instead of a defined circle or solid fill; the glyph itself is
   never blurred since the filter only applies to this ::before. */
.isolation-close::before {
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  background: rgba(255, 204, 0, 0.55);
  filter: blur(8px);
  opacity: 0;
  transition: opacity 0.2s ease;
  z-index: -1;
  pointer-events: none;
}

.isolation-close:hover::before {
  opacity: 1;
}

.isolation-trail {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 26px;
  flex-wrap: wrap;
  justify-content: center;
  max-width: 90vw;
}

.trail-dot {
  width: 26px;
  height: 26px;
  border: 1.5px solid transparent;
  padding: 0;
  background-color: var(--line);
  background-size: cover;
  background-position: center;
  transition: border-color 0.15s ease, transform 0.15s ease;
}

.trail-dot:hover {
  border-color: rgba(255, 204, 0, 0.7);
}

.trail-dot.current {
  border-color: rgba(255, 204, 0, 0.85);
  transform: scale(1.12);
}

.trail-sep {
  color: var(--ink);
  font-size: 10px;
}

.isolation-stage {
  flex: 1;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 56px;
  min-height: 0;
}

.isolation-image {
  display: block;
  object-fit: cover;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.25);
  flex: 0 0 auto;
}

.isolation-paths {
  display: flex;
  flex-direction: column;
  gap: 32px;
  width: 150px;
}

.path-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  background: transparent;
  border: none;
  padding: 6px;
}

.path-thumb {
  width: 46px;
  height: 46px;
  background-color: var(--line);
  background-size: cover;
  background-position: center;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.path-btn:hover .path-thumb {
  transform: scale(1.08);
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2), 0 0 26px 10px rgba(255, 204, 0, 0.35);
}

.isolation-hint {
  margin-top: 20px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--ink);
  text-align: center;
}

/* ---------------- info button + panel ---------------- */
/* Sits roughly where the old fixed metadata dock used to live, bottom
   left — a small, restrained "clue" rather than a conventional modal. */

.info-btn {
  position: fixed;
  left: 24px;
  bottom: 24px;
  z-index: 160;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--ink);
  border-radius: 50%;
  box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.35);
}

/* The "i" glyph is inline SVG filled with currentColor, so it follows
   the black/white button states shared with the zoom controls below. */
.info-btn-icon {
  display: block;
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
}

/* Same treatment as the category symbols: a small wiggle on hover, on
   top of the shared black/white state change. */
.info-btn:hover .info-btn-icon,
.info-btn:focus-visible .info-btn-icon {
  animation: symbolWiggle 0.45s ease-in-out 1;
}

/* ---------------- control button states ----------------
   One black-and-white scheme shared by zoom −/+ and the info button:
     default  — white fill, black glyph (black border)
     hover    — inverted: black fill, white glyph
     active   — black fill, white glyph, plus an inset white ring so a
                press (and the info button while its panel is open)
                reads as "pushed in", distinct from a plain hover. */

.info-btn,
.zoom-btn {
  background: var(--paper);
  color: var(--ink);
}

.info-btn:hover,
.zoom-btn:hover,
.info-btn:active,
.zoom-btn:active,
.info-btn[aria-expanded="true"] {
  background: var(--ink);
  color: var(--paper);
}

.info-btn:active,
.zoom-btn:active,
.info-btn[aria-expanded="true"] {
  box-shadow: inset 0 0 0 2px var(--ink), inset 0 0 0 3.5px var(--paper);
}

.info-btn:focus-visible,
.zoom-btn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.info-panel {
  position: fixed;
  left: 24px;
  bottom: 70px;
  z-index: 160;
  width: min(260px, calc(100vw - 48px));
  background: var(--paper);
  border: 1px solid var(--ink);
  box-shadow: 0 16px 40px -14px rgba(0, 0, 0, 0.3);
  padding: 18px 20px;
}

.info-panel[hidden] {
  display: none;
}

.info-panel p {
  margin: 0 0 10px;
  font-family: var(--font-body);
  font-size: 11.5px;
  line-height: 1.65;
  color: var(--ink);
}

.info-panel p:first-of-type {
  margin-top: 20px; /* a bit more breathing room below info-panel-close so it doesn't feel cramped against the first line — first-of-type since info-panel-close (a button, not a p) is the actual first child */
}

.info-panel p:last-child {
  margin-bottom: 0;
}

.info-panel-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 22px;
  height: 22px;
  border: none;
  background: transparent;
  color: var(--ink);
  font-size: 11px;
  border-radius: 50%;
}

.info-panel-close:hover {
  background-color: var(--yellow-hover);
}

/* ---------------- zoom controls ----------------
   Fixed bottom-right — the info button's mirror image at bottom-left,
   both riding above the canvas at the same z-index. */

.zoom-controls {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 160;
  display: flex;
  align-items: stretch;
  background: var(--paper);
  border: 1px solid var(--ink);
  box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.35);
}

.zoom-btn {
  width: 32px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 0; /* overrides the global button{} default — normal sharp square edges */
  font-family: var(--font-body);
  font-size: 22px;
  line-height: 1;
}

.zoom-btn:first-child {
  border-right: 1px solid var(--ink);
}

/* ---------------- symbol effects (instant triggers) ----------------
   EARTHQUAKE and CURSOR TRAIL (js/effects/earthquake.js,
   js/effects/cursor-trail.js) fire once per click and finish on their
   own — unlike the modes below, they never set body[data-mode] and
   never show a symbol as "active", since nothing about them stays on
   as a lens. */

/* EARTHQUAKE ("wiggle") — each card's own .photo-frame gets a small,
   independent shimmy, never the .photo-card itself: MAGNET/DRIFT write
   a card's transform every frame while active, and this can fire
   without deactivating either of them, so it needs a property of its
   own to animate rather than fighting theirs. JS sets each frame's
   --wig-x/--wig-y/--wig-rot/--wig-pop and its own animation-delay/
   -duration inline for per-card variation; 0% and 100% are both the
   identity transform so it always lands back exactly where it started.

   A card that's part of this trigger's shuffle (see earthquake.js)
   also gets --shuffle-dx/--shuffle-dy: the vector back to where it
   visually was before its position/size were swapped with a neighbor.
   The leading translate() in each stop blends that vector down to
   zero across the animation, so the card glides into its real new
   spot at the same time as it wiggles, rather than snapping there
   instantly. Chaining it before the wiggle's own translate() (rather
   than combining into one) keeps both additive regardless of the
   rotate/scale later in the same stop; an unshuffled card simply gets
   translate(0px, 0px) here via the var() fallback, so nothing changes
   for it. */
@keyframes cardWiggle {
  0%   { transform: translate(var(--shuffle-dx, 0px), var(--shuffle-dy, 0px)) translate(0, 0) rotate(0deg) scale(1); }
  20%  { transform: translate(calc(var(--shuffle-dx, 0px) * 0.55), calc(var(--shuffle-dy, 0px) * 0.55)) translate(calc(var(--wig-x) * 0.6), calc(var(--wig-y) * -0.4)) rotate(calc(var(--wig-rot) * 0.5)) scale(calc(1 + var(--wig-pop, 0) * 0.55)); }
  45%  { transform: translate(calc(var(--shuffle-dx, 0px) * 0.22), calc(var(--shuffle-dy, 0px) * 0.22)) translate(calc(var(--wig-x) * -0.85), calc(var(--wig-y) * 0.9)) rotate(calc(var(--wig-rot) * -0.9)) scale(calc(1 + var(--wig-pop, 0))); }
  70%  { transform: translate(calc(var(--shuffle-dx, 0px) * 0.06), calc(var(--shuffle-dy, 0px) * 0.06)) translate(calc(var(--wig-x) * 0.4), calc(var(--wig-y) * -0.3)) rotate(calc(var(--wig-rot) * 0.35)) scale(calc(1 + var(--wig-pop, 0) * 0.3)); }
  100% { transform: translate(0, 0) translate(0, 0) rotate(0deg) scale(1); }
}

.photo-frame.fx-wiggle {
  animation-name: cardWiggle;
  animation-timing-function: cubic-bezier(0.36, 0.07, 0.19, 0.97);
  /* duration/delay set per-card inline in JS */
}

/* CURSOR TRAIL — small copies of the page's own nav symbols, sprinkled
   around the pointer and falling mostly downward. JS sets each piece's
   symbol/size/position and its --fall-dx/--fall-dy/--fall-rot inline
   for variation; a piece is either .symbol-trail-fall (fades away
   completely) or .symbol-trail-stick (settles at low opacity and stays
   a while as a faint trace) — both share the same custom properties,
   just animated toward a different resting state, and both remove
   themselves from the DOM via their own JS timer once done. */
.symbol-trail-piece {
  position: fixed;
  display: block;
  pointer-events: none;
  z-index: 1400; /* above cards and the toolbar, below the isolation view's 1500 */
}

@keyframes symbolFallAway {
  0% { transform: translate(0, 0) rotate(0deg) scale(0.6); opacity: 0; }
  15% { opacity: 0.9; transform: translate(calc(var(--fall-dx) * 0.2), calc(var(--fall-dy) * 0.15)) rotate(calc(var(--fall-rot) * 0.2)) scale(1); }
  100% { transform: translate(var(--fall-dx), var(--fall-dy)) rotate(var(--fall-rot)) scale(0.9); opacity: 0; }
}

.symbol-trail-fall {
  animation-name: symbolFallAway;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}

@keyframes symbolFallStick {
  0% { transform: translate(0, 0) rotate(0deg) scale(0.6); opacity: 0; }
  40% { opacity: 0.8; transform: translate(calc(var(--fall-dx) * 0.6), calc(var(--fall-dy) * 0.7)) rotate(calc(var(--fall-rot) * 0.7)) scale(1); }
  100% { transform: translate(var(--fall-dx), var(--fall-dy)) rotate(var(--fall-rot)) scale(0.92); opacity: 0.32; }
}

.symbol-trail-stick {
  animation-name: symbolFallStick;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  animation-fill-mode: forwards;
}

/* DUPLICATES — a fresh copy leaps away from its source rather than
   just popping in at its final spot: it starts at the source's own
   position (the reverse of the duplicate's offset, via --dup-dx/
   --dup-dy set in js/modes/duplicates.js), overshoots slightly past
   its landing spot for a playful "jump" read, then settles there.
   animation-fill-mode:forwards (the trailing "both") keeps it at its
   resting position/scale/rotation once the jump settles, so no
   separate inline transform is needed afterward. */
@keyframes duplicatePop {
  0% { transform: translate(var(--dup-dx, 0px), var(--dup-dy, 0px)) scale(0.5) rotate(var(--duplicate-rot, 0deg)); opacity: 0; }
  60% { transform: translate(calc(var(--dup-dx, 0px) * -0.12), calc(var(--dup-dy, 0px) * -0.12)) scale(1.1) rotate(var(--duplicate-rot, 0deg)); opacity: 1; }
  100% { transform: translate(0, 0) scale(1) rotate(var(--duplicate-rot, 0deg)); opacity: 1; }
}

.duplicate-card {
  animation: duplicatePop 0.38s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

/* ---------------- mode hooks ----------------
   Scoped by the data-mode attribute ModeManager sets on <body>, so
   these only apply while their mode is active and never touch the
   plain archive. */

/* RAIN — a clicked photo falls instead of opening (js/modes/rain.js);
   pointer-events:none while falling so it can't be re-clicked or block
   whatever ends up underneath it mid-fall. */
body[data-mode="rain"] .photo-card.falling {
  pointer-events: none;
}

/* MAGNET / DRIFT — both write a card's transform every animation
   frame with their own easing already baked in (a lerp toward the
   selected center for MAGNET, velocity/position integration for
   DRIFT); the base .photo-card transition would otherwise re-smooth
   every one of those 60-times-a-second updates on top, adding a
   second, laggier layer of easing under the one already there. */
body[data-mode="magnet"] .photo-card,
body[data-mode="drift"] .photo-card {
  transition: none;
}

/* MAGNET — clicking a photo makes it the field's center (see
   js/modes/magnet.js); the base .photo-card cursor: pointer already
   reads as clickable, so no override is needed here. The center gets
   a soft glow on its frame, the same yellow already used for a
   hovered card's glow, so it's clear which photo the field is
   radiating from. */
body[data-mode="magnet"] .photo-card.magnet-center .photo-frame {
  box-shadow: 0 0 24px 6px rgba(255, 204, 0, 0.6);
}

/* PLAY WITH THE RULES — pick-up-and-move cards (js/modes/play-with-the-rules.js) */
body[data-mode="play-with-the-rules"] .photo-card {
  cursor: grab;
  touch-action: none;
}

body[data-mode="play-with-the-rules"] .photo-card.dragging {
  cursor: grabbing;
  z-index: 9999 !important;
}

body[data-mode="play-with-the-rules"] .photo-card.dragging .photo-frame {
  box-shadow: 0 20px 40px -14px rgba(0, 0, 0, 0.45), 0 6px 14px rgba(0, 0, 0, 0.16);
  outline-color: var(--yellow);
}

/* LOSE TRACK OF TIME — the wandering curiosity lure (js/modes/lose-track-of-time.js) */
.wander-lure {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 72; /* above ordinary cards, still under the toolbar's 80 */
  pointer-events: none;
  opacity: 0.7;
  will-change: transform;
}

.wander-lure svg {
  display: block;
  transform: translate(-50%, -50%);
  overflow: visible;
}

.wander-lure-wings ellipse {
  fill: var(--yellow);
  opacity: 0.55;
  animation: wanderFlutter 0.5s ease-in-out infinite;
}

.wander-lure-wings ellipse:nth-child(2) {
  animation-delay: -0.25s;
}

.wander-lure-body {
  fill: var(--ink-soft);
}

@keyframes wanderFlutter {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 0.22; }
}

/* ---------------- responsive ---------------- */

@media (max-width: 820px) {
  .toolbar { height: auto; padding: 16px 20px; }

  .icon-nav { flex-wrap: wrap; justify-content: center; row-gap: 12px; }

  /* Forces the title onto its own centered row between the two rows of
     three symbols, however the flex-wrap happens to break otherwise. */
  .brand { flex-basis: 100%; }

  .brand-name { font-size: 18px; }

  .brand-sub { font-size: 9.5px; }

  .isolation-stage {
    flex-direction: column;
    overflow-y: auto;
  }

  .isolation-paths {
    flex-direction: row;
    width: auto;
    flex-wrap: wrap;
    justify-content: center;
  }

  .isolation-image {
    max-width: 88vw;
    max-height: 40vh;
  }
}
