/*!
 * showcase.css — shared vignette CHROME + the size contract (FT-10564).
 * =============================================================================
 * The companion to showcase.js. Everything here was, until this file existed,
 * duplicated verbatim inside draft-showcase.html, draft-showcase-guests.html and
 * organizers.html. Only rules that were IDENTICAL in the pages that carried them
 * moved: each page keeps its own local deltas (organizers' `.demo` background +
 * font-family, both drafts' differing `.shot--mobile`, every page's
 * prefers-reduced-motion freeze) inline, AFTER this stylesheet, so the cascade
 * resolves exactly as it did before the extraction.
 *
 * LOAD ORDER IS LOAD-BEARING: link this file in <head> BEFORE the page's own
 * <style> block. Same-specificity page rules must still win.
 *
 * CONSUMERS
 *   - the three marketing pages (<link rel="stylesheet" href="showcase.css">)
 *   - the generated Vignette Gallery (scripts/build-vignette-gallery.ts links it)
 *   - the React wrapper ModuleVignette.tsx (injects the same <link> at runtime)
 *
 * The scene-specific skeleton CSS (`chat__`, `sched__`, `mapper__`… — one
 * namespace per vignette) is NOT here: it stays authored beside its markup in
 * the page that owns the scene, and the gallery generator lifts it per page,
 * scoped. This file is chrome and sizing only.
 */

/* ══ The shot frame ═══════════════════════════════════════════════════════════
   The framed fake-window a vignette lives inside (showcase-primer §2). */
.shot {
  position: relative;
  border-radius: 12px;
  border: 1px solid var(--line-strong);
  background: var(--surface);
  box-shadow: var(--frame-shadow);
  overflow: hidden;
}
.shot__img { width: 100%; height: auto; display: block; }
/* Placeholder shown until a real capture is wired into the manifest */
.shot__ph {
  display: flex; align-items: center; justify-content: center;
  min-height: 220px; aspect-ratio: 16 / 10;
  background: var(--surface);
  color: var(--faint);
  font-size: 0.8rem; font-weight: 500; letter-spacing: 0.04em;
  text-transform: lowercase;
}
.shot.is-loaded .shot__ph { display: none; }
/* Outer frame header = a tidy MODULE-NAME label (FT-5725 decision). Was a
   fake-browser window with macOS traffic-light dots; real Mixies modules
   have no such chrome, so it read wrong. Now: a small uppercased caption
   from data-label + a green dot marker, in the showcase's own design
   language. The demo's INNER chrome (the "# general" bar, the phone bezel)
   is part of the surface and stays — this only replaces the outer window. */
.shot--chrome { padding-top: 30px; }
.shot--chrome::before {
  content: attr(data-label);
  position: absolute; top: 0; left: 0; right: 0; height: 30px; z-index: 4;
  display: flex; align-items: center;
  padding: 0 0.85rem 0 1.55rem;
  background:
    radial-gradient(circle at 0.8rem 50%, var(--green) 0 3px, transparent 3.6px),
    color-mix(in srgb, var(--surface-2) 78%, var(--surface));
  border-bottom: 1px solid var(--line);
  font-family: 'Inter', sans-serif;
  font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted);
}
/* Center this scene's chrome label (grid-build centerpiece, sponsors-trio). The
   green dot marker stays anchored left; only the caption text centers. */
.shot--chrome.shot--label-center::before {
  justify-content: center;
  padding: 0 1.55rem;
  text-align: center;
}
/* A scene that reads as the real app SHELL (rail | sidebar | main), not a
   captioned single module — drops the outer chrome caption bar. */
.shot--nolabel { padding-top: 0; }
.shot--nolabel::before { display: none; }

/* ══ Demo tokens + base ══════════════════════════════════════════════════════
   One motion vocabulary across every surface demo. */
:root {
  --demo-ease: cubic-bezier(0.16, 1, 0.3, 1);
  /* Crisp pop with a hair of overshoot — used for messages arriving. */
  --demo-pop: cubic-bezier(0.22, 1.2, 0.36, 1);
  --demo-pad: clamp(0.7rem, 2.2vw, 1.05rem);
}
/* A live demo owns its frame — the placeholder is only a no-JS fallback. */
.shot[data-live="true"] .shot__ph { display: none; }
.demo { position: relative; width: 100%; overflow: hidden; }

/* ══ The size contract ═══════════════════════════════════════════════════════
   One authored scene, three sizes. `data-vig-size` on the `.vig` wrapper picks
   which; showcase.js publishes --vig-scale / --vig-h onto the same element.

     <div class="vig" data-vig-size="card">
       <div class="vig__inner"> <figure class="shot …"> … </figure> </div>
     </div>

   MARKETING IS THE INERT CASE — no fixed width, no transform, no height
   override — and a bare `<figure class="shot">` with no wrapper at all is
   identical to how the showcase pages rendered before the wrapper existed.
   That is what keeps the marketing render pixel-equivalent. */
.vig { position: relative; display: block; min-width: 0; }
.vig__inner { display: block; transform-origin: top left; }

/* Scaled sizes: the scene renders at its AUTHORED width (showcase.js sets the
   inline width from MixieDemo.SIZES) inside an absolutely-positioned box, then
   scales down to the container. Absolute positioning is deliberate — it takes
   the unscaled height out of layout, so the wrapper's height is exactly the
   engine-published --vig-h and never flashes at full size. */
.vig[data-vig-size="card"],
.vig[data-vig-size="seat"] {
  overflow: hidden;
  height: var(--vig-h, auto);
  min-height: var(--vig-min-h, 0px);
}
.vig[data-vig-size="card"] .vig__inner,
.vig[data-vig-size="seat"] .vig__inner {
  position: absolute; top: 0; left: 0;
  /* translate BEFORE scale, with transform-origin at the top left: the offsets
     are already in container pixels, so this places the scaled scene rather
     than scaling the offsets. Both default to 0px, which is the width-only
     fit's exact pre-FT-10934 rendering. */
  transform: translate(var(--vig-x, 0px), var(--vig-y, 0px)) scale(var(--vig-scale, 1));
}

/* ── contain fit (FT-10934) ──────────────────────────────────────────────────
   OPT-IN, via `data-vig-fit="contain"` on the same wrapper. A seat with a fixed
   height (the picker's art band, the detail seat, the tour bubble's demo frame)
   used to CROP any scene whose scaled height exceeded it, because the box's own
   height was the engine-published --vig-h and the inner box was pinned to the
   top left. In contain mode the SEAT owns the height and showcase.js fits the
   scene inside it — `scale = min(w/authoredW, h/authoredH)` — then centres it
   with --vig-x / --vig-y.

   `height: 100%` is what makes the observed height independent of the published
   one, which is what keeps the engine's ResizeObserver off a feedback loop. The
   min-height is a SAFETY NET only: a seat that opts in without a definite
   height of its own resolves `height: 100%` to auto (the inner box is out of
   flow, so that is 0), and the fallback makes it behave exactly like the
   width-only fit instead of collapsing. Declared after the size rules above so
   equal specificity resolves this way. */
.vig[data-vig-fit="contain"] {
  height: 100%;
  min-height: var(--vig-h, 0px);
}

/* Card = the §4.3 mini-demo. At card scale the 10px uppercase caption bar
   renders as unreadable mush, and the picker card carries the module's real
   name in house type right below the demo — so the chrome caption is dropped
   here and only here. The scene's own inner chrome is untouched. */
.vig[data-vig-size="card"] .shot--chrome { padding-top: 0; }
.vig[data-vig-size="card"] .shot--chrome::before { display: none; }

/* Reduced motion: showcase.js parks each scene on its `rest` beat and never
   starts a timeline; this only stops the wrapper's own fit transition. */
@media (prefers-reduced-motion: reduce) {
  .vig, .vig__inner { transition: none !important; }
}
