/* =========================================================================
   Live scene — a recreation of the ROOMS UI in plain HTML/CSS/SVG.

   Every rule here is a port of something in the andor app; the source is named
   in a comment above each block. EVERYTHING is scoped under `.scene`, because
   the app's token names (--background, --text-color, --accent-color,
   --border-radius, --btn-height) collide with this documentation page's own.
   Scoping means the replica carries its own theme and neither side can
   disturb the other.

   Keep in sync with:
     andor/src/styles/variables.scss
     andor/src/styles/mixins.scss
     andor/src/app/app.component.scss
     andor/src/app/core/components/header/main-nav/main-nav.component.scss
     andor/src/app/features/pre-join/room-login/room-login.component.scss
     andor/src/app/features/pre-join/room-plan/**
     andor/src/app/shared/components/{ui,form-fields}/**
   ========================================================================= */

/* -------------------------------------------------------------------------
   Tokens — andor/src/styles/variables.scss and
   andor/src/app/shared/constants/app-config-default.ts (`styles`)
   ------------------------------------------------------------------------- */

.scene {
  /* Runtime-themed in the app. The instance in the reference screenshots is
     themed blue, so the accent is wired to this page's own blue and the scene
     follows the docs palette instead of the app's default teal. */
  --accent-color: var(--accent);
  --success-color: 0, 188, 120;
  --error-color: 255, 85, 85;

  --border-radius: 0.75rem;
  --btn-height: 2.2rem;
  --input-field-vertial-padding: 0.5rem;
  --animation-duration: 0.2s;
  --animation-timefunc: ease;
}

/* Light is the app's default; `body.dark` overrides in the app, `[data-theme]`
   here. Declared on the same element so specificity stays flat. */
.scene,
:root[data-theme="light"] .scene {
  --primary-color: 203, 203, 203;
  --contrast-color: 255, 255, 255;
  --background: 250, 246, 246;
  --text-color: 0, 0, 0;
  /* Opaque stand-in for the app's wallpaper + scrim composite: the scene sits
     on this page's frosted glass, so it needs its own base to layer onto. */
  --app-base: 236, 240, 245;
  --logo: url("assets/logo_light.svg");
  /*
   * The app hardcodes `whitesmoke` for these (main-nav.component.scss), which
   * works against its dark nav band but disappears on the light one. A figure
   * that is illegible in one of the two themes this page supports is a
   * documentation defect, so light mode gets a readable value instead. This is
   * the one deliberate departure from the app's own values.
   */
  --nav-icon-color: rgba(20, 26, 34, 0.85);
}

:root[data-theme="dark"] .scene {
  --primary-color: 36, 46, 53;
  --contrast-color: 0, 0, 0;
  --background: 0, 0, 0;
  --text-color: 235, 239, 242;
  /*
   * Deliberately a mid grey, not a dark one. The app's dark tokens are literal
   * black (`--background: 0,0,0`) laid over a *bright* wallpaper photo at 70%
   * alpha — so the composited stage lands around #2e2e2e. Give this stand-in a
   * dark value and every layer stacks to near-black instead. This is the
   * wallpaper's luminance, not a surface color.
   */
  --app-base: 158, 160, 165;
  --logo: url("assets/logo_dark.svg");
  --nav-icon-color: whitesmoke;
}

/* -------------------------------------------------------------------------
   Figure shell
   ------------------------------------------------------------------------- */

.scene-figure {
  margin: 0 0 1.25rem;
  padding: 0.5rem;
  border-radius: var(--border-radius);
}

/*
 * The figure keeps `.glass`'s border, veil and shadow but drops its blur.
 *
 * There is nothing here for a blur to do: `.scene` is opaque and covers all of
 * the figure but its 0.5rem padding, and in pinned mode --stage-backing (0.94)
 * covers even that. What the blur *was* doing is worse than nothing — it made
 * the figure the outer level that every backdrop-filter inside the replica
 * then sampled through, which is the overlapping-blur case Chromium renders
 * badly, and it did so inside a sticky, zoom-scaled subtree that recomposites
 * on every scroll frame.
 */
.scene-figure::before {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.scene-figure figcaption {
  margin-top: 0.6rem;
  font-size: 0.85rem;
  line-height: 1.5;
  color: rgb(var(--text-muted));
  text-align: center;
}

/*
 * Reserve two lines, always.
 *
 * The caption changes with the scene state, and a longer one wrapping to a
 * second line grew the figure, which grew the pinned column, which shoved the
 * stepper off the bottom of the screen mid-transition. Fixing the height keeps
 * the pin a constant size no matter which state is showing.
 */
.scrolly--horizontal .scene-figure figcaption {
  display: flex;
  align-items: center;
  justify-content: center;
  height: calc(2 * 1.5 * 0.85rem);
  margin-top: 0.4rem;
  overflow: hidden;
}

/* Hidden until the script has drawn the tiles, so a no-JS visitor sees the
   <noscript> screenshot instead of a half-built replica. */
.scene {
  display: none;
}

.scene[data-ready="true"] {
  display: block;
}

.scene {
  position: relative;
  border-radius: 0.5rem;
  /* `clip` rather than `hidden`, same reasoning as .scrolly__box: the replica
     is full of focusable hotspots, and a scroll container here would let a
     focus or scrollIntoView inside it shift the whole stage. Nothing scrolls
     the scene deliberately, so there is no behaviour to preserve. */
  overflow: clip;
  background: rgb(var(--app-base));
  /*
   * Backdrop root — app.component.scss:66 does the same on .app-stage.
   * Everything the replica blurs (the nav band, the dock, the profile cards,
   * the create dialog) is meant to sample the stage it sits on, never the
   * documentation page behind it. Isolating here says so structurally, so a
   * backdrop-filter added later cannot quietly start reaching outside and
   * stack itself under whatever the page is blurring.
   *
   * Free of visual consequence because this background is fully opaque: there
   * is nothing behind it left to see.
   */
  isolation: isolate;
  color: rgba(var(--text-color), 1);
  /* The app's own stack — andor/src/styles/styles.scss */
  font-family:
    -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
    sans-serif;
  user-select: none;
}

/*
 * app.component.scss — .app-stage carries no background of its own, only a
 * backdrop blur. Every band below composites straight onto the wallpaper,
 * which is what --app-base stands in for.
 */
.scene__stage {
  display: flex;
  flex-direction: column;
}

/* -------------------------------------------------------------------------
   Logo band + nav — app.component.scss (.logo) and main-nav.component.scss
   ------------------------------------------------------------------------- */

.scene__logo {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 7rem;
  padding: 0.8rem;
  background: rgba(var(--primary-color), 0.7);
}

.scene__logo span {
  display: block;
  width: min(60%, 14rem);
  aspect-ratio: 806 / 182;
  background-image: var(--logo);
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

.scene__nav {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  height: 3.5rem;
}

/*
 * backdrop-blur-layer — mixins.scss:145. brightness(2) is not redundant here
 * the way the login field's blur is: it genuinely lifts the flat base showing
 * through the 0.8 tint. But it rides an inset pseudo rather than the band
 * itself, keeping the page's one invariant intact — nothing that paints ever
 * shares a box with a backdrop-filter.
 *
 * The tint moves onto the pseudo with it, for the reason spelled out at the
 * foot of this file: a background left on the host would sit *beneath* the
 * pseudo and become filter input, so brightness(2) would lift the tint itself
 * rather than the base showing through it. On the pseudo it paints above the
 * filter result — the same order as `backdrop-filter` plus `background` on
 * one box, which is what this replaces.
 */
.scene__nav::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(var(--primary-color), 0.8);
  backdrop-filter: brightness(2);
  -webkit-backdrop-filter: brightness(2);
  pointer-events: none;
}

.scene__nav-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.8rem;
  height: 1.8rem;
  margin: 0 0.25rem;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--nav-icon-color);
  cursor: pointer;
}

.scene__nav-btn .icon {
  width: 1.35rem;
  height: 1.35rem;
  transition: transform 0.2s linear;
}

/* The app scales the glyph rather than the button — main-nav.component.scss */
.scene__nav-btn:hover .icon,
.scene__nav-btn.is-active .icon {
  transform: scale3d(1.5, 1.5, 1.5);
}

.scene__nav-btn.is-active {
  color: rgb(var(--accent-color));
}

/* app.component.scss — .app-content */
.scene__content {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.5rem;
  background: rgba(var(--background), 0.7);
}

/* -------------------------------------------------------------------------
   Fieldset — shared/components/ui/fieldset/fieldset.component.scss
   ------------------------------------------------------------------------- */

.scene__fieldset {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 0.5rem;
  border-radius: var(--border-radius);
  background: rgba(var(--background), 0.25);
}

.scene__intro {
  margin: 0 0 0.5rem;
  text-align: center;
  font-size: 0.95rem;
}

/* -------------------------------------------------------------------------
   Room plan grid — room-plan.component.scss, plus the login's 3-column
   preview override in room-login.component.scss (`&.plan`)
   ------------------------------------------------------------------------- */

.scene__plan {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 10rem));
  justify-content: center;
  gap: 0.75rem;
  padding-bottom: 0.75rem;
}

/* -------------------------------------------------------------------------
   Room tile — room-plan-tile.component.scss
   ------------------------------------------------------------------------- */

.tile {
  /* One accent hue per room, translucent by default so the picked room can pop
     at full opacity; the door carries the only semantic color — green when the
     user can walk in, red when they must knock. */
  --plan-color: rgba(var(--accent-color), 0.5);
  --plan-door-open-color: rgb(var(--success-color));
  --plan-door-closed-color: rgb(var(--error-color));

  /* Overlay positions below are percentages of the tile box; they only line up
     with the drawing because this ratio matches the 200x150 viewBox. */
  aspect-ratio: 4 / 3;
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  cursor: pointer;
  transition: transform var(--animation-duration) var(--animation-timefunc);
}

.tile:hover {
  transform: translateY(-2px) scale(1.02);
}

.tile[aria-pressed="true"] {
  --plan-color: rgba(var(--accent-color), 1);
}

.tile svg {
  display: block;
  width: 100%;
  height: 100%;
}

.tile .wall {
  fill: none;
  stroke: var(--plan-color);
  stroke-width: 2.5;
  stroke-linecap: round;
}

/* Deliberately thicker than the walls so the door reads as the tile's
   primary signal. */
.tile .door-leaf {
  fill: none;
  stroke: var(--plan-door-open-color);
  stroke-width: 5;
  stroke-linecap: round;
}

.tile .door-leaf--closed {
  stroke: var(--plan-door-closed-color);
}

.tile .door-handle {
  fill: var(--plan-door-closed-color);
}

.tile .door-swing {
  fill: none;
  stroke: var(--plan-door-open-color);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
  opacity: 0.6;
}

.tile .table {
  fill: var(--plan-color);
  fill-opacity: 0.25;
  stroke: var(--plan-color);
  stroke-width: 2;
}

.tile .seat {
  fill: var(--plan-color);
}

.tile__overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Name plate in the clear band between the top wall and the top seat
   (center y 24/150 = 16%). Width capped so it never reaches the side door
   swings (x 44–156 in SVG units = 22–78%). */
.tile__plate {
  position: absolute;
  left: 50%;
  top: 16%;
  transform: translate(-50%, -50%);
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  max-width: 52%;
  padding: 0.2rem 0.45rem;
  border: 1px solid rgba(var(--accent-color), 0.5);
  border-radius: 1rem;
  background: rgba(var(--background), 0.4);
  color: rgba(var(--text-color), 1);
  font-size: 0.65rem;
  font-weight: 600;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Sits on the table; `top` is set per-tile because the table group drops
   slightly for side-door rooms. */
.tile__count {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 0.2rem;
  color: var(--plan-color);
  font-size: 0.75rem;
  font-weight: 700;
}

.tile__count .icon {
  width: 0.8rem;
  height: 0.8rem;
}

/* Schedule clock, then access icons, tucked inside the walls bottom-right.
   A translucent chip in the app background color lifts them off the drawing. */
.tile__meta {
  position: absolute;
  right: 8%;
  bottom: 10%;
  display: flex;
  gap: 0.3rem;
  padding: 0.15rem 0.3rem;
  border-radius: 0.4rem;
  background: rgba(var(--background), 0.35);
}

.tile__meta .icon {
  width: 0.85rem;
  height: 0.85rem;
  color: var(--plan-color);
  opacity: 0.7;
}

/* The lock mirrors the door's traffic-light color. */
.tile__meta .icon--lock {
  color: var(--plan-door-open-color);
  opacity: 1;
}

.tile__meta .icon--lock-closed {
  color: var(--plan-door-closed-color);
}

/* -------------------------------------------------------------------------
   Login form — room-login.component.scss (.login-area, .entry, .create-btn)
   ------------------------------------------------------------------------- */

.scene__login {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 0;
}

.scene__entry {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: min(24rem, 100%);
}

/*
 * accent-ring-surface — mixins.scss:83. The gradient ring is a masked overlay
 * so it can carry a gradient a plain border cannot. This page already ships
 * that mask as `.accent-ring`; the ring below re-declares it against the app's
 * --accent-color rather than the docs' --accent so the scene stays
 * self-contained.
 */
.scene__field,
.scene__icon-btn {
  position: relative;
  background: rgba(var(--text-color), 0.04);
  /*
   * No backdrop-filter, unlike the mixin. Two reasons, and the second is the
   * one that matters: the app blurs a wallpaper photo, whereas this replica's
   * stage is the flat opaque `rgb(var(--app-base))`, and blurring a flat
   * colour returns the same colour — so the app's blur(12px) would render
   * nothing here. And [data-spot] animates a box-shadow onto this very box
   * (scene-spotlight, below), which is exactly the pairing Chromium
   * rasterizes into one render surface and draws a frame out of sync
   * (crbug.com/41471914) — the shadow then detaches and strobes.
   */
  border-radius: var(--border-radius);
}

.scene__field::before,
.scene__icon-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(
    135deg,
    rgba(var(--accent-color), 0.4),
    rgba(var(--accent-color), 0.1) 40%,
    rgba(var(--accent-color), 0.25) 60%,
    rgba(var(--accent-color), 0.4)
  );
  -webkit-mask:
    linear-gradient(#fff 0 0) content-box,
    linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
  transition:
    padding 0.3s ease,
    opacity 0.3s ease;
}

.scene__field {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
}

/*
 * A div, not an <input>. A convincing but non-functional login form is a UX
 * trap — this cannot be focused or typed into, so nobody tries.
 */
.scene__field-value {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--input-field-vertial-padding) 0.75rem;
  font-size: 1rem;
  font-weight: 300;
  line-height: 1.2;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.scene__field-value--placeholder {
  color: rgba(var(--text-color), 0.45);
}

.scene__field-action {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  padding: 0 0.6rem;
}

.scene__field-action .icon {
  width: 1.1rem;
  height: 1.1rem;
  color: rgba(var(--text-color), 0.75);
}

/* room-login.component.scss:199 — .create-btn */
.scene__icon-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--btn-height);
  height: var(--btn-height);
  padding: 0;
  border: none;
  cursor: pointer;
}

.scene__icon-btn .icon {
  width: 1rem;
  height: 1rem;
  color: rgba(var(--accent-color), 0.5);
}

/* -------------------------------------------------------------------------
   Buttons — ui/buttons/gradient-btn/button.component.scss
   ------------------------------------------------------------------------- */

.scene__btn {
  --btn-rgb: var(--accent-color);
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--btn-height);
  min-width: 64px;
  margin: 0;
  padding: 0 1rem;
  border: 1px solid transparent;
  border-radius: var(--border-radius);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 400;
  color: rgba(var(--text-color), 1);
  cursor: pointer;
  transition: 0.3s all ease-in-out;
  /* glass-tint($rgb, 0.16, 0.4) — mixins.scss:176 */
  background: rgba(var(--btn-rgb), 0.16);
  border-color: rgba(var(--btn-rgb), 0.4);
}

.scene__btn:hover {
  background: rgba(var(--btn-rgb), 0.26);
  border-color: rgba(var(--btn-rgb), 0.55);
}

.scene__btn--compact {
  height: 1.8rem;
  padding: 0 0.75rem;
  font-size: 0.8rem;
  white-space: nowrap;
}

/* The flashy CTA: translucent gradient fill with a hairline highlight border
   so it still reads frosted. */
.scene__btn--gradient {
  padding: 0 2rem;
  text-transform: uppercase;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: calc(var(--btn-height) / 2);
  color: rgba(var(--contrast-color), 1);
  background:
    linear-gradient(
    130deg,
    rgba(var(--btn-rgb), 0.85),
    rgba(var(--contrast-color), 0.15)
  ),
    rgba(var(--btn-rgb), 0.7);
}

.scene__btn--gradient:hover {
  filter: brightness(1.1);
}

.scene__actions {
  display: flex;
  justify-content: center;
  padding: 0.25rem 0;
}

/* room-login.component.scss — .schedule-row. The button is centered as a
   stand-in when no meeting is scheduled, and only tucks into the bottom-right
   corner when a schedule occupies the center. No schedule here, so: centered. */
.scene__schedule-row {
  display: flex;
  justify-content: center;
}

/* -------------------------------------------------------------------------
   Duotone icons — layer 0 is the secondary (40% by FA convention), layer 1
   the primary. Single-shape icons ship only a primary.
   ------------------------------------------------------------------------- */

.icon {
  display: inline-block;
  fill: currentColor;
  flex: none;
}

/* Duotone opacity and fill-rule ride on the symbols as presentation
   attributes — a stylesheet cannot reach inside a <use> shadow tree. See
   tools/build-icon-sprite.js. `fill` still works here because it inherits. */

/* -------------------------------------------------------------------------
   Hotspots — the documentation layer, not part of the app
   ------------------------------------------------------------------------- */

.hotspot {
  position: relative;
  cursor: help;
}

/* Dotted underline-ish affordance that doesn't disturb the replica's look:
   a ring that only appears on hover/focus. */
.hotspot::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 0.5rem;
  border: 1px dashed rgba(var(--accent-color), 0);
  transition: border-color var(--animation-duration) var(--animation-timefunc);
  pointer-events: none;
}

.hotspot:hover::after,
.hotspot:focus-visible::after,
.hotspot[data-active="true"]::after {
  border-color: rgba(var(--accent-color), 0.9);
}

.hotspot:focus-visible {
  outline: none;
}

/*
 * .hotspot deliberately sets NO paint properties. It is always applied on top
 * of an already-styled replica element (a nav button, a field, the CTA), so a
 * background/border/color reset here would silently strip that element's own
 * styling — which is exactly what it did on first run.
 */

.scene-tip {
  position: fixed;
  z-index: 60;
  max-width: 17rem;
  padding: 0.5rem 0.7rem;
  border-radius: 0.5rem;
  font-size: 0.82rem;
  line-height: 1.45;
  color: rgb(var(--text));
  --surface-tint: rgba(252, 253, 255, 0.94);
  /* No --surface-blur, for the reason given on .scrolly__outro: the tip fades
     its own opacity in and out, and opacity below 1 makes it a backdrop root,
     so a blur beneath would only exist at the end of the fade and pop. At 0.94
     alpha there was next to nothing showing through it to blur anyway. */
  border: 1px solid var(--hairline);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
}

/* The tip is read against the page, not the scene, so it takes the docs
   palette and a near-opaque backing — it often overlaps the dark replica.

   Sets --surface-tint, NOT background. The backdrop-filter/box-shadow split at
   the foot of this file moved every decoupled surface's fill onto an inset
   ::before, and that pseudo paints above the host's own background — so a
   `background` here was covered by the light default tint above, giving a
   white bubble with white text in dark mode. */
:root[data-theme="dark"] .scene-tip {
  --surface-tint: rgba(24, 31, 41, 0.94);
}

.scene-tip[data-open="true"] {
  opacity: 1;
}

.scene-hint {
  margin: 0.5rem 0 0;
  font-size: 0.8rem;
  color: rgb(var(--text-muted));
  text-align: center;
}

/* -------------------------------------------------------------------------
   Narrow viewports. The app's own login drops to fewer plan columns; the
   scene is inside a text column, so it shrinks the tiles instead of
   reflowing them — three-across is the shape being documented.
   ------------------------------------------------------------------------- */

@media only screen and (max-width: 32rem) {
  .scene__logo {
    height: 4.5rem;
  }

  .scene__plan {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.4rem;
  }

  .scene__nav {
    gap: 0.25rem;
  }

  .tile__plate {
    font-size: 0.55rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tile,
  .scene__nav-btn .icon {
    transition: none;
  }

  .tile:hover {
    transform: none;
  }
}

/* =========================================================================
   Scrollytelling — one scene, five steps, stepped through horizontally

   The section is made tall and its contents pinned, so ordinary vertical
   scrolling advances a horizontal step counter. Nothing here intercepts the
   wheel: the browser scrolls normally, the scrollbar stays honest, and a step
   whose text overflows chains to the page by itself once it hits its end.

   Everything below is gated on `.scrolly--horizontal`, which the script adds
   once it has taken over. Without JavaScript, under prefers-reduced-motion, or
   on a viewport too short to pin anything, the section stays a plain vertical
   list of steps.
   ========================================================================= */

:root,
:root[data-theme="light"] {
  --stage-backing: rgba(242, 246, 250, 0.94);
}

:root[data-theme="dark"] {
  --stage-backing: rgba(14, 19, 27, 0.94);
}

/* --- Linear fallback (also the no-JS default) ---------------------------- */

.scrolly .step {
  scroll-margin-top: calc(var(--header-height) + 1.5rem);
}

.beat-text + .beat-text {
  margin-top: 1.5rem;
}

/* --- Pinned horizontal mode ---------------------------------------------- */

.scrolly--horizontal .scrolly__pin {
  position: sticky;
  top: calc(var(--header-height) + 0.75rem);
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.scrolly--horizontal .scrolly__stage .scene-figure {
  margin-bottom: 0;
  /* The page's frosted glass is translucent, which is right for a panel that
     sits still. This one has content moving behind it, and a blurred smear
     reads as a rendering fault. */
  background: var(--stage-backing);
}

/*
 * The box the steps live in.
 *
 * --step-box-pref is the one knob worth turning. It is the height the box
 * would like; the script caps it at half the viewport and shrinks it further
 * if the scene above would otherwise be squeezed below legibility. At 30rem
 * the scene is forced down to about half scale, which defeats the point of
 * having it there — hence the smaller default.
 */
.scrolly--horizontal {
  --step-box-pref: 19rem;
}

.scrolly--horizontal .scrolly__box {
  position: relative;
  z-index: 0;
  /* Replaced by an exact pixel height once the script has done the budgeting;
     this is what shows in the frame before that. */
  height: min(var(--step-box-pref), 50vh);
  /*
   * `clip`, not `hidden` — app.component.scss:146 makes the same swap for the
   * same reason. `hidden` clips visually but still leaves a *scroll container*,
   * and the inactive panels sit at translateX(±2.5rem), so the box has 40px of
   * scrollable overflow. Following a step link (#step-dock, the contents list,
   * the back button) makes the browser scroll that target into view, and with
   * nothing else able to move, it scrolled the box — leaving every card offset
   * 40px left of its own surface with the first character of each line cut off.
   * `clip` is visually identical and is not a scroll container, so there is
   * nothing to scroll.
   */
  overflow: clip;
}

/*
 * The step cards' frosted blur, moved off the cards and onto the box.
 *
 * The cards crossfade by animating their own opacity (see .panel below), and
 * anything that forms a *backdrop root* leaves a descendant's backdrop-filter
 * with nothing to sample. `opacity` below 1 forms one — but so does the
 * compositing promotion Chromium applies to an element with a transitioning
 * opacity, which does not go away between transitions. Measured against a
 * high-contrast test backdrop: with the blur on the card, the pattern behind
 * it stayed *pixel-sharp* — at opacity 1, mid-fade, at rest, always. The
 * cards' frosted surface was never actually frosted, and what read as flicker
 * was Chromium making and unmaking that root around each step change.
 *
 * It matters most in dark mode, the default, where the veil is only
 * 0.1/0.035 alpha — there, nearly the whole card surface *is* the blur.
 *
 * The box neither fades nor transitions, so a blur here is stable, and it is
 * the same blur: one pane serves every card, since they are all `inset: 0`
 * within a full-height .scrolly__panels, making the box's content rect and a
 * card's rect the same rectangle. The radius is repeated so the pane's corners
 * sit under the cards' rounded ones.
 */
.scrolly--horizontal .scrolly__box::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: var(--border-radius);
  backdrop-filter: blur(12px) saturate(1.2);
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  pointer-events: none;
}

/* Veil, border and inset highlight stay on the card — they are solid colours
   and do not care about a backdrop. Only the blur has to leave. Scoped to
   pinned mode: in the linear fallback the cards stack down the page, nothing
   fades, and each keeps its own `.glass` blur. */
.scrolly--horizontal .panel::before {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.scrolly--horizontal .scrolly__panels {
  position: relative;
  height: 100%;
}

/*
 * Panels stack and crossfade. The horizontal offset is what makes the fade
 * read as *stepping sideways* rather than dissolving in place; its direction
 * comes from data-dir on the box, so going back reverses the movement.
 */
/*
 * The panel is the glass card, and it must NOT scroll.
 *
 * `.glass` paints its frosted surface with an absolutely-positioned ::before,
 * and inside a scroll container such a pseudo scrolls away with the content —
 * so a long step lost its background partway down. Keeping the card static and
 * scrolling `.step__body` within it means the surface always covers the card.
 */
.scrolly--horizontal .panel {
  position: absolute;
  inset: 0;
  /* `clip`, not `hidden` — the card must not be a scroll container either.
     Only `.step__body` inside it is scrolled, and that one stays `hidden`
     precisely because the script drives its scrollTop. */
  overflow: clip;
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translateX(2.5rem);
  pointer-events: none;
  visibility: hidden;
  transition:
    opacity 0.32s ease,
    transform 0.32s ease,
    visibility 0s linear 0.32s;
}

/* The step's heading stays put; only its prose scrolls. */
.scrolly--horizontal .panel .step__head {
  flex: 0 0 auto;
}

/*
 * `hidden`, not `auto` — the body is scrolled programmatically from the page
 * position (see apply() in scene-room-login.js), which is what lets a long
 * step be read through wherever the pointer happens to be. Native nested
 * scrolling only worked with the cursor over the box, and would fight the
 * script for control of scrollTop. An overflow:hidden element is still a
 * scroll container, so scrollTop keeps working.
 */
.scrolly--horizontal .panel .step__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

/* --- Custom scrollbar ---------------------------------------------------- */

/*
 * With the native bar gone there is nothing to say a step has more to read,
 * so the indicator is drawn here instead: a slim track pinned beside the body
 * (it lives on the panel, not inside the scroller, so it cannot scroll away).
 * Purely an indicator — the page scroll moves the content.
 */
.panel__scroll {
  position: absolute;
  right: 0.45rem;
  width: 3px;
  border-radius: 3px;
  background: rgba(var(--text), 0.09);
  opacity: 0;
  transition: opacity var(--animation-duration) var(--animation-timefunc);
  pointer-events: none;
}

.panel__scroll[data-visible="true"] {
  opacity: 1;
}

.panel__thumb {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  border-radius: 3px;
  background: rgba(var(--accent), 0.7);
}

.scrolly--horizontal [data-dir="back"] .panel {
  transform: translateX(-2.5rem);
}

.scrolly--horizontal .panel.is-active {
  opacity: 1;
  transform: translateX(0);
  pointer-events: auto;
  visibility: visible;
  transition:
    opacity 0.32s ease,
    transform 0.32s ease,
    visibility 0s;
}

.scrolly--horizontal .panel.is-leaving {
  opacity: 0;
  transform: translateX(-2.5rem);
}

.scrolly--horizontal [data-dir="back"] .panel.is-leaving {
  transform: translateX(2.5rem);
}

/* --- Stepper ------------------------------------------------------------- */

.scrolly__nav {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}

.scrolly--horizontal .scrolly__nav {
  display: flex;
}

.scrolly__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(var(--accent), 0.4);
  background: rgba(var(--accent), 0.14);
  color: rgb(var(--text));
  cursor: pointer;
  transition: background var(--animation-duration) var(--animation-timefunc);
}

.scrolly__arrow:hover:not(:disabled) {
  background: rgba(var(--accent), 0.26);
}

.scrolly__arrow:disabled {
  opacity: 0.35;
  cursor: default;
}

.scrolly__arrow .icon {
  width: 0.9rem;
  height: 0.9rem;
  fill: currentColor;
}

[data-step-prev] .icon {
  transform: rotate(90deg);
}

[data-step-next] .icon {
  transform: rotate(-90deg);
}

.scrolly__dots {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.scrolly__dots li {
  margin: 0;
}

.scrolly__dot {
  display: block;
  width: 0.55rem;
  height: 0.55rem;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(var(--text), 0.25);
  cursor: pointer;
  transition:
    background var(--animation-duration) var(--animation-timefunc),
    transform var(--animation-duration) var(--animation-timefunc);
}

.scrolly__dot:hover {
  background: rgba(var(--text), 0.45);
}

.scrolly__dot[aria-current="true"] {
  background: rgb(var(--accent));
  transform: scale(1.35);
}

/* --- Motion & size guards ------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .scrolly--horizontal .panel {
    transition: none;
  }
}

@media only screen and (max-height: 56rem) {
  .scrolly--horizontal .scrolly__stage .scene__logo {
    height: 4rem;
  }
}

/* =========================================================================
   Room field dropdown — autocomplete-field.component.scss (.ac-panel /
   .ac-option) and room-login.component.scss (.room-option)
   ========================================================================= */

.scene__field-wrap {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

.scene__dropdown {
  position: absolute;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  z-index: 3;
  display: none;
  flex-direction: column;
  padding: 0.25rem;
  border-radius: var(--border-radius);
  /* The app's panel is opaque so options stay readable over the stage. */
  background: rgba(var(--background), 1);
  box-shadow: 0 8px 24px -6px rgba(0, 0, 0, 0.45);
  /*
   * The app caps its option list and scrolls it (.ac-list, max-height 16rem).
   * The cap is lower here because the scene is a fixed-height figure with
   * `overflow: hidden` — without it the list is simply cut off mid-row.
   */
  max-height: 11rem;
  overflow-y: auto;
}

[data-scene="search"] .scene__dropdown {
  display: flex;
}

.scene__option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  border-radius: calc(var(--border-radius) * 0.75);
  color: rgba(var(--text-color), 1);
  font-size: 0.95rem;
}

/* The fixed "new instant meeting" action: accented so it reads as a primary
   action rather than a listed room. */
.scene__option--instant {
  justify-content: flex-start;
  gap: 0.5rem;
  color: rgba(var(--success-color), 1);
  font-weight: 600;
  background: rgba(var(--text-color), 0.06);
}

.scene__option-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.scene__option-meta {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex: 0 0 auto;
  opacity: 0.75;
}

.scene__option-count {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.scene__option .icon {
  width: 0.9rem;
  height: 0.9rem;
}

/* =========================================================================
   Dialog layer — room-dialog.component.scss. In the app this opens in a CDK
   overlay above the stage; here it is an absolutely-positioned child.
   ========================================================================= */

.scene__overlay {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.45);
}

[data-scene="create-dialog"] .scene__overlay,
[data-scene="created"] .scene__overlay {
  display: flex;
}

.scene__dialog {
  display: none;
  flex-direction: column;
  gap: 0.9rem;
  width: 24rem;
  max-width: 100%;
  padding: 1.25rem;
  border-radius: var(--border-radius);
  /* frosted-glass, tinted with the app background so it reads as an
     app-coloured translucent panel rather than neutral white. */
  position: relative;
  z-index: 0;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-left-color: rgba(255, 255, 255, 0.18);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
  --surface-tint: rgba(var(--background), 0.75);
  --surface-blur: blur(12px) saturate(1.2);
}

[data-scene="create-dialog"] [data-dialog="create"],
[data-scene="created"] [data-dialog="created"] {
  display: flex;
}

.scene__dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.scene__dialog-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1.2rem;
  font-weight: 500;
}

.scene__dialog-title .icon {
  width: 1.2rem;
  height: 1.2rem;
}

.scene__dialog-close {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 50%;
  border: 1px solid rgba(var(--text-color), 0.08);
  background: rgba(var(--text-color), 0.08);
  color: rgba(var(--text-color), 0.85);
}

.scene__dialog-close .icon {
  width: 0.9rem;
  height: 0.9rem;
}

.scene__dialog-body {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.scene__dialog-entry {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.scene__dialog-lead {
  margin: 0;
  font-size: 0.95rem;
  opacity: 0.85;
}

.scene__label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
}

.icon--info {
  width: 0.9rem;
  height: 0.9rem;
  color: rgba(var(--accent-color), 1);
}

.icon--accent {
  color: rgba(var(--accent-color), 1);
}

.icon--success {
  color: rgba(var(--success-color), 1);
}

.scene__toggle-row {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}

/* Material slide toggle, off state — a track with the knob parked left. */
.scene__toggle {
  position: relative;
  flex: none;
  width: 3.25rem;
  height: 1.75rem;
  border-radius: 1rem;
  background: rgba(var(--text-color), 0.18);
  border: 1px solid rgba(var(--text-color), 0.2);
}

.scene__toggle::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0.28rem;
  transform: translateY(-50%);
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: rgba(var(--text-color), 0.55);
}

.scene__expansion,
.scene__invite {
  gap: 0.5rem;
  padding: 0 0.6rem;
  min-height: 2.6rem;
}

.scene__expansion .icon,
.scene__invite .icon {
  width: 1rem;
  height: 1rem;
  flex: none;
}

.scene__invite .scene__field-value {
  padding-left: 0;
  font-size: 0.9rem;
}

.scene__btn.is-disabled {
  opacity: 0.55;
  background: rgba(var(--text-color), 0.25);
  border-color: transparent;
  color: rgba(var(--text-color), 0.9);
  cursor: default;
}

/* =========================================================================
   Scene states — what each step emphasises
   ========================================================================= */

/* Step 3 pushes everything but the room plan back so the drawing carries the
   attention while the legend is being read. */
[data-scene="plan"] [data-region="fields"],
[data-scene="plan"] [data-region="actions"] {
  opacity: 0.3;
}

[data-region] {
  transition: opacity var(--animation-duration) var(--animation-timefunc);
}

/*
 * Spotlight — deliberately brighter than anything the real UI does, because it
 * is a documentation affordance, not a state the app can be in. Readers should
 * never mistake it for a control that is genuinely lit up.
 */
[data-spot] {
  transition:
    box-shadow var(--animation-duration) var(--animation-timefunc),
    transform var(--animation-duration) var(--animation-timefunc);
}

[data-scene="search"] [data-spot="room-field"],
[data-scene="create-spotlight"] [data-spot="create"],
[data-scene="join"] [data-spot="join"],
[data-scene="created"] [data-spot="invite"] {
  animation: scene-spotlight 1.6s ease-in-out infinite;
  transform: scale(1.08);
  z-index: 2;
}

/* The room field is a full-width row; scaling it would push the buttons beside
   it out of line, so it glows in place. */
[data-scene="search"] [data-spot="room-field"] {
  transform: none;
}

/*
 * Step 3 already picks the first room out by dimming its neighbours' section;
 * glowing the tile itself says *which* room the legend is describing. No
 * scale: the tiles sit in a grid and already lift on hover, so a second
 * transform would fight it.
 */
[data-scene="plan"] .scene__plan .tile:first-child {
  animation: scene-spotlight 1.6s ease-in-out infinite;
  border-radius: var(--border-radius);
  z-index: 2;
}

/*
 * The dialog carries the app's own drop shadow, so it gets a dedicated
 * keyframe that keeps that depth underneath the glow rather than replacing it.
 */
[data-scene="create-dialog"] .scene__dialog[data-dialog="create"] {
  animation: scene-dialog-glow 1.6s ease-in-out infinite;
}

@keyframes scene-spotlight {
  0%,
  100% {
    box-shadow:
      0 0 0 2px rgba(var(--accent-color), 0.9),
      0 0 14px 2px rgba(var(--accent-color), 0.55);
  }
  50% {
    box-shadow:
      0 0 0 3px rgba(var(--accent-color), 1),
      0 0 26px 7px rgba(var(--accent-color), 0.75);
  }
}

@keyframes scene-dialog-glow {
  0%,
  100% {
    box-shadow:
      0 6px 18px rgba(0, 0, 0, 0.25),
      0 0 0 1.5px rgba(var(--accent-color), 0.85),
      0 0 18px 2px rgba(var(--accent-color), 0.4);
  }
  50% {
    box-shadow:
      0 6px 18px rgba(0, 0, 0, 0.25),
      0 0 0 2px rgba(var(--accent-color), 1),
      0 0 30px 6px rgba(var(--accent-color), 0.62);
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-scene="search"] [data-spot="room-field"],
  [data-scene="create-spotlight"] [data-spot="create"],
  [data-scene="join"] [data-spot="join"],
  [data-scene="created"] [data-spot="invite"],
  [data-scene="plan"] .scene__plan .tile:first-child {
    animation: none;
    box-shadow:
      0 0 0 2px rgba(var(--accent-color), 1),
      0 0 18px 4px rgba(var(--accent-color), 0.6);
  }

  [data-scene="create-dialog"] .scene__dialog[data-dialog="create"] {
    animation: none;
    box-shadow:
      0 6px 18px rgba(0, 0, 0, 0.25),
      0 0 0 2px rgba(var(--accent-color), 1),
      0 0 22px 4px rgba(var(--accent-color), 0.5);
  }
}

/* The pinned stage competes with the step box for vertical room; trim the logo
   band, which is the one part carrying no information. */
.scrolly--horizontal .scrolly__stage .scene__logo {
  height: 4.5rem;
}

/* -------------------------------------------------------------------------
   End-of-section hint

   Appears as the last step runs out and fades again before the section
   unpins. Opacity is set inline from scroll position (see paintOutro), so it
   tracks the reader in both directions rather than firing once.
   ------------------------------------------------------------------------- */

.scrolly__outro {
  position: absolute;
  left: 50%;
  bottom: 0.85rem;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.45rem 0.9rem;
  border-radius: 999px;
  font-size: 0.82rem;
  white-space: nowrap;
  color: rgb(var(--text));
  --surface-tint: var(--outro-bg);
  /*
   * No --surface-blur, and the tint below carries the difference.
   *
   * paintOutro writes this pill's opacity inline on every scroll frame, so it
   * is almost never at exactly 1 — and an element with opacity below 1 forms a
   * backdrop root, leaving its own inset ::before nothing to sample. A blur
   * here would therefore be dead for the whole fade and appear only at the
   * instant the pill is fully opaque, which reads as the backing snapping in.
   * A slightly denser tint is the honest way to stay legible over step text.
   */
  border: 1px solid rgba(var(--accent), 0.35);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
  /* Never in the way: it sits over the step text and must not swallow a
     click, a hover or a text selection. */
  pointer-events: none;
  opacity: 0;
}

:root,
:root[data-theme="light"] {
  --outro-bg: rgba(255, 255, 255, 0.88);
}

:root[data-theme="dark"] {
  --outro-bg: rgba(18, 24, 33, 0.86);
}

.scrolly__outro .icon {
  width: 0.9rem;
  height: 0.9rem;
  fill: rgb(var(--accent));
  animation: outro-nudge 1.5s ease-in-out infinite;
}

@keyframes outro-nudge {
  0%,
  100% {
    transform: translateY(-2px);
  }
  50% {
    transform: translateY(2px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .scrolly__outro .icon {
    animation: none;
  }
}

/* =========================================================================
   Conference panel scene

   Ported from conference-panel.component.scss, room-info.component.scss,
   panel-controls.component.scss, and the chat / video feature styles. Shares
   every token with the room-login scene above — this section only adds the
   parts unique to the connected view.
   ========================================================================= */

.scene--conference .conf__stage {
  position: relative;
  display: flex;
  flex-direction: column;
  /* A fixed height rather than hugging its content: the panel is a layout, and
     letting it collapse to whatever the chat happens to contain would make it
     read as a small widget. The pin budget zooms it down where it must. */
  height: 26rem;
  /* app.component.scss — .app-content sits on the wallpaper at 70% */
  background: rgba(var(--background), 0.7);
}

/* --- Header (room-info) --------------------------------------------------- */

.conf__header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.6rem;
  background: rgba(var(--primary-color), 0.8);
}

.conf__logo {
  flex: 0 0 auto;
  width: 6.5rem;
  height: 1.6rem;
  background-image: var(--logo);
  background-repeat: no-repeat;
  background-position: left center;
  background-size: contain;
}

/* Pushed to the right, as in the app's connected header. */
.conf__room {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.3rem 0.6rem;
  border-radius: var(--border-radius);
  background: rgba(var(--background), 0.35);
}

.conf__room-name {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-weight: 600;
  font-size: 0.95rem;
}

.conf__room-name .icon {
  width: 0.85rem;
  height: 0.85rem;
  opacity: 0.7;
}

.conf__status-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.conf__chip {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  font-size: 0.72rem;
  background: rgba(var(--text-color), 0.1);
}

.conf__chip .icon {
  width: 0.7rem;
  height: 0.7rem;
}

.conf__chip--ok {
  color: rgba(var(--success-color), 1);
}

.conf__header-actions {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

/* glass-tint(--text-color, 0.06, 0.08) — square-icon-btn.component.scss */
.conf__icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid rgba(var(--text-color), 0.08);
  border-radius: var(--border-radius);
  background: rgba(var(--text-color), 0.06);
  color: rgba(var(--text-color), 0.85);
  cursor: pointer;
}

.conf__icon-btn:hover {
  background: rgba(var(--text-color), 0.12);
}

.conf__icon-btn .icon {
  width: 0.95rem;
  height: 0.95rem;
}

.conf__icon-btn--danger {
  border-color: rgba(var(--error-color), 0.32);
  background: rgba(var(--error-color), 0.18);
  color: rgba(var(--error-color), 1);
}

/* --- Body: stage + rail + participants ------------------------------------ */

.conf__body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  gap: 0.4rem;
  padding: 0.4rem;
  /* Clear of the floating dock. */
  padding-bottom: 3.2rem;
}

.conf__main {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

.conf__module {
  position: absolute;
  inset: 0;
  display: none;
}

.scene--conference[data-view="chat"] [data-module="chat"],
.scene--conference[data-view="video"] [data-module="video"],
.scene--conference[data-view="whiteboard"] [data-module="whiteboard"],
.scene--conference[data-view="split"] [data-module="split"] {
  display: flex;
}

.conf__module--split {
  gap: 0.4rem;
}

/*
 * The app pins a fixed 22rem column and either module can occupy it. Here the
 * split is fixed as chat-left / video-right at 30%, which is what the step
 * describes and keeps the demo's toggle to one meaning.
 */
.conf__module--split .conf__chat {
  flex: 0 0 30%;
  min-width: 0;
}

.conf__module--split .conf__video {
  flex: 1 1 auto;
  min-width: 0;
}

/* --- Chat ----------------------------------------------------------------- */

.conf__chat {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: 100%;
  min-height: 0;
  padding: 0.5rem;
  border-radius: var(--border-radius);
  background: rgba(var(--text-color), 0.05);
}

/*
 * Anchored to the bottom like a real chat, so older messages run off the top.
 * The mask fades that edge instead of slicing a line in half, which reads as a
 * clipping bug rather than as history.
 */
.conf__messages {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 0.55rem;
  overflow: hidden;
  mask-image: linear-gradient(to bottom, transparent 0, #000 2rem);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 2rem);
}

/* chat-msg.component.scss — own messages mirror to the right. */
.conf__msg {
  display: flex;
  align-items: flex-start;
  gap: 0.45rem;
}

.conf__msg.is-self {
  flex-direction: row-reverse;
}

.conf__msg-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

.conf__msg.is-self .conf__msg-body {
  align-items: flex-end;
}

.conf__msg-head {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: 0.7rem;
}

.conf__msg-name {
  font-weight: 600;
}

.conf__msg-time {
  opacity: 0.55;
}

.conf__bubble {
  padding: 0.35rem 0.6rem;
  border-radius: 0.7rem;
  font-size: 0.82rem;
  line-height: 1.35;
  background: rgba(var(--text-color), 0.09);
}

.conf__msg.is-self .conf__bubble {
  background: rgba(var(--accent-color), 0.25);
}

.conf__chat--compact .conf__msg-head,
.conf__chat--compact .conf__avatar {
  display: none;
}

.conf__chat--compact .conf__bubble {
  font-size: 0.72rem;
}

.conf__composer {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 0 0 auto;
  padding: 0.35rem 0.5rem;
  border-radius: var(--border-radius);
  background: rgba(var(--accent-color), 0.5);
}

.conf__composer-value {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.82rem;
  opacity: 0.85;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.conf__composer .icon {
  width: 0.95rem;
  height: 0.95rem;
}

.conf__send {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.7rem;
  height: 1.5rem;
  border-radius: 0.4rem;
  background: rgba(var(--accent-color), 0.85);
}

/* --- Video ---------------------------------------------------------------- */

/* video.component.scss @mixin table-4 */
.conf__video {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 0.4rem;
  width: 100%;
  height: 100%;
}

.conf__tile {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  border-radius: var(--border-radius);
  background: rgba(var(--text-color), 0.08);
  overflow: hidden;
}

.conf__tile-name {
  position: absolute;
  left: 0.35rem;
  bottom: 0.3rem;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.1rem 0.4rem;
  border-radius: 0.4rem;
  font-size: 0.7rem;
  background: rgba(var(--background), 0.5);
}

.conf__tile-hand {
  width: 0.7rem;
  height: 0.7rem;
  fill: rgb(245, 209, 39);
}

/* --- Whiteboard ----------------------------------------------------------- */

.conf__whiteboard {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: var(--border-radius);
  background: rgba(var(--text-color), 0.06);
  border: 1px dashed rgba(var(--text-color), 0.18);
}

.conf__whiteboard-hint {
  font-size: 0.8rem;
  opacity: 0.5;
}

/* --- Avatars -------------------------------------------------------------- */

.conf__avatar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  font-size: 0.7rem;
  font-weight: 700;
  color: #fff;
  background: hsl(var(--avatar-hue, 210) 55% 45%);
}

.conf__avatar--tile {
  width: 2.6rem;
  height: 2.6rem;
  font-size: 1.1rem;
}

/* --- View rail ------------------------------------------------------------ */

.conf__rail {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.conf__rail-split,
.conf__rail-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: none;
  color: rgba(var(--text-color), 0.75);
  cursor: pointer;
}

.conf__rail-split {
  background: rgba(var(--text-color), 0.1);
}

.conf__rail-bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.15rem;
  border-radius: 999px;
  background: rgba(var(--text-color), 0.1);
}

.conf__rail-btn .icon,
.conf__rail-split .icon {
  width: 1.05rem;
  height: 1.05rem;
}

.conf__rail-btn:hover {
  color: rgba(var(--text-color), 1);
}

.conf__rail-btn.is-active {
  color: rgb(var(--accent-color));
}

/* --- Participant rail ----------------------------------------------------- */

.conf__side {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  width: 2.6rem;
  padding: 0.4rem 0;
  border-radius: var(--border-radius);
  background: rgba(var(--background), 0.45);
}

.conf__side-list {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.conf__side-list .is-self {
  outline: 2px solid rgba(var(--accent-color), 0.8);
  outline-offset: 1px;
}

.conf__side-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.8rem;
  height: 1.8rem;
  border-radius: var(--border-radius);
  background: rgba(var(--text-color), 0.08);
}

.conf__side-actions .icon {
  width: 0.9rem;
  height: 0.9rem;
  opacity: 0.8;
}

/* --- Control dock --------------------------------------------------------- */

/* panel-controls.component.scss — floats over the stage, bottom centre. */
.conf__dock {
  position: absolute;
  left: 50%;
  bottom: 0.5rem;
  transform: translateX(-50%);
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.35rem 0.5rem;
  border-radius: 999px;
  --surface-tint: rgba(var(--background), 0.75);
  --surface-blur: blur(10px);
  border: 1px solid rgba(var(--text-color), 0.1);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
}

.conf__dock-group {
  display: flex;
  align-items: center;
  border-radius: 0.6rem;
  background: rgba(var(--text-color), 0.06);
  border: 1px solid rgba(var(--text-color), 0.08);
}

/* Muted mic / camera read red, as in the app. */
.conf__dock-group.is-off {
  background: rgba(var(--error-color), 0.16);
  border-color: rgba(var(--error-color), 0.4);
  color: rgba(var(--error-color), 1);
}

.conf__dock-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: none;
  border-radius: inherit;
  background: none;
  color: inherit;
  cursor: pointer;
}

.conf__dock-btn .icon {
  width: 1rem;
  height: 1rem;
}

.conf__dock-chevron {
  display: flex;
  align-items: center;
  padding-right: 0.3rem;
  border-left: 1px solid currentColor;
  opacity: 0.6;
}

.conf__dock-chevron .icon {
  width: 0.6rem;
  height: 0.6rem;
  margin-left: 0.2rem;
}

.conf__dock-divider {
  width: 1px;
  height: 1.3rem;
  margin: 0 0.15rem;
  background: rgba(var(--text-color), 0.15);
}

/* --- Scene states --------------------------------------------------------- */

/* Step 2 pushes the stage back so the rail carries the attention. */
[data-scene="rail"] .conf__main,
[data-scene="rail"] .conf__side,
[data-scene="rail"] .conf__header {
  opacity: 0.35;
}

.conf__main,
.conf__side,
.conf__header {
  transition: opacity var(--animation-duration) var(--animation-timefunc);
}

[data-scene="rail"] .conf__rail-bar,
[data-scene="view-split"] [data-spot="split"],
[data-scene="dock"] [data-spot="dock"],
[data-scene="hand"] [data-spot="dock-hand"] {
  animation: scene-spotlight 1.6s ease-in-out infinite;
  border-radius: 999px;
  z-index: 4;
}

@media (prefers-reduced-motion: reduce) {
  [data-scene="rail"] .conf__rail-bar,
  [data-scene="view-split"] [data-spot="split"],
  [data-scene="dock"] [data-spot="dock"],
  [data-scene="hand"] [data-spot="dock-hand"] {
    animation: none;
    box-shadow:
      0 0 0 2px rgba(var(--accent-color), 1),
      0 0 18px 4px rgba(var(--accent-color), 0.6);
  }
}

/* -------------------------------------------------------------------------
   Expanded participant rail — conference-panel.component.scss (side panel)
   and side-panel/user-entry/user-entry.component.html
   ------------------------------------------------------------------------- */

.conf__side {
  transition: width var(--animation-duration) var(--animation-timefunc);
}

.scene--conference[data-panel="expanded"] .conf__side {
  width: 11rem;
  align-items: stretch;
  padding: 0.4rem;
}

.conf__entry {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  width: 100%;
  padding: 0.25rem;
  border: none;
  border-radius: var(--border-radius);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.conf__entry:hover,
.conf__entry.is-open {
  background: rgba(var(--text-color), 0.09);
}

/* Collapsed, an entry is only its avatar — the name, status and action all
   belong to the expanded rail. */
.conf__entry-info,
.conf__entry-action,
.conf__side-tabs {
  display: none;
}

.scene--conference[data-panel="expanded"] .conf__entry-info {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  flex: 1 1 auto;
  min-width: 0;
}

.scene--conference[data-panel="expanded"] .conf__entry-action {
  display: block;
  width: 0.85rem;
  height: 0.85rem;
  opacity: 0.6;
}

.scene--conference[data-panel="expanded"] .conf__side-tabs {
  display: flex;
  gap: 0.25rem;
  justify-content: center;
}

/* The single button that opens the rail; hidden once it is open, as the app
   swaps it for the tab row. */
.scene--conference[data-panel="expanded"] .conf__side-open {
  display: none;
}

.conf__entry-name {
  font-size: 0.78rem;
  font-weight: 600;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* user-status-btn.component — a pill carrying the status text. */
.conf__status-pill {
  align-self: flex-start;
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  font-size: 0.62rem;
  background: rgba(var(--success-color), 0.18);
  color: rgba(var(--success-color), 1);
}

.conf__status-pill.is-away {
  background: rgba(var(--warning), 0.2);
  color: rgb(var(--warning));
}

.conf__side-actions {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  width: 100%;
  align-items: center;
}

.conf__side-open,
.conf__side-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.8rem;
  height: 1.8rem;
  padding: 0;
  border: 1px solid rgba(var(--text-color), 0.08);
  border-radius: var(--border-radius);
  background: rgba(var(--text-color), 0.08);
  color: inherit;
  cursor: pointer;
}

.conf__side-open:hover,
.conf__side-tab:hover {
  background: rgba(var(--text-color), 0.16);
}

.conf__side-open .icon,
.conf__side-tab .icon {
  width: 0.9rem;
  height: 0.9rem;
  opacity: 0.85;
}

/* -------------------------------------------------------------------------
   Profile dialogs — settings-profile.component.html and
   participant-mini-profile.component.html. Anchored over the rail, matching
   the app's context menus (xPosition="before").
   ------------------------------------------------------------------------- */

/*
 * The dialogs are laid out by this container rather than positioned by hand.
 * A fixed `top` fitted the shorter mini-profile but left the taller profile
 * settings hanging 2px past the stage, where `overflow: hidden` clipped it —
 * and any future dialog would have re-broken it. Centring inside a padded box
 * cannot overflow whatever the height.
 */
.conf__profiles {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0.6rem;
  /* Clear of the expanded rail where there is room; over it when there is
     not. */
  padding-right: clamp(0.6rem, calc(100% - 15.5rem), 12rem);
  pointer-events: none;
}

.conf__profile {
  position: relative;
  width: 15rem;
  max-width: 100%;
  max-height: 100%;
  display: none;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.7rem;
  border-radius: var(--border-radius);
  z-index: 0;
  border: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 8px 26px rgba(0, 0, 0, 0.35);
  --surface-tint: rgba(var(--background), 0.9);
  --surface-blur: blur(12px) saturate(1.2);
  pointer-events: auto;
}

.scene--conference[data-profile="self"] [data-profile-panel="self"],
.scene--conference[data-profile="peer"] [data-profile-panel="peer"] {
  display: flex;
}

.conf__profile-title {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
}

.conf__label {
  font-size: 0.72rem;
  opacity: 0.75;
}

.conf__field-row {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.conf__field {
  display: flex;
  align-items: center;
  padding: 0.3rem 0.5rem;
  border-radius: 0.5rem;
  border: 1px solid rgba(var(--accent-color), 0.35);
  background: rgba(var(--text-color), 0.05);
}

.conf__field-value {
  font-size: 0.78rem;
}

.conf__field-value.is-placeholder {
  opacity: 0.45;
}

.conf__toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

/* --- Avatar picker -------------------------------------------------------- */

.conf__avatar-picker {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem;
  border-radius: 0.6rem;
  border: 1px dashed rgba(var(--text-color), 0.25);
}

.conf__avatar-shot {
  position: relative;
  flex: 0 0 auto;
}

.conf__avatar--picker {
  width: 2.2rem;
  height: 2.2rem;
  font-size: 0.95rem;
}

.conf__avatar-cam {
  position: absolute;
  right: -0.15rem;
  bottom: -0.15rem;
  width: 0.8rem;
  height: 0.8rem;
  padding: 0.12rem;
  border-radius: 50%;
  background: rgba(var(--background), 0.9);
}

.conf__avatar-hint {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex: 1 1 auto;
  min-width: 0;
}

.conf__avatar-hint .icon {
  width: 0.9rem;
  height: 0.9rem;
  opacity: 0.7;
}

.conf__avatar-hint-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.conf__avatar-hint-text strong {
  font-size: 0.75rem;
}

.conf__avatar-hint-text small {
  font-size: 0.65rem;
  opacity: 0.6;
}

.conf__avatar-remove {
  width: 0.85rem;
  height: 0.85rem;
  opacity: 0.6;
}

.conf__note {
  display: flex;
  gap: 0.35rem;
  margin: 0;
  font-size: 0.65rem;
  line-height: 1.4;
  opacity: 0.65;
}

.conf__note .icon {
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
  margin-top: 0.1rem;
}

/* --- Mini profile --------------------------------------------------------- */

.conf__mini-head {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}

.conf__mini-identity {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.conf__mini-name-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.conf__mini-name {
  font-size: 0.85rem;
  font-weight: 600;
}

.conf__status-dot {
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: rgba(var(--success-color), 1);
}

.conf__mini-role {
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  font-size: 0.6rem;
  background: rgba(var(--accent-color), 0.2);
  color: rgb(var(--accent-color));
}

.conf__mini-status {
  font-size: 0.7rem;
  opacity: 0.7;
}

.conf__mini-meta {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.65rem;
  opacity: 0.7;
}

.conf__mini-joined {
  display: flex;
  align-items: center;
  gap: 0.2rem;
}

.conf__mini-meta .icon,
.conf__mini-badge .icon {
  width: 0.65rem;
  height: 0.65rem;
}

/* mat-slider — a track with the filled portion and a thumb. */
.conf__slider {
  position: relative;
  display: block;
  height: 0.25rem;
  border-radius: 999px;
  background: rgba(var(--text-color), 0.18);
}

.conf__slider-fill {
  position: absolute;
  inset: 0 45% 0 0;
  border-radius: 999px;
  background: rgb(var(--accent-color));
}

.conf__slider-thumb {
  position: absolute;
  top: 50%;
  left: 55%;
  transform: translate(-50%, -50%);
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 50%;
  background: rgb(var(--accent-color));
}

.conf__mini-actions {
  display: flex;
  gap: 0.3rem;
}

/* --- Scene states for the new steps --------------------------------------- */

[data-scene="side-collapsed"] .conf__main,
[data-scene="side-collapsed"] .conf__rail,
[data-scene="side-expanded"] .conf__main,
[data-scene="side-expanded"] .conf__rail {
  opacity: 0.35;
}

.conf__rail {
  transition: opacity var(--animation-duration) var(--animation-timefunc);
}

[data-scene="side-collapsed"] [data-spot="side-open"],
[data-scene="side-expanded"] [data-spot="side-tabs"] {
  animation: scene-spotlight 1.6s ease-in-out infinite;
  border-radius: var(--border-radius);
  z-index: 4;
}

[data-scene="profile-self"] [data-profile-panel="self"],
[data-scene="profile-peer"] [data-profile-panel="peer"] {
  animation: scene-dialog-glow 1.6s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  [data-scene="side-collapsed"] [data-spot="side-open"],
  [data-scene="side-expanded"] [data-spot="side-tabs"] {
    animation: none;
    box-shadow:
      0 0 0 2px rgba(var(--accent-color), 1),
      0 0 18px 4px rgba(var(--accent-color), 0.6);
  }

  [data-scene="profile-self"] [data-profile-panel="self"],
  [data-scene="profile-peer"] [data-profile-panel="peer"] {
    animation: none;
    box-shadow:
      0 8px 26px rgba(0, 0, 0, 0.35),
      0 0 0 2px rgba(var(--accent-color), 1);
  }
}

/* =========================================================================
   Frosted surfaces — shadow and blur on separate boxes

   Chromium rasterizes an element's box-shadow into the same render surface as
   its backdrop-filter and intermittently draws the two a frame out of sync
   while ancestors animate or recomposite, so the surface flickers
   (crbug.com/41471914). This page animates constantly — crossfades, spotlight
   keyframes, a zoomed pinned stage — which makes it easy to hit.

   `.glass` in styles.css already splits them. Every surface written by hand
   afterwards did not, so they are corrected here in one place: the tint and
   the blur move onto an inset ::before, the shadow stays on the host.

   The tint has to move too. The pseudo's own background paints *above* its
   blur result, whereas a background left on the host would sit beneath it and
   become blur input — leaving a flat panel with no visible blur at all.

   Sets no z-index: hosts that already have one keep it (that is enough of a
   stacking context to contain the pseudo); the two that had none get
   `z-index: 0` at their own rule.
   ========================================================================= */

.scene-tip,
.scene__dialog,
.scrolly__outro,
.conf__dock,
.conf__profile {
  background: none;
}

.scene-tip::before,
.scene__dialog::before,
.scrolly__outro::before,
.conf__dock::before,
.conf__profile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: var(--surface-tint, transparent);
  backdrop-filter: var(--surface-blur, none);
  -webkit-backdrop-filter: var(--surface-blur, none);
  pointer-events: none;
}
