/* Duet Lab — the crowd backdrop.
 *
 * Ported from the scene2.* bench, which stays in the repo as the art direction
 * bench. What changed on the way in is only the framing: on the bench the crowd
 * WAS the page, so it owned the body — `overflow: hidden`, a crosshair cursor,
 * and a hint line at the bottom. Here it is the layer underneath a panel of
 * type and controls, so it owns nothing but its own fixed box, and the cursor
 * and overflow rules live in style.css where the rest of the page can see them.
 *
 * The figures themselves are all drawn on the canvas by crowd.js. This file
 * styles the canvas's box and the one DOM overlay, the hover bubble.
 *
 * Tokens are not repeated here — style.css declares them on `:root` and is
 * loaded first. That was a deliberate change from the bench, which duplicated
 * the block so it could stand alone. */

/* ---------- Stage ---------- */

/* Fixed, full-viewport, behind everything. Fixed rather than absolute so the
   crowd stays put if the page ever does scroll — the layout is a single fold by
   design, but a short viewport can still overflow, and a backdrop that slid
   away from the panel would come apart.

   `z-index: 0` rather than a negative value: a negative index would drop the
   canvas behind the body's background, which is opaque black, and the crowd
   would vanish. The content above it gets a positive index in style.css.

   The explicit `width`/`height` are NOT redundant next to `inset: 0`, which is
   the trap here. A canvas is a replaced element with an intrinsic size of
   300x150, and for replaced elements that intrinsic size wins over stretching
   between opposite offsets — so `inset: 0` alone leaves the element at 300x150
   pinned to the top-left, and the crowd draws into a small box in the corner
   while the rest of the viewport stays empty black. `100%` against a fixed
   element resolves to the viewport, which is what the sizing in crowd.js then
   measures.

   `display: block` kills the inline-element descender gap. */
#stage {
  position: fixed;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* The figures are the interface here, so the I-beam a canvas would otherwise
     get is wrong. Scoped to the canvas — the panel over it holds real text and
     buttons, which want their own normal cursors. */
  cursor: crosshair;
}

/* A vignette over the crowd: black at the very top and bottom, clear through
   the middle. Purely legibility, and it earns its place at the bottom in
   particular — the feature strip and the colophon sit down there on nothing but
   line work, and 12px tertiary mono over a dense field of white strokes is not
   readable. The panel solves its own background with a blur; these two are too
   small and too light to carry one, so the page darkens beneath them instead.

   Sits between the canvas and the content: above `#stage` (z-index 0), below
   `.fold` (z-index 1). `pointer-events: none` is what keeps it from stealing
   the crowd's hover — without it this layer would cover the entire viewport and
   nothing would ever reach the canvas.

   The stops are asymmetric on purpose, and they are measured against where the
   content actually lands rather than picked for symmetry: the feature strip
   sits at roughly 88-93% of viewport height and the colophon at 95-98%, so the
   ramp has to be substantially dark by 85% and near-solid through the last
   tenth. The top band only has to stop the crowd from ending abruptly at the
   frame edge, so it is shorter and lighter.

   The middle is left completely clear — that is the band the panel floats in,
   and the crowd should be at full strength either side of it. */
#stage-veil {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(0, 0, 0, 0.7) 0%,
      rgba(0, 0, 0, 0.2) 10%,
      rgba(0, 0, 0, 0) 26%,
      rgba(0, 0, 0, 0) 55%,
      rgba(0, 0, 0, 0.3) 70%,
      rgba(0, 0, 0, 0.72) 82%,
      rgba(0, 0, 0, 0.92) 90%,
      rgba(0, 0, 0, 0.97) 100%);
}

/* ---------- Hover bubble ---------- */

/* A speech bubble in the app's label voice. It is positioned by JS as a
   transform on the element's top-left, with the translate(-50%, -100%) here
   anchoring it above and centred on the figure's head — so JS only ever has to
   write the raw head coordinate and never do bubble arithmetic.
   `will-change` keeps it on its own layer, since it moves every frame that the
   hover target changes.

   Above the panel in the stack, not just above the canvas: a figure whose head
   sits just outside the panel's edge is hoverable, and its bubble opens upward
   and inward — so it has to be allowed to draw over the panel rather than
   sliding behind it. */
.bubble {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 20;
  pointer-events: none;
  will-change: transform, opacity;
  transform: translate(-50%, -100%);
}

.bubble[hidden] { display: none; }

/* The bubble body: a white hairline capsule on the page's own black, matching
   the line weight of the figures underneath so it reads as part of the same
   drawing rather than as UI pasted on top. The fill is the ground colour, not
   transparent — a figure's strokes showing through the label would make it
   unreadable in a dense field. */
.bubble-label {
  display: block;
  padding: 7px 13px 6px;
  border: 1px solid rgba(255, 255, 255, 0.85);
  border-radius: 999px;
  background: var(--background);
  font-family: var(--label);
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-primary);
  /* The same bloom the figures get on the canvas, so the highlight reads as one
     light source across both layers. */
  box-shadow: 0 0 20px rgba(255, 255, 255, 0.18);
}

/* The tail. A rotated square with two of its borders drawn, sitting half-outside
   the capsule's bottom edge and masked by the capsule's own background — the
   standard trick, and cheaper than an SVG for a shape this small. */
.bubble-label::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -4px;
  width: 7px;
  height: 7px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--background);
  border-right: 1px solid rgba(255, 255, 255, 0.85);
  border-bottom: 1px solid rgba(255, 255, 255, 0.85);
}

/* The bubble pops rather than fades in: a scale from just under 1 with a slight
   overshoot in the easing, which is what makes it feel like speech rather than
   a tooltip. Driven by a class so JS can re-trigger it per figure. */
.bubble.in .bubble-label {
  animation: bubblePop 220ms cubic-bezier(0.22, 1.4, 0.36, 1) both;
}

@keyframes bubblePop {
  from { opacity: 0; transform: translateY(5px) scale(0.86); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .bubble.in .bubble-label { animation-duration: 0.01ms; }
}
