/* Lab-specific styles for The Color of Time.
   Paired with frontend/lab.html. The shared scroll primitives ([data-reveal], and the .glow-instrument
   warm-up / .glow-instrument-flicker settle keyframes on the wordmark) live in motion.css and must not be
   duplicated here. Everything below is the Lab entrance splash — a "colorimeter warming up" boot sequence
   shown once per page load — plus the extra, Lab-only keyframes that layer over the shared warm-up.

   Timeline (normal motion), tuned so the ramp reads as an instrument powering on rather than a flat fade
   (extended 2026-07-03 per owner request — a longer, richer boot):
     0.00s  panel dark; ambient power-on wash begins; a faint graticule grid develops over the field
     0.00s  "LAB" ignites ember->bright (motion.css .glow-instrument, 2.2s ease-out) over an ember halo
     0.00s  eyebrow tracks in; boot bar starts a stepped POST-style fill; status reads INITIALIZING
     0.35s  first calibration sweep crosses the field left->right
     1.20s  status -> CALIBRATING (CSS step, so no JS re-render can restart the warm-up)
     2.00s  pigment-library check: six specimen chips blink on one by one under the readout
     2.50s  a second, fainter return sweep crosses right->left
     3.90s  halo strikes + settles (tube-strike flicker); status -> READY; bar completes
     4.55s  overlay blooms (scale) + fades out, revealing the settled tool underneath
     5.30s  overlay unmounts (JS sc-if) — nothing left to block the tool
   Reduced motion: the same frame, but rendered already-settled with no ramp/sweep/flicker/scale — only a
   short opacity cross-fade remains (a low-vestibular-risk substitution, the recommended reduced-motion
   pattern) so the tool is revealed without a blank flash or forced animation. */

.lab-entrance {
  /* Single source of truth for the boot teardown schedule: lab.html reads these off computed style so
     its fade/unmount timers stay in sync with this CSS. Reduced-motion overrides live in the @media
     (prefers-reduced-motion: reduce) block below. */
  --entrance-fade-at: 4550ms;
  --entrance-gone-at: 5300ms;
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  overflow: hidden;
  background: var(--color-card-dark, #1B1712);
  opacity: 1;
  transform: none;
  transition:
    opacity .62s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)),
    transform .62s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1));
}
/* ambient power-on wash rising from behind the wordmark (kept faint; the halo does the tight glow) */
.lab-entrance::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(128vw, 960px);
  height: min(128vw, 960px);
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(200, 69, 59, .14), rgba(200, 69, 59, 0) 62%);
  opacity: 0;
  pointer-events: none;
  animation: labEntranceAmbient 3.2s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) both;
}
/* faint graticule grid developing over the panel — the instrument's measurement field waking up.
   Line alpha is .05, so even at full animation opacity this stays a whisper behind the core. */
.lab-entrance::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(0deg, rgba(237, 228, 210, .05) 0 1px, transparent 1px 44px),
    repeating-linear-gradient(90deg, rgba(237, 228, 210, .05) 0 1px, transparent 1px 44px);
  -webkit-mask-image: radial-gradient(ellipse 72% 62% at 50% 50%, #000 28%, transparent 76%);
  mask-image: radial-gradient(ellipse 72% 62% at 50% 50%, #000 28%, transparent 76%);
  opacity: 0;
  animation: labEntranceGrid 4.6s var(--motion-ease-develop, cubic-bezier(.3, .05, .25, 1)) .2s both;
}
/* fade-out reads as the instrument "opening" into the tool: a slight bloom + fade, not a flat cut */
.lab-entrance.is-fading {
  opacity: 0;
  transform: scale(1.045);
}

/* one calibration sweep across the panel (a thin lit band that passes once) */
.lab-entrance-scan {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg,
    transparent 44%,
    rgba(237, 228, 210, .05) 49%,
    rgba(200, 69, 59, .10) 50%,
    rgba(237, 228, 210, .05) 51%,
    transparent 56%);
  background-size: 260% 100%;
  animation: labEntranceScan 2.1s cubic-bezier(.5, .05, .3, 1) .35s both;
}
/* the return pass: fainter, opposite direction, mid-boot — a raster scanning back to home position */
.lab-entrance-scan-return {
  background: linear-gradient(75deg,
    transparent 44%,
    rgba(237, 228, 210, .04) 49%,
    rgba(200, 69, 59, .07) 50%,
    rgba(237, 228, 210, .04) 51%,
    transparent 56%);
  background-size: 260% 100%;
  animation: labEntranceScanReturn 1.9s cubic-bezier(.5, .05, .3, 1) 2.5s both;
}

.lab-entrance-core {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(13px, 2.2vw, 22px);
}

.lab-entrance-eyebrow {
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: clamp(10px, 1.3vw, 12px);
  letter-spacing: .42em;
  text-transform: uppercase;
  color: rgba(237, 228, 210, .44);
  padding-left: .42em; /* re-center against the trailing tracking */
  animation: labEntranceRise .8s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) both;
}

.lab-entrance-mark-wrap {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* persistent ember glow behind the wordmark. It is NOT a .glow-instrument class, so reduced-motion's
   neutralization of those does not strip it — the mark still reads as "lit" with animation off. */
.lab-entrance-halo {
  position: absolute;
  left: 50%;
  top: 52%;
  width: 122%;
  height: 152%;
  transform: translate(-50%, -50%);
  background: radial-gradient(ellipse at center,
    rgba(200, 69, 59, .38),
    rgba(200, 69, 59, .10) 45%,
    rgba(200, 69, 59, 0) 70%);
  filter: blur(6px);
  pointer-events: none;
  animation:
    labEntranceHalo 2.2s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) both,
    labEntranceStrike .9s steps(1, end) 3.9s 1 both;
}

.lab-entrance-mark {
  position: relative;
  font-family: var(--ff-display, 'Fraunces', serif);
  font-weight: 600;
  font-size: clamp(66px, 17vw, 156px);
  line-height: .88;
  letter-spacing: .06em;
  color: #F6EFE0;
}

.lab-entrance-readout {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: clamp(9.5px, 1.2vw, 11px);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: rgba(200, 69, 59, .82);
  animation: labEntranceRise .8s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) .25s both;
}
.lab-entrance-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent, #C8453B);
  animation: statusPulse 1.05s ease-in-out infinite; /* statusPulse defined in lab.html's inline <style> */
}
/* status steps INITIALIZING -> CALIBRATING -> READY. The three phases are localized text nodes
   ({{ lx.entranceInit }} / {{ lx.entranceCalibrating }} / {{ lx.entranceReady }}) stacked in one grid cell
   (so the readout width holds at the widest word and the dot never shifts) and crossfaded purely in CSS —
   no JS re-render is involved, so the warm-up animations can never be interrupted. */
.lab-entrance-status { display: grid; justify-items: start; }
.lab-entrance-phase { grid-area: 1 / 1; white-space: nowrap; opacity: 0; }
.les-init  { animation: labPhaseInit  4.5s linear both; }
.les-cal   { animation: labPhaseCal   4.5s linear both; }
.les-ready { animation: labPhaseReady 4.5s linear both; }

.lab-entrance-bar {
  width: clamp(120px, 20vw, 190px);
  height: 2px;
  border-radius:3px;
  overflow: hidden;
  background: rgba(237, 228, 210, .12);
  animation: labEntranceRise .8s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) .35s both;
}
.lab-entrance-bar-fill {
  display: block;
  width: 0;
  height: 100%;
  background: linear-gradient(90deg, #7A2B25, var(--color-accent, #C8453B));
  box-shadow: 0 0 8px rgba(200, 69, 59, .55);
  /* linear on purpose: the stepped plateaus in the keyframes are the character; an eased
     timing function on top would smear the checkpoint holds. */
  animation: labEntranceBar 3.9s linear both;
}

/* pigment-library check: six specimen chips (cinnabar, orpiment, malachite, azurite,
   ultramarine, lead white) blink on one by one — the Lab loading its palette. Decorative
   (aria-hidden); colors are approximate sRGB of the six default-card pigments. */
.lab-entrance-chips {
  display: flex;
  gap: 8px;
  animation: labEntranceRise .8s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) .45s both;
}
.lab-entrance-chips i {
  width: 13px;
  height: 18px;
  border-radius:5px;
  corner-shape: squircle;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .35), 0 0 10px rgba(200, 69, 59, .18);
  opacity: 0;
  animation: labChipPop .5s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) both;
}
.lab-entrance-chips i:nth-child(1) { background: #B3452F; animation-delay: 2.00s; }
.lab-entrance-chips i:nth-child(2) { background: #E3B84B; animation-delay: 2.22s; }
.lab-entrance-chips i:nth-child(3) { background: #55925C; animation-delay: 2.44s; }
.lab-entrance-chips i:nth-child(4) { background: #33607E; animation-delay: 2.66s; }
.lab-entrance-chips i:nth-child(5) { background: #3D4C9E; animation-delay: 2.88s; }
.lab-entrance-chips i:nth-child(6) { background: #EFE9DA; animation-delay: 3.10s; }

@keyframes labEntranceAmbient {
  0%   { opacity: 0; }
  60%  { opacity: 1; }
  100% { opacity: .72; }
}
@keyframes labEntranceHalo {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.7); }
  55%  { opacity: 1; }
  100% { opacity: .9; transform: translate(-50%, -50%) scale(1); }
}
/* fluorescent-tube strike: a couple of quick dips right after the ramp, then it holds steady */
@keyframes labEntranceStrike {
  0%   { opacity: .9; }
  12%  { opacity: .5; }
  24%  { opacity: 1; }
  42%  { opacity: .62; }
  60%  { opacity: 1; }
  100% { opacity: .9; }
}
@keyframes labEntranceScan {
  0%   { background-position: 170% 0; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { background-position: -70% 0; opacity: 0; }
}
@keyframes labEntranceScanReturn {
  0%   { background-position: -70% 0; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { background-position: 170% 0; opacity: 0; }
}
@keyframes labEntranceGrid {
  0%   { opacity: 0; }
  30%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { opacity: .55; }
}
@keyframes labChipPop {
  0%   { opacity: 0; transform: translateY(5px) scale(.6); }
  55%  { opacity: 1; transform: translateY(0) scale(1.14); }
  100% { opacity: 1; transform: none; }
}

/* ---------- ΔE-over-a-century chart: one draw-in on page load ----------
   The curve sweeps left→right like a plotter pen (pathLength="1" on the SVG path makes the
   dash trick length-independent), the fill develops behind it, the year marker pops last.
   Mount-keyed and one-shot (`both`): reactive updates afterwards repaint instantly. */
.lab-century-path {
  stroke-dasharray: 1;
  animation: labCenturyDraw 1.4s var(--motion-ease-develop, cubic-bezier(.3, .05, .25, 1)) .5s both;
}
.lab-century-area { animation: labCenturyFill .8s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) 1.5s both; }
.lab-century-dot {
  transform-box: fill-box; transform-origin: center;
  animation: labCenturyDot .45s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) 1.7s both;
}
@keyframes labCenturyDraw { 0% { stroke-dashoffset: 1; } 100% { stroke-dashoffset: 0; } }
@keyframes labCenturyFill { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes labCenturyDot {
  0%   { opacity: 0; transform: scale(.3); }
  70%  { opacity: 1; transform: scale(1.25); }
  100% { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .lab-century-path, .lab-century-area, .lab-century-dot {
    animation: none !important; opacity: 1 !important; transform: none !important;
    stroke-dasharray: none !important;
  }
}
@keyframes labEntranceRise {
  0%   { opacity: 0; letter-spacing: .7em; transform: translateY(5px); }
  100% { opacity: 1; }
}
@keyframes labEntranceBar {
  0%   { width: 0; }
  100% { width: 100%; }
}
/* status phase crossfades (quick opacity ramps over ~0.18s at each hand-off; ready holds via `both`) */
@keyframes labPhaseInit  { 0% { opacity: 1; } 30% { opacity: 1; } 37% { opacity: 0; } 100% { opacity: 0; } }
@keyframes labPhaseCal   { 0% { opacity: 0; } 30% { opacity: 0; } 37% { opacity: 1; } 79% { opacity: 1; } 86% { opacity: 0; } 100% { opacity: 0; } }
@keyframes labPhaseReady { 0% { opacity: 0; } 79% { opacity: 0; } 86% { opacity: 1; } 100% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  /* considered static fallback: the settled instrument frame (LAB lit, status shown), with every ramp /
     sweep / flicker / scale removed. Only a short opacity cross-fade survives to reveal the tool —
     coherent and branded, never blank, and free of the motion that reduced-motion opts out of. */
  .lab-entrance {
    --entrance-fade-at: 650ms;
    --entrance-gone-at: 950ms;
    transition: opacity .3s linear !important;
    transform: none !important;
  }
  .lab-entrance.is-fading { transform: none !important; }
  .lab-entrance::before { animation: none !important; opacity: .7 !important; }
  .lab-entrance-scan { display: none !important; }
  .lab-entrance-eyebrow,
  .lab-entrance-readout,
  .lab-entrance-bar {
    animation: none !important;
    opacity: 1 !important;
    letter-spacing: .42em;
    transform: none !important;
  }
  .lab-entrance-halo {
    animation: none !important;
    opacity: .9 !important;
    transform: translate(-50%, -50%) scale(1) !important;
  }
  .lab-entrance-bar-fill { animation: none !important; width: 100% !important; }
  .lab-entrance-dot { animation: none !important; }
  /* settled status: show only the final phase, no crossfade */
  .lab-entrance-phase { animation: none !important; opacity: 0 !important; }
  .les-ready { opacity: 1 !important; }
  /* new boot dressing renders settled: graticule faint and steady, all six chips lit */
  .lab-entrance::after { animation: none !important; opacity: .5 !important; }
  .lab-entrance-chips { animation: none !important; opacity: 1 !important; }
  .lab-entrance-chips i { animation: none !important; opacity: 1 !important; transform: none !important; }
}

/* =====================================================================================
   Analyzing state (analysis===null && analyzing===true): a thin scan sweep + two shimmer
   skeleton rows (where the detected type / materials will land) + a stepped status line
   cycling anaS1 -> anaS2 -> anaS3, reusing the Lab-boot readout's technique (three
   absolutely-stacked spans, opacity keyframed on a shared timeline) but looped FOREVER
   (`infinite`, unlike the boot's one-shot phases) since analyze time is unbounded, and on
   a longer 5.4s cycle so it reads as patient rather than jittery. Every animation here is
   pure CSS and keyed only to the `.lab-analyzing` block's mount, so a re-render while
   analyzing stays true (a slider tweak, a language switch) can never reset the cycle.
   ===================================================================================== */
.lab-analyzing { position: relative; overflow: hidden; border-radius:14px; }
/* Raster scan (lead-authored): a horizontal beam with a bright leading edge and a faint
   vermilion exposure trail, sweeping top->bottom like an instrument reading the specimen
   line by line. One pass per 1.8s, so the 5.4s status cycle gets exactly three sweeps per
   phase — the two rhythms beat together instead of drifting. */
.lab-analyzing-scan {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}
.lab-analyzing-scan::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -46%;
  height: 46%;
  background: linear-gradient(180deg,
    rgba(200, 69, 59, 0) 0%,
    rgba(200, 69, 59, .035) 52%,
    rgba(200, 69, 59, .09) 84%,
    rgba(237, 228, 210, .26) 96%,
    rgba(200, 69, 59, .5) 99.4%,
    rgba(200, 69, 59, 0) 100%);
  animation: labAnaScan 1.8s var(--motion-ease-mechanical, cubic-bezier(.45, .05, .55, .95)) infinite;
}
/* Skeleton shimmer (lead-authored): two rows shaped like the text they stand in for —
   different widths, out-of-phase pulses — so it reads as a pending sentence, not two bars. */
.lab-shimmer-row {
  height: 13px;
  width: 84%;
  border-radius:9px;
  margin-bottom: 8px;
  background: linear-gradient(90deg,
    rgba(34, 29, 23, .06) 25%,
    rgba(34, 29, 23, .15) 37%,
    rgba(34, 29, 23, .06) 63%);
  background-size: 400% 100%;
  animation: labShimmer 1.7s var(--motion-ease-mechanical, cubic-bezier(.45,.05,.55,.95)) infinite;
}
.lab-shimmer-row + .lab-shimmer-row {
  width: 58%;
  margin-bottom: 0;
  animation-delay: -.85s;
}
.lab-analyzing-title {
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: 9px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #9a8b73;
  margin: 13px 0 4px;
}
.lab-analyzing-status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: rgba(34, 29, 23, .68);
}
.lab-ana-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--color-accent, #C8453B);
  animation: statusPulse 1s ease-in-out infinite; /* statusPulse defined in lab.html's inline <style> */
}
/* Stepped-phase roll (lead-authored): three phases stacked in one grid cell (readout width
   holds at the widest word). Each phase enters rising from 5px below and exits lifting 5px
   up — a vertical roll, the way a flip-board or instrument readout advances — instead of a
   plain crossfade. Phase 1 starts visible at mount for instant feedback; its wrap-around
   re-entry (94%→100%) repositions below-baseline on a 0.1% jump-cut the eye can't see
   because opacity is 0 through it. */
.lab-ana-cycle { position: relative; display: inline-grid; }
.lab-ana-phase { grid-area: 1 / 1; white-space: nowrap; opacity: 0; transform: translateY(5px); }
.lap-1 { animation: labAnaPhase1 5.4s var(--motion-ease-roll, cubic-bezier(.3, .1, .3, 1)) infinite; }
.lap-2 { animation: labAnaPhase2 5.4s var(--motion-ease-roll, cubic-bezier(.3, .1, .3, 1)) infinite; }
.lap-3 { animation: labAnaPhase3 5.4s var(--motion-ease-roll, cubic-bezier(.3, .1, .3, 1)) infinite; }
.lab-ana-static { display: none; }   /* reduced-motion fallback: plain "…" beside the pulsing dot */

/* Raster-scan travel: band starts fully above the panel (top:-46%) and exits fully below.
   318% of its own height ≈ (100% + 46%) / 46% of the container. */
@keyframes labAnaScan {
  0%   { transform: translateY(0); opacity: 0; }
  8%   { opacity: 1; }
  90%  { opacity: 1; }
  100% { transform: translateY(318%); opacity: 0; }
}
@keyframes labShimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0 0; }
}
@keyframes labAnaPhase1 {
  0%, 27%    { opacity: 1; transform: none; }
  33%, 93.9% { opacity: 0; transform: translateY(-5px); }
  94%        { opacity: 0; transform: translateY(5px); }
  100%       { opacity: 1; transform: none; }
}
@keyframes labAnaPhase2 {
  0%, 33%   { opacity: 0; transform: translateY(5px); }
  39%, 60%  { opacity: 1; transform: none; }
  66%, 100% { opacity: 0; transform: translateY(-5px); }
}
@keyframes labAnaPhase3 {
  0%, 66%   { opacity: 0; transform: translateY(5px); }
  72%, 94%  { opacity: 1; transform: none; }
  100%      { opacity: 0; transform: translateY(-5px); }
}

/* ---------- analysis result rows: a 3-step stagger as the real detection replaces the skeleton ---------- */
@keyframes labResultIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.lab-result-row { animation: labResultIn .42s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) both; }

/* ---------- development sweep: a darkroom-style wipe played once per settled backend result ----------
   Restarted from JS via classList remove/reflow/add (see lab.html's _restartDevSweep), because
   re-adding the same class name doesn't otherwise retrigger a CSS animation. */
.lab-dev-sweep {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* no z-index on purpose: as a z-auto positioned box it paints above the two canvases (earlier
     siblings) but below every later positioned sibling — corner labels, divider, handle, busy
     pill — which an explicit positive z-index here would wrongly cover. */
  opacity: 0;
  /* Chromatic development band (lead-authored): a tight cream core with a vermilion approach
     above and an ochre wash decaying below — the plate "develops" behind the band as it
     passes, echoing the fresh→aged palette the viewer itself is showing. */
  background: linear-gradient(180deg,
    rgba(20, 12, 8, 0) 0%,
    rgba(200, 69, 59, .07) 38%,
    rgba(200, 69, 59, .15) 46%,
    rgba(247, 241, 227, .32) 50%,
    rgba(215, 163, 60, .13) 56%,
    rgba(215, 163, 60, .05) 66%,
    rgba(20, 12, 8, 0) 100%);
}
.lab-dev-sweep.run { animation: labDevSweep .85s var(--motion-ease-develop, cubic-bezier(.3, .05, .25, 1)); }
@keyframes labDevSweep {
  0%   { opacity: 0; transform: translateY(-62%); }
  12%  { opacity: 1; }
  70%  { opacity: 1; transform: translateY(58%); }
  100% { opacity: 0; transform: translateY(102%); }
}

/* ---------- year-scrubber thumb: box-shadow glow tied to the live severity color (verdictColor) ---------- */
.lab-year-thumb { transition: box-shadow .3s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)); }

@media (prefers-reduced-motion: reduce) {
  .lab-analyzing-scan, .lab-shimmer-row { display: none !important; }
  .lab-ana-cycle { display: none !important; }
  .lab-ana-static { display: inline !important; }
  .lab-dev-sweep.run { animation: none !important; opacity: 0 !important; }
}

/* ==================== instrument layout (2026-07-03 redesign) ====================
   The before/after viewer owns the wide main column and stays put (sticky) while every
   reading and control lives in a compact rail of collapsible cards on its right. The
   accordion animates grid-template-rows (0fr <-> 1fr): the inner row clips its content,
   so a closed card takes exactly the header's height. This is a UI state transition,
   not scroll-driven motion, so the transform/opacity-only rule for scroll motion does
   not apply; under reduced motion the page-level rule zeroes the transition anyway. */
.lab-grid { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
.lab-main { flex: 2.5 1 560px; min-width: min(100%, 360px); position: sticky; top: 72px; }
.lab-rail { flex: 1 1 300px; min-width: 280px; max-width: 440px; display: flex; flex-direction: column; gap: 10px; }
@media (max-width: 980px) {
  .lab-main { position: static; }        /* stacked: the viewer leads, the rail follows */
  .lab-rail { max-width: none; }
}

.lab-acc { background: #F7F1E3; border: 1px solid rgba(34, 29, 23, .1); border-radius:22px; corner-shape: squircle; }
.lab-acc-head { display: flex; align-items: center; gap: 10px; width: 100%; background: transparent; border: none; cursor: pointer; padding: 11px 14px; font-family: var(--ff-mono); text-align: left; color: inherit; border-radius: inherit; }
.lab-acc-head:hover .lab-acc-eyebrow { color: #6d604c; }
.lab-acc-eyebrow { flex: 1; min-width: 0; font-size: 10px; font-weight: 600; letter-spacing: .13em; text-transform: uppercase; color: #8a7d68; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color .2s ease; }
.lab-acc-meta { flex-shrink: 1; min-width: 0; max-width: 50%; font-size: 11px; color: #8a7d68; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lab-acc-chev { flex-shrink: 0; font-size: 9px; line-height: 1; color: rgba(34, 29, 23, .45); transition: transform .28s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)); }
.lab-acc-head[aria-expanded="false"] .lab-acc-chev { transform: rotate(-90deg); }
.lab-acc-body { display: grid; grid-template-rows: 1fr; transition: grid-template-rows .32s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)); }
.lab-acc-body.is-closed { grid-template-rows: 0fr; }
.lab-acc-inner { min-height: 0; overflow: clip; }
/* padding lives INSIDE the clipped row so a closed card collapses to zero, not to its padding */
.lab-acc-pad { border-top: 1px solid rgba(34, 29, 23, .08); padding: 12px 14px 14px; }
/* the thin rule between the timeline and the four dose sliders (one shared card, owner request) */
.lab-acc-rule { border-top: 1px solid rgba(34, 29, 23, .1); margin: 12px 0; }

/* ==================== the stage: tall works scroll inside their own frame (2026-07-09) ====
   The viewer canvas keeps full width for every aspect ratio (no letterboxing), but the frame
   caps its height; anything taller — hanging scrolls, handscrolls shot upright — scrolls
   INSIDE the box. overscroll-behavior:contain stops the page from moving when the wheel hits
   either end. Corner tags and the busy pill sit in zero-height sticky layers so they stay
   pinned to the frame; the divider handle is re-centered from JS (--lab-handle-y) so the grab
   point rides the visible middle instead of scrolling away with the content. */
.lab-stage {
  position: relative;
  max-height: clamp(300px, calc(100vh - 240px), 860px);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  border-radius:18px;
  corner-shape: squircle;
  background: #120e0a;
  scrollbar-width: thin;
  scrollbar-color: rgba(237, 228, 210, .35) transparent;
}
.lab-stage::-webkit-scrollbar { width: 8px; }
.lab-stage::-webkit-scrollbar-thumb { background: rgba(237, 228, 210, .28); border-radius:5px; }
.lab-stage::-webkit-scrollbar-track { background: transparent; }
.lab-stage-inner {
  position: relative;
  width: 100%;
  cursor: ew-resize;
  /* pan-y (not none): vertical touch gestures scroll the frame, horizontal ones drag the seam */
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  outline: none;
}
.lab-stage-pin { position: sticky; left: 0; height: 0; z-index: 3; pointer-events: none; }
.lab-stage-pin-top { top: 0; }
.lab-stage-pin-bot { bottom: 0; }
.lab-stage-chip {
  position: absolute;
  top: 9px;
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: 9.5px;
  letter-spacing: .08em;
  color: #EDE4D2;
  background: rgba(20, 12, 8, .5);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  padding: 3px 7px;
  border-radius:8px;
  corner-shape: squircle;
}
.lab-stage-handle {
  position: absolute;
  top: var(--lab-handle-y, 50%);
  transform: translate(-50%, -50%);
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: #EDE4D2;
  border: 2px solid var(--color-accent, #C8453B);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .5);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: 12px;
  color: var(--color-accent, #C8453B);
}
.lab-stage-pill {
  position: absolute;
  left: 50%;
  bottom: 10px;
  transform: translateX(-50%);
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: 9.5px;
  letter-spacing: .06em;
  color: #EDE4D2;
  background: rgba(20, 12, 8, .62);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  padding: 4px 11px;
  border-radius:11px;
  corner-shape: squircle;
  display: flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
}
.lab-stage-pill-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent, #C8453B);
  animation: statusPulse 1s ease-in-out infinite; /* statusPulse lives in lab.html's inline <style> */
}
/* the hint only exists while the work is actually taller than the frame; it retires after the
   first scroll (JS adds .is-scrolled) rather than nagging */
.lab-stage-hint {
  position: absolute;
  right: 10px;
  bottom: 10px;
  display: none;
  align-items: center;
  gap: 6px;
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: 9px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #EDE4D2;
  background: rgba(20, 12, 8, .55);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  padding: 3px 8px;
  border-radius:8px;
  corner-shape: squircle;
  opacity: .92;
  transition: opacity .45s linear;
}
.lab-stage.is-scrollable .lab-stage-hint { display: inline-flex; }
.lab-stage.is-scrolled .lab-stage-hint { opacity: 0; }

/* ==================== called cards: the rail's raking-light pulse (2026-07-09) ====
   _callCard/_ringCard add .is-called for one beat when a card is summoned — by the environment
   ask, by detection starting, or as the quiet nudge when a new specimen arrives and the visitor
   has already chosen a world. Box-shadow only: no layout, safe mid-transition. */
.lab-acc.is-called { animation: labCardCall 1.6s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) both; }
@keyframes labCardCall {
  0%   { box-shadow: 0 0 0 0 rgba(200, 69, 59, 0); }
  20%  { box-shadow: 0 0 0 2px rgba(200, 69, 59, .55), 0 0 28px 3px rgba(200, 69, 59, .2); }
  100% { box-shadow: 0 0 0 0 rgba(200, 69, 59, 0); }
}

/* ---------- detection on demand: the invitation that replaced the auto-run ---------- */
.lab-detect-invite {
  border: 1.5px dashed rgba(34, 29, 23, .25);
  border-radius:16px;
  corner-shape: squircle;
  padding: 12px;
  background: rgba(34, 29, 23, .02);
}
.lab-detect-invite-title {
  font-family: var(--ff-display, 'Fraunces', serif);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -.01em;
}
.lab-detect-invite-body {
  font-size: 11.5px;
  line-height: 1.5;
  color: rgba(34, 29, 23, .68);
  margin: 5px 0 10px;
}
.lab-detect-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--color-accent, #C8453B);
  color: #F7F1E3;
  border: none;
  border-radius:14px;
  corner-shape: squircle;
  padding: 8px 13px;
  cursor: pointer;
  font-family: var(--ff-mono, 'IBM Plex Mono', monospace);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .05em;
}
.lab-detect-btn:hover { background: var(--color-accent-hover, #b23a31); }
.lab-detect-btn:focus-visible { outline: 2px solid var(--color-accent, #C8453B); outline-offset: 2px; }

/* ---------- the environment ask: the case opens itself and puts the question ---------- */
.lab-acc.is-asking .lab-acc-eyebrow,
.lab-acc.is-asking .lab-acc-meta { color: var(--color-accent, #C8453B); }
.lab-env-prompt {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 10px 11px;
  margin-bottom: 11px;
  border: 1px solid rgba(200, 69, 59, .35);
  background: rgba(200, 69, 59, .06);
  border-radius:15px;
  corner-shape: squircle;
}
.lab-env-prompt-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-accent, #C8453B);
  flex-shrink: 0;
  margin-top: 5px;
  animation: statusPulse 1.6s ease-in-out infinite;
}
.lab-env-prompt-title {
  font-family: var(--ff-display, 'Fraunces', serif);
  font-weight: 600;
  font-size: 15.5px;
  letter-spacing: -.01em;
}
.lab-env-prompt-body {
  font-size: 11.5px;
  line-height: 1.5;
  color: rgba(34, 29, 23, .7);
  margin-top: 3px;
}
/* while the question is open the five worlds present themselves one by one */
.lab-env-ask .lab-env-item { animation: labEnvOffer .5s var(--motion-ease-presence, cubic-bezier(.16, 1, .3, 1)) both; }
@keyframes labEnvOffer {
  from { opacity: 0; transform: translateX(-7px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .lab-acc.is-called { animation: none !important; }
  .lab-env-ask .lab-env-item { animation: none !important; }
  .lab-env-prompt-dot, .lab-stage-pill-dot { animation: none !important; }
  .lab-stage-hint { transition: none !important; }
}
