/* ============================================================
   v3 — Play / Sandbox.  Light (same paper as the archive),
   shared easing + motion language.  Three options.
   ============================================================ */
body { background: var(--paper); color: var(--ink); overflow: hidden; }

/* —— shared chrome —————————————————————————————————————— */
.play-switch { position: fixed; left: 50%; transform: translateX(-50%); bottom: clamp(14px, 2vw, 22px); z-index: 70;
  display: flex; gap: 0.3rem; font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.03em;
  border: 1px solid var(--line); padding: 4px; background: rgba(244,243,239,0.72); backdrop-filter: blur(8px); }
.play-switch a { padding: 4px 10px; color: var(--ink-mute); transition: color 0.3s ease, background 0.3s ease; }
.play-switch a.is-on { background: var(--ink); color: var(--paper); }
.play-switch a:not(.is-on):hover { color: var(--ink); }

.play-hint { position: fixed; right: clamp(18px, 2.4vw, 40px); bottom: clamp(16px, 2vw, 24px); z-index: 70;
  font-family: var(--mono); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-mute); }

/* minimal expand icon (corner brackets) */
.exp-ico { position: absolute; width: 16px; height: 16px; pointer-events: none; color: #fff; mix-blend-mode: difference; }
.exp-ico::before, .exp-ico::after { content: ""; position: absolute; width: 9px; height: 9px; }
.exp-ico::before { top: 0; right: 0; border-top: 1.5px solid currentColor; border-right: 1.5px solid currentColor; }
.exp-ico::after { bottom: 0; left: 0; border-bottom: 1.5px solid currentColor; border-left: 1.5px solid currentColor; }

/* ============================================================
   OPTION 01 · STACK — images scale 0 → full-bleed on a fast beat.
   Hard-edged, opaque, cropped to the window; the newest grows over
   the rest, so several sit nested as concentric hard-edged crops.
   ============================================================ */
.stage01 { position: fixed; inset: 0; overflow: hidden; cursor: pointer; background: var(--ink); }
.scard { position: absolute; inset: 0; overflow: hidden; transform: scale(0); transform-origin: center; will-change: transform; }
.scard img { width: 100%; height: 100%; object-fit: cover; display: block; }

.deck__cap { position: fixed; left: 0; right: 0; bottom: clamp(56px, 11vh, 110px); z-index: 6; text-align: center;
  font-family: var(--mono); font-size: var(--t-cap); text-transform: uppercase; letter-spacing: 0.06em; color: #fff; mix-blend-mode: difference; pointer-events: none; }
.deck__cap b { font-family: var(--display); font-weight: 500; font-size: 1.05rem; letter-spacing: -0.01em; text-transform: none; }

/* header → solid paper bar; the bottom hint still reads over the imagery via difference */
body.stack-page .play-hint { color: #fff; mix-blend-mode: difference; }
/* touch devices don't "click" — swap the hint wording */
.play-hint .hint-touch { display: none; }
@media (hover: none) { .play-hint .hint-pointer { display: none; } .play-hint .hint-touch { display: inline; } }
body.stack-page .v3head { background: var(--paper); color: var(--ink); mix-blend-mode: normal; border-bottom: 1px solid var(--line); }
body.stack-page .v3head a { color: var(--ink); }

/* ============================================================
   OPTION 02 · KINETIC — full-bleed mood board; cards push/swing
   in. Click an image → it anchors, a new related board swings in.
   ============================================================ */
.stage02 { position: fixed; inset: 0; overflow: hidden; }
.kcard { position: absolute; transform: translate(-50%, -50%); border-radius: 8px; overflow: hidden; cursor: pointer;
  box-shadow: 0 24px 60px rgba(0,0,0,0.16); opacity: 0; will-change: transform, opacity; }
.kcard img { width: 100%; height: 100%; object-fit: cover; display: block; transition: filter 0.5s ease; filter: grayscale(0.1); }
.kcard:hover { z-index: 50; }
.kcard:hover img { filter: none; }
.kcard .exp-ico { top: 12px; right: 12px; opacity: 0; transition: opacity 0.4s var(--ease-out-expo); }
.kcard:hover .exp-ico { opacity: 1; }
.kcard__cap { position: absolute; left: 10px; bottom: 9px; font-family: var(--mono); font-size: 0.56rem;
  text-transform: uppercase; letter-spacing: 0.03em; color: #fff; mix-blend-mode: difference; }

/* ============================================================
   OPTION 03 · FLOW — non-overlapping captioned cards stream
   bottom→top on scroll; idle they drift on their own; click an
   image → full bleed, click again → out.
   ============================================================ */
.stage03 { position: fixed; inset: 0; overflow: hidden; cursor: pointer; }
.flow-field { position: absolute; inset: 0; }
.fcard { position: absolute; top: 0; display: block; cursor: pointer; will-change: transform; }
.fcard__img { position: relative; width: 100%; overflow: hidden; background: var(--paper-2);
  box-shadow: 0 14px 40px rgba(0,0,0,0.08); }
.fcard__img img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.9s var(--ease-out-expo); }
.fcard:hover .fcard__img img { transform: scale(1.04); }
.fcard__cap { margin-top: 0.55rem; font-family: var(--mono); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--ink-soft); }
.fcard .exp-ico { top: 12px; right: 12px; opacity: 0; transition: opacity 0.4s var(--ease-out-expo); }
.fcard:hover .exp-ico { opacity: 1; }

.flow-full { position: fixed; inset: 0; z-index: 80; opacity: 0; pointer-events: none; background: var(--paper); cursor: zoom-out;
  transition: opacity 0.5s var(--ease-out-expo); }
.flow-full.open { opacity: 1; pointer-events: auto; }
.flow-full img { width: 100%; height: 100%; object-fit: cover; display: block; transform: scale(1.05); transition: transform 0.7s var(--ease-out-expo); }
.flow-full.open img { transform: scale(1); }
.flow-full__close { position: fixed; top: clamp(16px,3vw,28px); right: clamp(18px,3vw,30px); z-index: 81;
  font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: #fff; mix-blend-mode: difference; opacity: 0; transition: opacity 0.4s ease; }
.flow-full.open .flow-full__close { opacity: 1; }

/* ============================================================
   OPTION 04 · FRAMES — equal portrait grid; each column slides
   vertically (extreme ease-in-out), staggered across columns
   (time-displacement). Hover an image → lightbox + blurred grid;
   click → back to the animation.
   ============================================================ */
.stage04 { position: fixed; inset: 0; overflow: hidden; background: var(--paper); --pad: clamp(18px, 2.1vw, 36px); }   /* clean off-white field */
.fgrid { position: absolute; inset: 0; display: grid; grid-template-rows: 1fr; gap: var(--pad); transition: filter 0.6s var(--ease-out-expo); }
.fgrid.is-blur { filter: blur(18px) brightness(0.6) saturate(0.92); }
.fgrid.is-paused .fg-strip { animation-play-state: paused; }
.fg-col { position: relative; overflow: hidden; }
.fg-strip { position: absolute; top: 0; left: 0; width: 100%; will-change: transform; transform: translate3d(0,0,0); }   /* v3-play-grid.js drives the vertical drift + scroll-steer (rAF) */
.fg-cell { position: relative; width: 100%; overflow: hidden; cursor: pointer; margin-bottom: var(--pad); background: var(--paper); }   /* aspect set inline — true proportions, no crop; matches the stage so transparent PNGs blend + it's the loading placeholder */
/* NB do NOT add `contain` here. Tried `contain: layout paint` to let Chrome skip the ~90% of
   cells parked off-screen inside the tall strip; measured, it made stutter WORSE (16 dropped
   frames/2s vs 14 baseline, and it cancelled out the filter+transition wins below). Inside a
   single promoted, transform-animated layer the extra paint boundaries cost more than they save. */
.fg-cell img, .fg-cell video { width: 100%; height: 100%; object-fit: cover; display: block; transition: opacity 0.7s var(--ease-out-expo), filter 0.8s var(--ease-out-expo); }
.fg-cell:hover img, .fg-cell:hover video { transform: scale(1.04); transition: transform 0.5s var(--ease-out-expo); }   /* the transform transition lives ONLY on hover — off-hover it isn't a live target on all ~270 cells while the strip animates */
.fg-cell .fg-media { opacity: 0; }                              /* blur up: each frame fades (images also un-blur) in as it loads */
.fg-cell img.fg-media { filter: blur(14px); }
/* while a grid cell's image is actually fetching → a light-grey box + a looping stroke spinner (clear it's still loading) */
.fg-cell.is-loading { background: #e7e6e2; }
.fg-cell.is-loading::after {
  content: ""; position: absolute; top: 50%; left: 50%; z-index: 3;
  width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%;
  border: 2px solid rgba(17,17,16,0.16); border-top-color: rgba(17,17,16,0.62);
  animation: fg-spin 0.7s linear infinite;
}
@keyframes fg-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .fg-cell.is-loading::after { animation-duration: 1.8s; } }
.fg-cell .fg-media.is-loaded { opacity: 1; filter: none; }      /* `none`, NOT `blur(0)` — a 0px blur still costs a filter pass every frame the
                                                                   strip moves, for zero visual difference. `none` interpolates the same way
                                                                   (blur(14px) → 0) so the blur-up is unchanged, but it ends off the filter path. */
.fg-cell--eyes { background: var(--paper); }                 /* clean field behind the live googly-eyes frame */
.fg-cell--eyes googly-eyes { display: block; width: 100%; height: 100%; }
.fg-cell--face { background: var(--paper); }                 /* clean field behind the live look-away-face frame */
.fg-cell--face canvas { display: block; width: 100%; height: 100%; }
.fg-cell--frame { background: var(--paper); }                /* sandboxed interactive sketches (suns, dsc-physics) */
.fg-cell--frame iframe { display: block; width: 100%; height: 100%; border: 0; }

.fg-light { position: fixed; inset: 0; z-index: 75; display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity 0.5s var(--ease-out-expo); }
.fg-light.on { opacity: 1; }
.fg-light img, .fg-light video { height: 80vh; width: auto; max-width: 92vw; object-fit: contain; box-shadow: 0 50px 130px rgba(0,0,0,0.55);
  transform: scale(0.92); transition: transform 0.6s var(--ease-out-expo); }
.fg-light.on img, .fg-light.on video { transform: scale(1); }
.fg-light__wrap { height: 80vh; max-width: 92vw; background: var(--paper); box-shadow: 0 50px 130px rgba(0,0,0,0.55);   /* big live preview of an interactive frame (aspect set inline) */
  transform: scale(0.92); transition: transform 0.6s var(--ease-out-expo); }
.fg-light.on .fg-light__wrap { transform: scale(1); }
.fg-light__wrap > * { display: block; width: 100%; height: 100%; border: 0; }

/* —— thumb → full-res hand-off ————————————————————————————————————
   The grid only ever holds a 700px thumb (8MB across the whole strip instead of 63MB),
   so opening a frame fetches the real file. Rather than a blank wait, the thumb stands in
   upscaled from the first frame and the full one cross-fades over it once decoded — the
   picture is there instantly and simply sharpens. .fg-light centres by flex, so the
   stand-in is absolutely centred instead (inset+margin:auto) to sit exactly on top. */
.fg-light__ph { position: absolute; inset: 0; margin: auto; transition: opacity 0.45s var(--ease-out-expo), transform 0.6s var(--ease-out-expo); }
.fg-light__ph.is-out { opacity: 0; }
.fg-light__full { opacity: 0; transition: opacity 0.45s var(--ease-out-expo), transform 0.6s var(--ease-out-expo); }
.fg-light__full.is-in { opacity: 1; }
.fg-light.is-loading::after { content: "Loading full size"; position: absolute; left: 50%; bottom: clamp(1.4rem, 4vh, 3rem); transform: translateX(-50%);
  font-family: var(--mono); font-size: var(--t-cap); text-transform: uppercase; letter-spacing: 0.06em; color: #fff; mix-blend-mode: difference;
  animation: fgLoad 1.1s ease-in-out infinite; }
@keyframes fgLoad { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.9; } }

/* —— PHONES (≤760): the studies grid scrolls NATIVELY (compositor momentum) instead of the
   JS transform-drift. Native scroll is both smoother (no dropped-frame jitter) and faster-feeling
   (real fling momentum). js/v3-play-grid.js drops to a single column, un-doubled, and never starts
   the rAF loop or hijacks wheel/touch on mobile. Desktop keeps the eased cascade untouched. —— */
@media (max-width: 760px) {
  body { overflow: visible; }                                       /* override the base overflow:hidden → native VIEWPORT scroll (real touch momentum) */
  .stage04 { position: static; inset: auto; height: auto; min-height: 100dvh; overflow: clip visible;
    padding-top: calc(env(safe-area-inset-top, 0px) + 3.6rem); }   /* clip any horizontal spill, pass vertical to the document; clear the fixed header */
  .fgrid { position: static; inset: auto; display: block; }
  .fg-col { overflow: visible; }
  .fg-strip { position: static; transform: none !important; will-change: auto; }
}
