/* The Zen of Venn — viewer stage.
 * Colors and easing curves arrive as CSS custom properties set by
 * src/viewer/theme.ts from @zov/core's tokens.ts — see that file's
 * comment for why no hex digit belongs in this file.
 *
 * No `var(--zov-*, #hex)` fallback values here on purpose (Bianca's gate
 * on PR #83: an earlier version of this file carried six hand-copied hex
 * literals as fallbacks — `guards/scan.ts`'s HEX_GLOBS doesn't scan
 * `.css`, so nothing would have caught them drifting from tokens.ts).
 * Every var() below is left to resolve against ITS OWN property's CSS
 * initial value until theme.ts's `applyTheme()` sets the real one — that
 * call happens at module-eval time in a `type="module"` script, which
 * runs before `DOMContentLoaded` and, on this page's own markup (no
 * blocking stylesheet delaying the parser, no synchronous script ahead
 * of it), before the browser's first paint. The residual risk of a
 * fallback-free var() (an unstyled flash if a future markup change
 * introduces something that paints before the module script runs) is
 * judged smaller than the guaranteed-forever risk of a second,
 * guard-blind copy of the palette. */

/* BALOO IS SERVED FROM HERE, and until 2026-09-03 it was not served at
 * all (issue 304). This file named 'Baloo 2' first in its stack, and
 * `renderVennSvg` named it first in the `font-family` it writes on every
 * label — and nothing anywhere fetched the face. Every shared venn opened
 * in a browser was painted in system-ui: a stranger's first sight of the
 * toy, in the wrong voice, with labels fitted against Baloo's advances
 * and drawn with somebody else's.
 *
 * SELF-HOSTED, NOT A CDN. There is no third-party font host here on
 * purpose — the same "no external font/image refs" rule the rasterizer
 * keeps. These are the repo's OWN files: the 400 is `woff2` of the exact
 * `.ttf` the thumbnail rasterizer bundles and core's advance table is
 * generated from, so the viewer, the PNG unfurl and the fitting
 * arithmetic now describe one identical set of 856 code points. The 600
 * is the same conversion of the SemiBold face the app draws with. The
 * OFL license rides beside them, as it must.
 *
 * NOT SUBSET, deliberately, though 128 of those code points are
 * Devanagari and most of the weight is theirs. A subset would make the
 * served face carry fewer characters than core's table claims to know,
 * and a label in a script the picture cannot draw would be fitted as if
 * it could. Coverage matching everywhere is worth the bytes.
 *
 * THE FILENAMES CARRY THEIR OWN CONTENT HASH, which is what earns the
 * `immutable` in `_headers`: a different font is a different URL, so a
 * cached one can never be stale. `guards/site-fonts.test.ts` pins the
 * hashes, the sources they came from, and the fact that this file names
 * exactly the files on disk. Regenerating them is `site/scripts/README-fonts.md`.
 *
 * `swap`, not `block`: the venn is the page's whole point and it must
 * never be invisible while a font is in flight. The 400 is preloaded from
 * index.html so the swap usually happens before the venn arrives at all —
 * it is behind an API round trip and the arrival ceremony. The 600 is
 * chrome and can afford to be late. */
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/baloo2-400-9407eb20.woff2') format('woff2');
}
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/baloo2-600-84e4dfeb.woff2') format('woff2');
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  background: var(--zov-ground);
  color: var(--zov-ink);
  font-family:
    'Baloo 2',
    system-ui,
    -apple-system,
    'Segoe UI',
    Roboto,
    sans-serif;
}

.stage {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--zov-ground);
  transition: background 600ms var(--zov-ease-turn, ease);
}

/* Act 1 — the token bloom. Plays immediately, pure chrome, implies
 * nothing about the network result in flight behind it. */
.bloom-token {
  width: 72px;
  height: 72px;
  border-radius: 999px;
  background: var(--zov-identity);
  opacity: 0;
  transform: scale(0.2);
}

.stage[data-state='bloom'] .bloom-token {
  animation: token-bloom 900ms var(--zov-ease-glide-out, ease) forwards;
}

@keyframes token-bloom {
  0% {
    opacity: 0;
    transform: scale(0.2);
  }
  60% {
    opacity: 1;
    transform: scale(1.15);
  }
  100% {
    opacity: 0.9;
    transform: scale(1);
  }
}

/* Act 2 — the assembly. Only ever shown once the envelope has passed
 * every read-route check; a doc that fails never reaches this markup. */
.venn-mount {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
}

.stage[data-state='assembling'] .venn-mount,
.stage[data-state='settled'] .venn-mount {
  opacity: 1;
}

.venn-mount svg {
  /* Headroom for a real venn's own canvas dimensions, which can run far
   * larger than this suite's fixtures (390x400) — at a 1024x768 test
   * viewport, 90vmin is 691px, well above either fixture's intrinsic
   * size, so NEITHER of e2e/viewer.spec.ts's committed screenshot
   * baselines ever exercises this cap at all (Ferruccio's gate, PR #83,
   * nit — "the dead rule the baselines can't see"). Left in place
   * rather than removed: it is real, load-bearing headroom for a
   * document this suite has simply never tried, not dead code — but the
   * gap in coverage is real too, named here rather than silently
   * assumed covered. */
  max-width: 90vmin;
  max-height: 90vmin;
  cursor: grab;
}

.venn-mount svg:active {
  cursor: grabbing;
}

/* Each top-level painted layer (a circle, a region, a label group) the
 * assembly cascades in carries this class; arrival.ts sets the stagger
 * via --zov-cascade-delay, computed from the document's own set count —
 * "cascade timing derived from the actual set count" per the plan. */
.cascade-piece {
  opacity: 0;
  transform: scale(0.85);
  animation: assemble 520ms var(--zov-ease-glide-out, ease) forwards;
  animation-delay: var(--zov-cascade-delay, 0ms);
}

@keyframes assemble {
  0% {
    opacity: 0;
    transform: scale(0.85);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

/* Closed — a definitive denial. The stage cools to cream and shows one
 * quiet line, distinct from the transient state below on purpose. */
.status-line {
  position: relative;
  z-index: 1;
  font-size: 1.1rem;
  opacity: 0;
  text-align: center;
  padding: 0 1.5rem;
}

.stage[data-state='closed'] {
  background: var(--zov-ground);
}

.stage[data-state='closed'] .status-line {
  animation: settle-in 700ms var(--zov-ease-turn, ease) forwards;
  cursor: pointer;
}

/* Transient — a hiccup, never the closed copy, never the closed color
 * treatment. A gentle breathing pulse rather than a settle, so it reads
 * as "still trying," not "done." */
.stage[data-state='transient'] .status-line {
  animation: breathe 1600ms var(--zov-ease-turn, ease) infinite;
  opacity: 0.85;
}

@keyframes breathe {
  0%,
  100% {
    opacity: 0.55;
  }
  50% {
    opacity: 1;
  }
}

@keyframes settle-in {
  0% {
    opacity: 0;
    transform: translateY(6px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

.landing {
  max-width: 40rem;
  margin: 4rem auto;
  padding: 0 1.5rem;
  text-align: center;
}

.landing h1 {
  color: var(--zov-identity);
  font-size: 2.5rem;
}

.landing-note {
  opacity: 0.75;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
