/* ============================================================
   LAB.CSS — the live-demo embed grid on /lab/.

   Loads after projects.css, so the shared card system (.proj-row,
   .proj-section-title, .reveal) still does the heavy lifting; this
   file only styles the iframe demo cards that sit between the
   section headings.

   PERF NOTE (same lesson as projects.css / card.css): every demo
   iframe contains a live animating canvas. Do NOT put a CSS
   gradient overlay (scanlines, vignette) on top of one and do NOT
   use filter: drop-shadow() here — compositing either over a live
   canvas stalls Chrome's rasterizer for seconds at a time. Hard
   box-shadows only.
   ============================================================ */

/* Explicit single -> two column switch rather than auto-fit: the
   breakpoint has to be knowable, because the two frames are sized
   differently on each side of it (see .demo-frame--* below). */
.lab-demos {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(2rem, 4vw, 3.5rem);
  margin: 4rem 0 1rem;
  align-items: start;
  justify-items: center;
}

.demo-card {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.4rem;
}

/* ---------- the framed embed ----------
   aspect-ratio holds the box before the iframe paints, so the grid
   doesn't reflow when a lazy demo finally loads. Each variant caps
   its own width: the portrait card is 9:16, so an unbounded width
   would make it absurdly tall next to the square one. */
/* No border or drop shadow here on purpose: each widget already draws
   its own outer edge (the raster sunset has a rounded orange frame of
   its own), so a second frame around it read as double-matting. The
   background still paints the box before a lazy iframe arrives. */
.demo-frame {
  width: 100%;
  background: #07060f;
  overflow: hidden;
}

.demo-frame--square { aspect-ratio: 1 / 1; }
.demo-frame--portrait { aspect-ratio: 9 / 16; }

/* STACKED (one column): match WIDTHS so the two embeds share the same
   left/right edges as you scroll past them. The portrait one just runs
   taller, which costs nothing in a vertical stack. */
.demo-frame { max-width: 420px; }

.demo-frame iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* ---------- caption ---------- */
.demo-cap {
  max-width: 44ch;
  text-align: center;
}

.demo-num {
  display: inline-block;
  font-family: vcr-osd-mono, ibm-plex-mono, monospace;
  font-size: 0.8rem;
  letter-spacing: 2px;
  background: #f9a392;
  color: #000;
  border: 2px solid #000;
  padding: 1px 8px;
  margin-bottom: 0.7rem;
}

.demo-title {
  margin: 0 0 0.6rem;
  font-family: vcr-osd-mono, ibm-plex-mono, monospace;
  font-weight: 400;
  font-size: clamp(1.05rem, 2.2vw, 1.35rem);
  letter-spacing: 3px;
  text-transform: uppercase;
  color: #fff;
  text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.85);
}

.demo-desc {
  margin: 0 0 0.8rem;
  font-family: ibm-plex-mono, ui-monospace, monospace;
  font-size: 0.85rem;
  line-height: 1.65;
  color: rgba(255, 255, 255, 0.74);
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.85);
}

.demo-tag {
  font-family: ibm-plex-mono, ui-monospace, monospace;
  font-size: 0.72rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #f9a392;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.85);
}

/* ---------- full-width cabinet embed (Estate Jewelry Box) ----------
   The React/Three.js widget inside is a wide cinematic stage: it sizes
   itself to ~86% of the iframe's height (min 580px, cap 900px) and caps
   its own width at 1680px, so the frame gets an explicit HEIGHT instead
   of an aspect-ratio — a ratio would let the widget's internal minimum
   overflow a short frame and clip (the iframe is scrolling="no").
   Below 720px of iframe width the widget switches to a fixed 760px
   height, so the mobile frame is tall enough to hold it plus the demo
   page's own padding. */
.demo-card--wide {
  width: 100%;
  margin-top: clamp(2rem, 4vw, 3.5rem);
}

.demo-frame--cabinet {
  max-width: none;
  height: min(82vh, 900px);
  min-height: 660px;
  /* the widget document is fully transparent, so no fill here either:
     the box + its two floating blocks composite straight onto the
     page's starfield instead of sitting in a visible square */
  background: transparent;
}

@media (max-width: 780px) {
  /* widget is 560px tall on narrow iframes + ~40px of demo-page padding */
  .demo-frame--cabinet {
    height: 600px;
    min-height: 600px;
    transition: height 380ms ease;
    /* Full-bleed: reclaim .proj-wrap's horizontal padding. The cabinet renders
       wider than the stage on a thin phone and gets clipped at the stage edge,
       so that padding read as black bars cutting into the box. The frame is a
       centered flex item, so making it wider than its container simply
       overflows evenly on both sides — no margin/transform trickery. */
    width: calc(100% + 2 * clamp(1rem, 4vw, 3rem));
    max-width: none;
  }

  /* Contain that overflow so it can't produce a horizontal scrollbar. `clip`
     rather than `hidden` on purpose: hidden would make this a scroll
     container, clip does not. */
  .proj-wrap { overflow-x: clip; }
  /* The widget grows its own stage to 700px while open (it needs the room for
     the raised lid — the status card and control are pinned to the phone
     stage's edges and can't move apart). The iframe has to grow with it or the
     extra height is simply clipped. Driven by the same focus class, so both
     transitions run together with the background fade. */
  html.jbw-focus .demo-frame--cabinet { height: 740px; min-height: 740px; }
}

@media (max-width: 780px) and (prefers-reduced-motion: reduce) {
  .demo-frame--cabinet { transition: none; }
}

/* ============================================================
   FOCUS MODE — see focus-mode.js. While the cabinet is open the
   animated hero fades down to .page-hero's flat #07060f so the only
   thing moving on the page is the widget itself.

   Fading the CANVAS's own opacity (rather than fading in a new cover
   element over it) is deliberate: the canvas already owns a
   compositing layer, so this animates an existing layer instead of
   stacking a fresh one on top of a live canvas — the exact pattern
   that stalls this machine's rasterizer for seconds (see the perf
   note at the top of this file).

   visibility flips only after the fade so the CRT overlays stop being
   composited at all while focused, rather than sitting there at
   opacity 0 still costing a layer.
   ============================================================ */
.page-hero canvas,
.hero-scanlines,
.hero-vignette {
  transition: opacity 380ms ease;
}

html.jbw-focus .page-hero canvas {
  opacity: 0;
}

html.jbw-focus .hero-scanlines,
html.jbw-focus .hero-vignette {
  opacity: 0;
  visibility: hidden;
  transition: opacity 380ms ease, visibility 0s linear 380ms;
}

@media (prefers-reduced-motion: reduce) {
  .page-hero canvas,
  .hero-scanlines,
  .hero-vignette { transition: none; }
}

/* SIDE BY SIDE (two columns): match HEIGHTS instead. 420 * 9/16 = 236,
   so the 9:16 frame ends level with the 1:1 one and neither column
   trails a long empty gap under it. */
@media (min-width: 1000px) {
  .lab-demos { grid-template-columns: 1fr 1fr; }
  .demo-frame--portrait { max-width: 236px; }
}

/* ============================================================
   MOBILE TIGHTENING — the 9:16 demo is inherently tall (its frame
   alone is ~636px at phone width). Trimming the caption's internal
   spacing keeps the whole card inside a single screen instead of
   spilling a few pixels past it.
   ============================================================ */
@media (max-width: 560px) {
  .demo-card { gap: 1rem; }
  .demo-num { margin-bottom: 0.5rem; }
  .demo-title { margin-bottom: 0.45rem; }
  .demo-desc { margin-bottom: 0.55rem; }
}
