/* viewer.css — The viewer toolbar, the four overlay bands, the axes hint, the model
   labels and the two badges.
   Cascade position 18 of 18. Order is load-bearing and is asserted by
   verify-fixtures.mjs; index.html links these explicitly (never @import) because Caddy's
   try_files turns a missing file into a 200 text/html SPA shell.
   Owner after Step 1: section (n). Items 54, 58, 59, 60, 110.

   ══ The band system (items 54, 58, 59) ══════════════════════════════════════════════
   Every overlay used to be its own absolutely-positioned box hung off the stage, and
   three of them shared one 18px baseline. MEASURED on dev before this change, three
   models loaded:

     1440x900  .tools 530.53 x 57.39 at x=224.34, .axes 476.51 wide at x=18
               -> 4,347 px2 of overlap; every button 49.39px tall (two-line labels)
     1280x800  .tools at x=169.94 vs .axes right edge 494.51
               -> 324.57px of horizontal overlap, 5,222 px2
     1024x768  stage 632, .tools 530.53 -> 7,140 px2 of overlap
     390x844   .tools at x=-70.27, right 460.27 in a 390px stage: clipped at BOTH edges,
               15,694 px2 printed through a 3-line hint strip

   A band is a reserved row. Nothing positions itself against the stage any more; it
   positions itself inside its band, and two bands cannot occupy the same pixels.

     tl  top-left      free for section (l)'s work caption (DESIGN-SPEC §3.4)
     tr  top-right     pose badge
     r1  bottom row 1  the axes hint OR the underside badge — never both (mocks B1)
     r2  bottom row 2  the toolbar alone, so it is genuinely centred at every width

   Row 2's bottom is 16px and the island is 40px tall, so row 1's bottom is 16+40+8 = 64px
   and the two rects are 8px apart by construction rather than by hoping.

   The band is `pointer-events: none` and its children are `auto`: an overlay row spans the
   stage, and the stage is an orbit surface. Only the pixels an overlay actually paints may
   take a drag. A touch-drag that starts ON the toolbar still scrolls row 2, because scroll
   chaining follows the ancestor chain from the event target and does not consult
   pointer-events on the way up.

   ══ No backdrop-filter anywhere in this file — deliberate ════════════════════════════
   REFERENCE-STUDY §16 puts the island on `rgba(22,24,29,.85) + blur(12px)`. We ship the
   opaque `--sn-surface-2` instead, for a reason this project has already paid for once:
   row 2 must be a scroll container with a 28px edge mask (mocks/NOTES.md B2), a `mask` on
   an ancestor makes it a **Backdrop Root**, and IMPLEMENTATION-PLAN §5.2 is the record of
   `backdrop-filter` silently becoming a no-op under exactly that construction
   (`.sheet__dock`, item 2, the dock menu measured at 1.03:1). DESIGN-SPEC §2.8 says the
   line is the separator and the shadow is only depth on a #09090b page; the same argument
   retires the blur. An opaque island cannot fail silently, and it is what makes the
   toolbar's contrast measurable at all: over a WebGL canvas there is no surface to
   composite against, so `tools/contrast-check.mjs` could not check the label before.

   ══ One elevation recipe for all four overlays ═══════════════════════════════════════
   Fill + a 1px hairline as box-shadow stop 1, no `border` (COMPONENT-PLAN §0.1 rule 11).
   `--sn-line-raised` rather than REFERENCE-STUDY §16's 10% white: 10% white over
   `--sn-surface-2` is 1.22:1 against its own fill, under the 2:1 `raised` bar
   tools/contrast-check.mjs sets for a floating surface's edge; `--sn-line-raised` is
   2.56:1 and is the line DESIGN-SPEC §2.2 already gives every floating surface. */

/* ══ The four reserved bands ═════════════════════════════════════════════════════════ */
.band {
  position: absolute; z-index: 5;
  display: flex; align-items: flex-end; gap: var(--sn-space-2);
  pointer-events: none;
}
.band > * { pointer-events: auto; }

.band--tl { top: var(--sn-space-4); left: var(--sn-space-4); max-width: min(390px, 40%); }
.band--tr {
  top: var(--sn-space-4); right: var(--sn-space-4);
  max-width: min(360px, 40%); justify-content: flex-end;
}
.band--r1 { left: var(--sn-space-4); right: var(--sn-space-4); bottom: 64px; }

/* mocks/NOTES.md B2. `safe center` centres the row while it fits and pins it to the START
   when it does not, so the first button can never end up at a negative x the way the old
   `left: 50%; translate: -50%` box did (measured x = -70.27 at 390). The threshold is not
   a breakpoint — it is "the toolbar is wider than the stage" — so the rule is
   unconditional and the scroll simply never engages while everything fits. */
.band--r2 {
  left: var(--sn-space-4); right: var(--sn-space-4); bottom: var(--sn-space-4);
  justify-content: safe center;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity; scrollbar-width: none;
}

.support-preview-legend {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center;
  gap: var(--sn-space-1) var(--sn-space-2); max-width: 390px; padding: 7px var(--sn-space-2);
  border-radius: var(--sn-radius-sm); pointer-events: none;
  background: var(--sn-surface-2); box-shadow: inset 0 0 0 1px var(--sn-line-raised);
}
.support-preview-legend i {
  grid-row: 1 / span 2; width: 9px; height: 9px; border-radius: 50%; background: var(--sn-warn);
}
.support-preview-legend[data-mode="risk"] i { background: var(--sn-bad); }
.support-preview-legend span {
  min-width: 0; font-size: var(--sn-text-2xs); line-height: var(--sn-lh-2xs); font-weight: 650; color: var(--sn-ink);
}
.support-preview-legend small {
  font-size: var(--sn-text-2xs); line-height: 1.3; color: var(--sn-ink-mute);
}

/* ══ The toolbar island (items 58, 60, 62) ═══════════════════════════════════════════
   REFERENCE-STUDY §16's insight, kept exactly: the island's padding equals the buttons'
   gap (4px), and the button radius equals the island radius minus that padding
   (12 - 4 = 8), so a hovered button at the island's edge nests concentrically instead of
   leaving a crescent. Both numbers are existing tokens: --sn-radius-md and --sn-radius-sm.
   §16's own 10px/8px pair is not concentric at 4px of padding; ours is. */
.tools {
  display: none; gap: var(--sn-space-1); padding: var(--sn-space-1);
  border-radius: var(--sn-radius-md);
  background: var(--sn-surface-2);
  box-shadow: inset 0 0 0 1px var(--sn-line-raised);
}
.tools.on { display: flex; }
.tools button {
  flex: 0 0 auto; height: 32px; padding: 0 var(--sn-space-2);
  border: 0; border-radius: var(--sn-radius-sm); cursor: pointer;
  background: none; color: var(--sn-ink-dim); font: inherit;
  font-size: var(--sn-text-xs); font-weight: 600; white-space: nowrap;
  display: inline-flex; align-items: center; gap: var(--sn-space-1);
  scroll-snap-align: center;
  transition: background-color var(--sn-dur-fast) var(--sn-ease-out),
              color var(--sn-dur-fast) var(--sn-ease-out);
}
/* Fill only, at rest and on hover — no transform, no shadow growth (house rule 1). */
.tools button:hover:not([aria-pressed="true"]) {
  background: rgb(var(--sn-white-rgb) / .047058823529411764); color: var(--sn-ink);
}
/* The same pressed language as every other toggle in the app (css/controls.css `.seg`,
   `.chip`, `.type-card`): accent tint plus accent label. REFERENCE-STUDY §16 uses 14%
   white, but 7%-white-versus-14%-white is not a state indicator a person can find, and
   DESIGN-SPEC §9 asks a non-text state indicator to be legible against its neighbours. */
.tools button[aria-pressed="true"] { background: var(--sn-accent-tint); color: var(--sn-accent); }
/* 1 x 20 at --sn-line-strong (1.69:1 on the island; a divider inside a filled surface is
   decoration, so contrast-check's 1.2:1 hairline bar applies, not the 3:1 control bar).
   4px of margin plus the island's 4px flex gap is §16's 8px of air on each side. */
.tools .sep {
  flex: 0 0 auto; align-self: center;
  width: 1px; height: 20px; margin: 0 var(--sn-space-1);
  background: var(--sn-line-strong);
}

/* ══ The hint strip (items 54, 59) ═══════════════════════════════════════════════════
   mocks/NOTES.md B1: on band row 2 with the toolbar it had ~95px and wrapped to four
   lines. It gets row 1 to itself, one `nowrap` line, on the island's own surface so it is
   legible over a render and measurable by contrast-check. REFERENCE-STUDY §17's one
   universal finding: a hint is one size smaller (11px vs 12.5px) and one step more muted
   (--sn-ink-mute vs --sn-ink-dim) than the label it explains. */
.axes {
  display: none; gap: var(--sn-space-3); flex-wrap: nowrap; max-width: 100%;
  padding: 5px var(--sn-space-2); border-radius: var(--sn-radius-sm);
  background: var(--sn-surface-2);
  box-shadow: inset 0 0 0 1px var(--sn-line-raised);
  font-size: var(--sn-text-2xs); line-height: var(--sn-lh-2xs); color: var(--sn-ink-mute);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.axes.on { display: flex; }
.axes i { font-style: normal; flex: 0 0 auto; }
.axes i b { font-weight: 600; color: var(--sn-ink-dim); }
/* Row 1's two occupants are mutually exclusive by construction (mocks B1): while the
   camera is under the bed, "drag past the plate to see underneath" is advice about
   something the user has already done. */
.stage.is-underside .axes { display: none; }

/* ══ Model labels — Wave 0b Lane B's per-model identity, not restyled ════════════════
   The border colour IS the identity: section (k)'s row badge reads the same
   `--model-color` and its contract pins the two to the same rgb() (batch2-contract.md,
   "k: PUBLISHED"). It stays a `border`, not a box-shadow ring, so that pairing keeps
   measuring. House rule 11's gate is "no element has both a border and a spread ring";
   this element has exactly one of the two. */
.model-labels { position: absolute; inset: 0; z-index: 5; pointer-events: none; }
.model-label {
  position: absolute; transform: translate(-50%, -100%); pointer-events: none;
  max-width: min(260px, 90%); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: var(--sn-space-1) 7px;
  border: 1px solid var(--model-color, var(--sn-accent)); border-radius: 7px;
  background: var(--sn-surface-1); color: var(--sn-ink);
  font-size: var(--sn-text-xs); line-height: var(--sn-lh-xs);
}
.model-number { color: var(--model-color, var(--sn-accent)); font-weight: 700; }

/* ══ The underside badge ═════════════════════════════════════════════════════════════
   Under the bed the scene loses its usual cues, so the page says where you are rather
   than leaving the user to work it out from an upside-down grid. */
.under-badge {
  display: none; max-width: min(320px, 45%);
  padding: var(--sn-space-2) var(--sn-space-3); border-radius: var(--sn-radius-sm);
  background: var(--sn-surface-2);
  box-shadow: inset 0 0 0 1px var(--sn-accent-edge);
  font-size: var(--sn-text-xs); line-height: var(--sn-lh-xs); color: var(--sn-ink-soft);
}
.stage.is-underside .under-badge { display: block; }
.under-badge b { display: block; color: var(--sn-accent); font-weight: 700; margin-bottom: 2px; }
.stage.is-underside::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background: linear-gradient(to top, rgb(var(--sn-accent-rgb) / .0784313725490196), transparent 42%);
}

/* ══ The pose badge (item 110) ═══════════════════════════════════════════════════════
   The one claim the product is making, said next to the thing it is about. Section (j)
   supplies the line: `poseVerdictLine()` writes the VERDICT here and the reasons stay in
   the dossier, so the two are never character-identical. */
.pose-badge {
  max-width: 100%;
  padding: var(--sn-space-3); border-radius: var(--sn-radius-md);
  background: var(--sn-surface-3);
  box-shadow: inset 0 0 0 1px var(--sn-line-raised);
  font-size: var(--sn-text-xs); line-height: var(--sn-lh-xs); color: var(--sn-ink-dim);
}
.pose-badge b {
  display: block; color: var(--sn-ink); font-size: var(--sn-text-xs); font-weight: 700;
  margin-bottom: 3px;
}
.pose-badge.is-warn {
  box-shadow: inset 0 0 0 1px var(--sn-warn-edge);
  background: var(--sn-surface-3); color: var(--sn-warn-ink);
}
.pose-badge.is-warn b { color: var(--sn-warn); }
.pose-badge .pose-badge__swatch {
  display: inline-block; width: 9px; height: 9px; border-radius: 2px;
  background: var(--sn-accent); margin-right: 5px; vertical-align: baseline;
}
/* DESIGN-SPEC §9 names `#poseBadgeToggle` (h18) as a target under the 32px floor. The box
   grows, the text does not move relative to its own line box, and nothing else in the
   badge reflows. */
.pose-badge .link-btn { min-height: 32px; display: inline-flex; align-items: center; }

/* ══ Below 1160px the toolbar is wider than the stage ════════════════════════════════
   mocks/NOTES.md B2's 28px right-edge mask, on the BAND rather than on the toolbar, so it
   is the scrollport that fades and not the content. While the toolbar fits, the last 28px
   of the band is empty and the fade is invisible; the moment it overflows, the fade lands
   on the cut-off button and says there is more. */
@media (max-width: 1160px) {
  .band--r2 {
    mask-image: linear-gradient(to right, rgb(var(--sn-black-rgb)) calc(100% - 28px), transparent);
  }
}

/* ══ Phones — item 54 / DESIGN-SPEC §5.8 ═════════════════════════════════════════════
   Below 640px the stage is the full viewport width and about 405px tall (measured at
   390x844), and four overlays cannot be given four separate rows in that. So:
     · the toolbar takes the full stage width minus 24px at bottom 12px, and scrolls;
     · row 1 moves to the TOP of the stage — its only phone occupant is the underside
       badge, because the hint strip is mouse copy ("Right-drag to pan", "Scroll to
       zoom") and is not rendered on a touch screen at all;
     · the underside badge and the pose badge therefore share the top of the stage, and
       the underside badge wins while the camera is below the bed. They are never both
       rendered, which is what item 54's "zero pairwise intersection" needs. */
@media (max-width: 640px) {
  .band--r2 { left: var(--sn-space-3); right: var(--sn-space-3); bottom: var(--sn-space-3); }
  .band--r1 {
    top: var(--sn-space-3); bottom: auto;
    left: var(--sn-space-3); right: var(--sn-space-3);
  }
  .band--tr {
    top: var(--sn-space-3); right: var(--sn-space-3); left: var(--sn-space-3);
    max-width: none; height: 30%; align-items: flex-start;
  }
  .band--tr > * { flex: 1 1 auto; }
  /* The badge itself, not its band: an element hidden through its parent still reports a
     0 x 0 rect at (0, 0), and item 54's gate is read off rects. `display: none` on the
     badge says what is true — it is not rendered. */
  .stage.is-underside .pose-badge { display: none; }
  .axes.on { display: none; }
  .under-badge { max-width: none; }
  /* DESIGN-SPEC §5.8: the badge may take at most 30% of the stage and scrolls past that.
     The 30% is a definite `height` on the BAND, not a `max-height` on either box, and both
     alternatives were MEASURED before this line was written: a percentage max-height on
     the badge resolves against an auto-height parent, so it becomes `none` and the badge
     measured 95.25px in a 253.2px stage (37.6%); a max-height on the band with
     `align-items: flex-end` made the badge overflow the band UPWARDS to y = 48.7, four
     pixels above the top of the stage. A definite height gives the badge's own
     `max-height: 100%` something to resolve against. */
  .pose-badge { max-height: 100%; overflow-y: auto; }
}

/* ══ Quiet redesign — the viewport lane (23 Sep 2026) ═════════════════════════════════
   DESIGN.md (outputs/uiux-2026-09-18/designs/7-final) §3, §4.1, §4.3, §4.4, §5.3, §5.4.
   Every overlay below is built by js/viewport-ui.js inside #stage. Text over the 3D view
   is --sn-ink-dim, never --sn-ink-mute: mute measures 4.46:1 on the lit bed (§2).

   The two translucent cards (drop card, progress card) use backdrop-filter. The header's
   "no backdrop-filter" rule is about the BANDS: row 2 is a masked scroll container, a
   Backdrop Root, where the blur silently no-ops. These cards are direct children of the
   stage and sit in no band. */

/* ── Retiring the four text bands in the simple view ────────────────────────────────
   Owner's rule (UI-QUIET-BUILD-PLAN): with "Show details for testers" ON, everything the
   app shows today is still visible, so the bands stay exactly as they were under
   html[data-detail="full"]. In the simple view:
     r2  the old toolbar (#tools): its camera half is the new column (Fit/Top/Front/Side/
         Reset); Turntable, Bed grid and Bed contact move to the shell's floating toolbar
         through setTurntable/setBedGrid/setBedContact; Underside, Wireframe and Run log
         are tester tools and stay in the full view.
     r1  the "Drag to rotate" hint and the underside badge: informational, full view.
     tr  the pose badge: its neutral verdict is said by the result panel, full view —
         EXCEPT the amber `is-warn` state (the preview is rebuilt from the bed direction
         and the turn may differ), which is a warning and is never hidden by the toggle.
     tl  the support-preview legend explains orange/red paint ON the model, so it stays in
         both views; top-left under the crumb, below the progress card while it shows
         (it used to move bottom-left, under the toolbar: e2e audit N4). */
html:not([data-detail="full"]) #bandR2,
html:not([data-detail="full"]) #bandR1 { display: none; }
html:not([data-detail="full"]) #bandTr:not(:has(.pose-badge.is-warn)) { display: none; }
/* e2e audit N4: at the bottom-left the support legend sat under the floating toolbar
   (1728 and 1280). It stays top-left, under the crumb, and only steps down below the
   progress card while that card is showing. */
html:not([data-detail="full"]) .stage.has-vp-progress .band--tl { top: 136px; }
/* Under the stats line, clear of the gizmo and the camera column on the right. */
.band--tr { top: 48px; right: 104px; }

/* ── Stats line, top-right (§5.3) ───────────────────────────────────────────────── */
.vp-stats { position: absolute; z-index: 5; right: 104px; top: 12px; border-radius: var(--sn-radius-sm); }
.vp-stats__line {
  margin: 0; display: flex; align-items: center; gap: var(--sn-space-2);
  padding: 6px 10px; border-radius: var(--sn-radius-sm);
  font-size: var(--sn-text-xs); line-height: var(--sn-lh-xs); color: var(--sn-ink-dim);
  white-space: nowrap; cursor: default;
  transition: color 600ms var(--sn-ease-out);
}
.vp-stats.is-flash .vp-stats__line { color: var(--sn-ink); }
.vp-stats__full {
  position: absolute; right: 0; top: 0; min-width: 248px; margin: 0;
  padding: var(--sn-space-3) var(--sn-space-4); border-radius: var(--sn-radius-md);
  background: var(--sn-surface-3); box-shadow: var(--sn-shadow-raised);
  display: grid; grid-template-columns: auto auto; row-gap: var(--sn-space-2); column-gap: var(--sn-space-5);
  font-size: var(--sn-text-xs); line-height: var(--sn-lh-xs);
  opacity: 0; transform: translateY(-4px); pointer-events: none;
  transition: opacity var(--sn-dur-base) var(--sn-ease-out), transform var(--sn-dur-base) var(--sn-ease-out);
}
.vp-stats__full dt { color: var(--sn-ink-mute); }
.vp-stats__full dd { margin: 0; text-align: right; color: var(--sn-ink); }
.vp-stats:hover .vp-stats__full,
.vp-stats:focus-within .vp-stats__full { opacity: 1; transform: none; pointer-events: auto; }

/* ── Gizmo and the camera column (§5.4) ───────────────────────────────────────────
   Without three.js (fixture mode, or the CDN failed) there is no camera to move, so the
   two go with the canvas rather than offering buttons that do nothing. */
.vp-gizmo { position: absolute; z-index: 5; right: 12px; top: 8px; width: 84px; height: 84px; }
.vp-gizmo svg { width: 100%; height: 100%; overflow: visible; }
.vp-gizmo__ax { cursor: pointer; }
.vp-gizmo text {
  font: 700 var(--sn-text-2xs)/1 var(--sn-font); fill: var(--sn-surface-0);
  text-anchor: middle; dominant-baseline: central; pointer-events: none;
}
.vp-tools {
  position: absolute; z-index: 5; right: 38px; top: 108px;
  display: flex; flex-direction: column; gap: var(--sn-space-1);
}
.vp-tools__sep { height: var(--sn-space-2); }
.vp-tool {
  width: 32px; height: 32px; display: grid; place-items: center; padding: 0;
  border: 0; border-radius: var(--sn-radius-sm); cursor: pointer;
  background: none; color: var(--sn-ink-dim);
  transition: background-color var(--sn-dur-fast) var(--sn-ease-out),
              color var(--sn-dur-fast) var(--sn-ease-out);
}
.vp-tool:hover { background: rgb(var(--sn-white-rgb) / .06); color: var(--sn-ink); }
.stage:not(:has(> canvas)) .vp-gizmo,
.stage:not(:has(> canvas)) .vp-tools { display: none; }

/* ── Empty-state card on the bed and the drag veil (§4.1) ─────────────────────────── */
.vp-drop {
  position: absolute; z-index: 4; left: 50%; top: 46%; transform: translate(-50%, -50%);
  width: min(400px, calc(100% - 32px)); padding: var(--sn-space-5);
  border-radius: var(--sn-radius-md); text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: var(--sn-space-1);
  background: rgb(var(--sn-surface-0-rgb) / .55); backdrop-filter: blur(8px);
  animation: vp-rise 500ms var(--sn-ease-out);
}
.vp-drop__icon {
  width: 40px; height: 40px; display: grid; place-items: center;
  margin-bottom: var(--sn-space-2); border-radius: var(--sn-radius-md);
  background: var(--sn-surface-3); color: var(--sn-ink-soft);
}
.vp-drop__title { margin: 0; font: 700 var(--sn-text-lg)/1.2 var(--sn-font-display); color: var(--sn-ink); }
.vp-drop__sub { margin: 0; font-size: var(--sn-text-sm); line-height: var(--sn-lh-sm); color: var(--sn-ink-dim); }
.vp-drop__acts { display: flex; gap: var(--sn-space-2); margin-top: var(--sn-space-4); }
.vp-drop .btn { width: auto; min-height: 36px; font-size: var(--sn-text-sm); }
.vp-veil {
  position: absolute; inset: 0; z-index: 7; display: grid; place-items: center;
  border-radius: inherit; pointer-events: none;
  background: var(--sn-accent-tint); box-shadow: inset 0 0 0 2px var(--sn-accent-edge);
  font: 700 var(--sn-text-lg)/1 var(--sn-font-display); color: var(--sn-ink);
  opacity: 0; transition: opacity var(--sn-dur-base) var(--sn-ease-out);
}
.stage.dragging .vp-veil, .vp-veil.is-on { opacity: 1; }

/* ── Progress card, viewport top-left (§4.3) ──────────────────────────────────────── */
.vp-progress {
  position: absolute; z-index: 5; left: var(--sn-space-4); top: 64px;
  display: flex; align-items: center; gap: var(--sn-space-3);
  padding: var(--sn-space-2) var(--sn-space-4) var(--sn-space-2) var(--sn-space-2);
  border-radius: var(--sn-radius-md); pointer-events: none;
  background: rgb(var(--sn-surface-0-rgb) / .78); backdrop-filter: blur(12px);
  animation: vp-rise var(--sn-dur-base) var(--sn-ease-out);
}
.vp-progress__ring { position: relative; width: 44px; height: 44px; flex: none; }
.vp-progress__ring svg { position: absolute; inset: 0; transform: rotate(-90deg); }
.vp-progress__track { fill: none; stroke: rgb(var(--sn-white-rgb) / .1); stroke-width: 3; }
.vp-progress__arc { fill: none; stroke: var(--sn-accent); stroke-width: 3; stroke-linecap: round; }
.vp-progress__pc {
  position: absolute; inset: 0; display: grid; place-items: center;
  font: 700 var(--sn-text-sm)/1 var(--sn-font-display); color: var(--sn-ink);
}
.vp-progress__lbl { display: flex; flex-direction: column; min-width: 200px; }
.vp-progress__title { font-weight: 500; font-size: var(--sn-text-sm); line-height: var(--sn-lh-sm); color: var(--sn-ink); }
.vp-progress__sub { font-size: var(--sn-text-xs); line-height: var(--sn-lh-xs); color: var(--sn-ink-dim); }
.vp-progress__sub.is-warn { color: var(--sn-warn); }

@keyframes vp-rise { from { opacity: 0; transform: translateY(-4px); } }
/* The drop card is centred with a transform, so its entrance cannot animate transform. */
.vp-drop { animation-name: vp-fade; }
/* e2e audit N1: the bed of a result with no model on it says why, quietly, centred. */
.vp-bednote {
  position: absolute; z-index: 4; left: 50%; top: 50%; translate: -50% -50%;
  max-width: min(420px, 80%); margin: 0; padding: var(--sn-space-2) var(--sn-space-4);
  border-radius: var(--sn-radius-md); text-align: center; pointer-events: none;
  background: rgb(var(--sn-surface-0-rgb) / .72); backdrop-filter: blur(8px);
  font-size: var(--sn-text-sm); line-height: var(--sn-lh-sm); color: var(--sn-ink-dim);
}
.vp-bednote[hidden] { display: none; }
@keyframes vp-fade { from { opacity: 0; } }

/* Phones: no gizmo or camera column (a touch drag IS the camera); the stats line takes
   the top of the stage and may wrap. */
@media (max-width: 640px) {
  .vp-gizmo, .vp-tools { display: none; }
  .vp-stats { left: var(--sn-space-3); right: var(--sn-space-3); top: var(--sn-space-3); }
  .vp-stats__line { white-space: normal; }
  .band--tr { top: var(--sn-space-3); right: var(--sn-space-3); }
}
