/* Shared voice-rail styles. Source of truth: web/lobby/voice.css - the copy inside a game's
   WebGL template is written by tools/sync-lobby.sh, exactly like lobby.css.

   The GEOMETRY below is the original IG platform rail, recovered from
   packages/nextjs/components/community/elements/sidebar/ and written down in VOICE_PLAN.md:
   60px wide, 32px slots, 40px buttons, 15px rhythm, 10px radii.

   The COLOUR is not. The original was indigo (#202957 -> #192043) with a mint speaking halo,
   and Jordan replaced both on 2026-08-27: near-black to sit with this app's own #04060d shell,
   and gold for "talking" instead of the green. Still absolute rather than themed by
   LOBBY_CONFIG.palette - the rail is the one surface identical in every game, which is what
   makes "am I in voice" a thing a player learns once.

   Nothing here uses backdrop-filter. Same ban, same reason, as .lobby-round-btn in lobby.css
   (which is where the ban is stated now that v1.22 deleted the two in-match floats that carried
   that plate): this element overlays the live WebGL canvas for the whole match, and a blur over it
   costs a compositor readback of the game's output every single frame. */

:root {
  --voice-rail-w: 60px;
  /* The wash the rail lays over the page's own gradient. Blue-grey rather than black, and at
     80% rather than opaque, so the gradient underneath still reads through it - at pure black
     the rail was a dead column, because the thing it was 80% over is itself near-black
     (--lobby-bg is #04060d) and 80% of nothing is nothing.
     Lifting the tint off black is what buys the translucency somewhere to show. */
  --voice-wash: rgba(22, 28, 44, 0.8);
  /* And with the surfaces this close in value, the SEAM is what separates them - so the border
     does that work rather than a contrast step that is not there. */
  --voice-rail-border: rgba(255, 255, 255, 0.2);
  /* The empty-slot ghost. On black a coloured ghost reads as a filled slot. */
  --voice-ghost: #ffffff;
  /* White: the speaking halo, the icon hover, the tooltip, the join state. The rail's ONE
     accent (Jordan, 2026-08-27 - gold before that, mint in the original). White is doing real
     work here rather than dodging a colour choice: the rail sits over a live game whose palette
     nobody controls, and white is the only accent that cannot collide with it. */
  --voice-accent: #ffffff;
  /* The near-black the accent is legible on - tooltip ink and the avatar's ring. */
  --voice-ink: #0b0c10;
  /* The base ink of every rail icon. #B8BCE0 is the OG platform's `night1`, adopted 2026-08-31
     with the game controls (RAIL_GAME_CONTROLS_PLAN) so the rail's icons read as one set again —
     it was #8b8fa3, a half-step darker, which left the borrowed OG glyphs looking dimmer than the
     ones drawn for this rail. The HOVER accent is untouched and stays white; no mint anywhere. */
  --voice-icon: #B8BCE0;
  --voice-slot: 32px;
  --voice-btn: 40px;
  /* The one vertical rhythm of the module: slot gaps AND the rail's own padding. */
  --voice-gap: 15px;
  --voice-radius: 10px;
}

/* Fixed, full height, and a SIBLING of #lobby-overlay rather than a child of it (see the mount
   comment in voice.js): the overlay is display:none for the whole "playing" phase and the rail
   has to survive that.

   z-index 55 sits above the overlay (20) and its host-unavailable veil (50), so the rail is
   never covered by the lobby, and below the body-level float controls (60) and the exit-confirm
   modal (70), so a question being asked is always on top of it. Hidden until voice.js has a
   roster with this player in it. */
.voice-rail {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  z-index: 55;
  box-sizing: border-box;
  width: var(--voice-rail-w);
  padding: var(--voice-gap) 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Bottom-pinned: the original's voice module hung off the bottom of the sidebar. */
  justify-content: flex-end;
  gap: var(--voice-gap);
  /* No background of its own: .voice-backdrop, its first child, paints both the gradient and
     the black wash over it. See that rule for why it is a separate node. */
  border-right: 1px solid var(--voice-rail-border);
  /* Nothing here transitions on mount: the rail is part of the layout from first paint, and a
     fade-in would be the same visual event as the shift it exists to avoid. */
  /* font-family is inherited and this node is on document.body, where the document default is a
     serif - the same Times New Roman bug .lobby-float fixes for the other injected controls. */
  font-family: var(--lobby-display, "Unbounded", "Arial Black", -apple-system, sans-serif);
  -webkit-user-select: none;
  user-select: none;
}

/* Dormant: no seat yet, no reachable backend, or a deployment with no LiveKit at all. The rail
   still stands and still occupies its 60px - only the ink changes. Buttons stop inviting a press;
   the stack keeps its ghosts. */
.voice-rail-dormant .voice-btn {
  opacity: 0.4;
  cursor: default;
}

.voice-rail-dormant .voice-btn:hover {
  color: var(--voice-icon);
  background: rgba(255, 255, 255, 0.04);
}

.voice-rail-dormant .voice-tip {
  display: none;
}

/* The rail's whole surface, and the reason it is a NODE rather than a background on .voice-rail.

   Jordan (2026-08-27): the rail should be black at ~80% with the page's gradient showing through,
   and that gradient should be THE SAME whether the lobby is up or a match is being played.

   Those two things fight. The gradient belongs to #lobby-overlay, which is display:none for the
   whole "playing" phase, and behind it during a match is the Unity canvas - opaque, so nothing
   can literally show through it. A translucent rail would therefore be washed lobby-gradient in
   the lobby and washed arcade-scene in game: two different surfaces.

   So the rail paints its own copy. `--lobby-backdrop` is a stack of radial gradients sized to the
   VIEWPORT, so painting it into a 60px box would squeeze all three into 60px and look nothing
   like the lobby's left edge. `background-size: 100vw 100vh` on a box pinned to 0,0 renders it at
   viewport scale and lets the box clip it - which is exactly the left 60px slice the lobby shows.
   The result is pixel-identical in both phases, which is what was actually asked for.

   The ::after is the 80% black over it. Both are pointer-events:none; the controls sit above. */
.voice-backdrop {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--voice-rail-w);
  /* A slight blue-grey lift over the page's own gradient. The gradient is sized to the VIEWPORT
     and this box is pinned to 0,0, so the box clips exactly the left 60px slice the lobby shows -
     painting --lobby-backdrop into a 60px box instead would squeeze all three radials into 60px
     and look nothing like the page behind it. */
  background:
    linear-gradient(rgba(34, 43, 66, 0.55), rgba(34, 43, 66, 0.55)),
    var(--lobby-backdrop, #04060d);
  background-size: 100vw 100vh;
  background-repeat: no-repeat;
  background-position: 0 0;
  /* THE point of this element. 0.8, on the layer itself, so the rail is genuinely 80% opaque and
     whatever is behind it reads through - the lobby's gradient, or the game.

     This is why the opaque colour had to come out of the stack above: --lobby-backdrop ends in
     `var(--lobby-bg)`, an OPAQUE #04060d, so every wash layered over it composited to a solid
     surface however translucent the wash itself was. 80% of an opaque base is still opaque. */
  opacity: 0.8;
  pointer-events: none;
}

/* ---- the wordmark ---- */

/* The rail's logo, pinned top. A LINK home on every page since 2026-08-30 (Jordan: "the logo at
   the top should always bring you back to the home page") - so it takes the pointer, and a hover
   lifts it to full white the way a control should. z-lifted over .voice-backdrop for the same
   reason .voice-nav is. */
.voice-logo {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  margin-bottom: auto;
  color: #ffffff;
  opacity: 0.92;
  cursor: pointer;
}

.voice-logo:hover,
.voice-logo:focus-visible {
  opacity: 1;
}

.voice-logo svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* ---- game-page nav folded into the rail (WORDY_WATERWAY_PLAN Phase 1 step 0) ----

   Game pages have no top header any more: the site's global nav lives in the rail. The Leaderboard
   link sits directly under the logo, and the account + claim-host controls hang at the foot. The
   `margin-bottom: auto` that pins the top group lives on `.voice-top`, the cluster's wrapper — see
   that rule for why it is no longer on either of the two nodes it wraps.

   Unscoped since 2026-08-30: the shell's dock renders the SAME Rail (nav included) and the shell
   pages lost their header, so the trophy must sit in the same spot on every page. */
.voice-rail .voice-logo {
  margin-bottom: 0;
}

/* Every rail nav control is POSITIONED and z-lifted: .voice-backdrop is a fixed first child that
   paints above any static sibling (the same reason .voice-btn is positioned), so without this the
   80% wash sits on the control and it reads as disabled.

   TRANSPARENT at rest since 2026-08-31 (RAIL_GAME_CONTROLS_PLAN, the OG's IconButton): the resting
   4% plate drew a visible square behind every icon, and Jordan's ask was the icon alone. The hover
   plate stays — it is what says "this is pressable", and it now only appears when it is true. */
.voice-nav {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  width: var(--voice-btn);
  height: var(--voice-btn);
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--voice-radius);
  background: none;
  color: var(--voice-icon);
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease;
}

.voice-nav:hover {
  color: var(--voice-accent);
  background: rgba(255, 255, 255, 0.1);
}

.voice-nav .voice-btn-icon {
  display: flex;
  width: 20px;
  height: 20px;
}

.voice-nav .voice-btn-icon svg {
  width: 100%;
  height: 100%;
}

/* The top cluster's wrapper (RAIL_GAME_CONTROLS_PLAN). It carries the spacer that pushes the bottom
   cluster down — the auto that used to be on the logo, then on the Leaderboard link. It lives on a
   WRAPPER now because neither of those nodes is unconditional any more: in a match the logo is
   replaced by Exit and the Leaderboard link is not rendered at all, and an auto margin on a node
   that comes and goes would drop the whole cluster to the middle of the rail.

   Its gap is the rail's own, so logo + Leaderboard sit exactly where they always did. */
.voice-top {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--voice-gap);
  margin-bottom: auto;
}

/* ---- the in-game controls (RAIL_GAME_CONTROLS_PLAN) ---- */

/* Exit. The one rail icon that does NOT take the white hover: it is #FC5E5E (the OG's
   functional-red) at rest and on hover alike, because it is the danger control and a colour that
   drains towards the neutral accent on approach would say the opposite of what it means. Only the
   plate answers the pointer, and it answers in the same red. */
.voice-exit,
.voice-exit:hover {
  color: #fc5e5e;
}

.voice-exit:hover {
  background: rgba(252, 94, 94, 0.15);
}

/* The exit's slot with no exit in it (under the veil, or on a game without the capability). Same
   40px box, nothing drawn, nothing pressable — it exists only so the mute button below keeps its
   y. See the .voice-account-hold block for the same idea at the other end of the rail. */
.voice-exit-hold {
  background: none;
  cursor: default;
  pointer-events: none;
}

/* Mute wears the standard nav skin — base ink, white on hover, no resting plate — so nothing extra
   is needed for it here. The red slash inside its muted glyph is baked into the SVG (constants.ts),
   deliberately outside currentColor's reach. */

/* The account control at the foot. Signed-out is a dashed ring (a placeholder identity); signed-in
   is a filled avatar with the account's initial. */
.voice-account-out {
  border: 1px dashed rgba(255, 255, 255, 0.35);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
}

.voice-account-out:hover {
  border-color: rgba(255, 255, 255, 0.55);
}

.voice-account-in {
  border-radius: 999px;
  background: none;
}

/* The slot before the session has been checked (LOBBY_LOADING_PLAN Part 1): the same 40px the two
   controls above take, and nothing in it — no ring, no avatar, no hover, nothing to press. It is
   held for the sub-second lobby.js needs to answer "am I signed in", so the rail never offers the
   wrong identity and never changes shape when the answer lands. */
.voice-account-hold {
  background: none;
  cursor: default;
}

.voice-account-hold:hover {
  background: none;
}

/* The account menu: a flyout to the RIGHT of the rail, anchored to its foot beside the button
   that opened it — the same skin and anchoring as .voice-settings above, but mounted only while
   open (React renders it conditionally), so no opacity/pointer-events gate is needed. */
.voice-account-menu {
  position: absolute;
  left: calc(100% + 10px);
  bottom: var(--voice-gap);
  min-width: 150px;
  padding: 6px;
  box-sizing: border-box;
  border-radius: 12px;
  background: #16181d;
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
}

.voice-account-menu-row {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: 8px;
  background: none;
  font-family: var(--lobby-body, "Inter", sans-serif);
  font-size: 13px;
  font-weight: 600;
  text-align: left;
  color: #d5d8e4;
  cursor: pointer;
  white-space: nowrap;
}

.voice-account-menu-row:hover {
  background: rgba(255, 255, 255, 0.06);
  color: #ffffff;
}

.voice-account-avatar {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 999px;
  background: #2c3568;
  color: #ffffff;
  box-shadow: 0 0 0 1px var(--voice-ink);
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
  overflow: hidden;
}

.voice-account-portrait {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The nav controls' tooltips reuse .voice-tip, which is revealed on .voice-btn hover — restate the
   reveal for the nav's own hover/focus. */
.voice-nav:hover .voice-tip,
.voice-nav:focus-visible .voice-tip {
  opacity: 1;
  transform: translate(0, -50%);
}

/* ---- controls ---- */

.voice-controls {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.voice-btn {
  position: relative;
  width: var(--voice-btn);
  height: var(--voice-btn);
  flex: 0 0 auto;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--voice-radius);
  background: rgba(255, 255, 255, 0.04);
  color: var(--voice-icon);
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease;
}

.voice-btn:hover {
  color: var(--voice-accent);
  background: rgba(255, 255, 255, 0.10);
}

/* The two toggles' OFF state (mic muted, ears deafened). The rail says nothing in words, so the
   colour has to: the lobby's own danger red, not a dimmer grey, because a muted mic the player
   has forgotten about is the failure this button exists to prevent. */
.voice-btn-off,
.voice-btn-off:hover {
  color: var(--lobby-danger, #ff6b6b);
  background: rgba(255, 107, 107, 0.12);
}

/* The microphone the browser REFUSED. Amber, because it is the one mic state a click cannot
   fix - the player has to grant permission in the browser's own UI first. Deliberately not the
   danger red the muted state wears: muted is a choice, blocked is an obstacle. */
.voice-btn-warn,
.voice-btn-warn:hover {
  color: #ffc24b;
  background: rgba(255, 194, 75, 0.14);
}

/* Joining: the click landed, the 590 KB client and the mic prompt are in flight. */
.voice-btn-busy {
  opacity: 0.6;
  cursor: default;
}

.voice-btn-icon {
  display: flex;
  width: 20px;
  height: 20px;
}

.voice-btn-icon svg {
  width: 100%;
  height: 100%;
}

/* The original's tooltip, in this app's gold: a pill to the RIGHT of the rail with dark bold text, sliding in
   from 8px left over 300ms. Rendered always and revealed on hover, so nothing is built or
   measured at hover time. pointer-events:none so it can never sit between the pointer and the
   button it describes. */
.voice-tip {
  position: absolute;
  left: calc(100% + 10px);
  top: 50%;
  transform: translate(-8px, -50%);
  opacity: 0;
  pointer-events: none;
  padding: 6px 10px;
  border-radius: 8px;
  background: var(--voice-accent);
  color: var(--voice-ink);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  white-space: nowrap;
  transition: opacity 300ms ease, transform 300ms ease;
}

.voice-btn:hover .voice-tip,
.voice-btn:focus-visible .voice-tip {
  opacity: 1;
  transform: translate(0, -50%);
}

/* ---- the speaker stack ---- */

.voice-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--voice-gap);
}

.voice-slot {
  position: relative;
  width: var(--voice-slot);
  height: var(--voice-slot);
  flex: 0 0 auto;
  border-radius: var(--voice-radius);
}

/* The ghost ramp: white at 2% at the top of the stack, 10% at the bottom. The gradient down the
   five slots is what makes the bottom read as the live end - which is the end voice.js fills
   first. The positions are fixed because the five nodes are built once and only re-dressed. */
.voice-slot-empty {
  background: var(--voice-ghost);
}

.voice-stack .voice-slot-empty:nth-child(1) {
  opacity: 0.02;
}
.voice-stack .voice-slot-empty:nth-child(2) {
  opacity: 0.04;
}
.voice-stack .voice-slot-empty:nth-child(3) {
  opacity: 0.06;
}
.voice-stack .voice-slot-empty:nth-child(4) {
  opacity: 0.08;
}
.voice-stack .voice-slot-empty:nth-child(5) {
  opacity: 0.1;
}

/* The gold halo, and the reason the plate is a sibling rather than a shadow: it is scaled to
   120%, so it has to be its own box behind the avatar. Invisible until its occupant is actually
   speaking - an occupied-but-quiet slot is just the avatar, waiting out its 20s watchdog. */
.voice-plate {
  position: absolute;
  inset: 0;
  border-radius: 12px;
  transform: scale(1.2);
  background: linear-gradient(180deg, var(--voice-accent) 0%, rgba(255, 255, 255, 0.45) 100%);
  opacity: 0;
}

.voice-slot-speaking .voice-plate {
  opacity: 1;
  animation: voice-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes voice-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

/* The avatar is initials for now (D3 - real images arrive with the accounts work). The 1px ring
   is a box-shadow rather than a border so it does not eat into the 32px the original specifies,
   and it is what separates the plate's gold from the avatar underneath. */
.voice-avatar {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  border-radius: var(--voice-radius);
  box-shadow: 0 0 0 1px var(--voice-ink);
  background: #2c3568;
  color: #ffffff;
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
}

.voice-slot-filled .voice-avatar {
  display: flex;
}

/* The rail's one animation, so it is the one thing this file has to turn off. The halo still
   shows - a speaker is still marked - it simply stops breathing. */
@media (prefers-reduced-motion: reduce) {
  .voice-slot-speaking .voice-plate {
    animation: none;
  }

  .voice-btn,
  .voice-tip {
    transition: none;
  }
}

/* ---- Phase C: what voice knows, drawn on the lobby's own player cards ----

   lobby.js owns the markup (buildPlayerCard: classes on the card, at most one badge node inside
   .lobby-card-avatar); this file owns every pixel of it. The names are `lobby-` prefixed rather
   than `voice-` because they land on a lobby card, and a reader looking at that card in the
   inspector should not have to know which file it came from to find the rule.

   Cheap on purpose: box-shadow and a 9px dot, no layout properties at all. These classes come and
   go on every speaking event, and a card that reflowed when somebody started talking would shove
   the whole column around mid-conversation. */

/* ---------- the room the rail takes ----------

   The rail is `position: fixed`, so it overlaps the page rather than displacing it — and
   .lobby-panel centres inside #lobby-overlay, which starts at x=0. Without this the panel loses
   its first 60px underneath the rail and the headings lose their initials ("HOW TO PLAY" reading
   as "OW TO PLAY").

   UNCONDITIONAL, and that is the point (Jordan, 2026-08-27). This used to hang off a class that
   voice.js added once it knew voice was reachable, which meant the whole lobby jumped 60px
   sideways the moment a token request came back. The rail is furniture: it is there from first
   paint whether or not anyone can talk, so the space it takes is reserved from first paint too.
   The cost is that a page which can never join a call still gives up 60px — accepted, because a
   stable layout is worth more than 60px on a page nobody is talking on. */
#lobby-overlay {
  /* The rail's width PLUS the overlay's own gutter - not the width alone. `padding-left` replaces
     the left value of the shorthand rather than adding to it, so setting it to 60px put the
     content flush against the rail while the right edge kept its full gutter. Adding the two
     lands the content the same distance from the rail as it is from the right edge. */
  padding-left: calc(var(--voice-rail-w) + var(--lobby-pad-x, clamp(20px, 4vw, 56px)));
}

/* Results mode zeroes the overlay's padding to go full-bleed (lobby.css) — which put the results
   screen UNDER the rail (Jordan, 2026-08-31: rail beside it, not on top). Restate just the rail's
   width, no gutter: the results screen owns its own edges, it only has to clear the furniture.
   This file loads after lobby.css so the longhand wins there; the shell's game pages restate the
   same left value in their own results-mode rule (src/game/game.css), which loads after this. In
   an embed --voice-rail-w is collapsed to 0 and this pads nothing, like everything else rail. */
#lobby-overlay.lobby-results-mode {
  padding-left: var(--voice-rail-w);
}

/* And the same room for the GAME, which is the other thing the rail was sitting on top of
   (Jordan, 2026-08-28: "flex box in a row" - rail and game side by side, not stacked).

   It is not an actual flex row, because the rail cannot stop being `position: fixed`: it has to
   survive #lobby-overlay going display:none for the whole "playing" phase, and a flex item can
   only displace a sibling it is actually laid out beside. The equivalent is to hand the canvas
   the space the rail is not using - which is geometrically identical here precisely because the
   rail's 60px is unconditional furniture (see above): the gap is always there, so subtracting it
   is always right.

   TemplateData/style.css (both games) has `#unity-container { position:absolute; inset:0;
   width:100% }` and `#unity-canvas { width:100vw }`, and voice.css is the LAST stylesheet in
   both templates, so these override cleanly. `width:auto` is load-bearing: without it the
   `width:100%` from the shorthand's rule would keep the box a full viewport wide and simply push
   its right edge 60px off-screen. left + right + auto lets the used width fall out as the
   remainder. Height is untouched - top/bottom stay 0 and the canvas keeps its 100vh.

   No build is touched by any of this. The templates leave `matchWebGLToCanvasSize` at its
   default on purpose (there is a comment in index.html saying so), so Unity re-sizes its render
   target to whatever DOM size the canvas reports: a 60px-narrower canvas re-renders at the
   correct resolution rather than being squashed, and the game centres in its own, rail-excluded
   box - which is what "beside, not under" means.

   The mock harness has no #unity-container, so both rules match nothing there. */
#unity-container {
  left: var(--voice-rail-w);
  width: auto;
  right: 0;
}

#unity-canvas {
  width: 100%;
}

/* v1.22 (RAIL_GAME_CONTROLS_PLAN) removed the rule that pushed the floating exit button clear of
   the rail: the exit is a rail button now (.voice-exit), so there is no float left to dodge. */

/* The spectating chip hangs over the CANVAS, so its centre is the canvas's centre - and the canvas
   now starts at x=60. lobby.css centres it on the viewport (left:50% + translateX(-50%)), which
   lands it half a rail too far left; adding half the rail width back puts it over the middle of
   the thing it is talking about. Unconditional for the same reason the padding above is: the rail
   reserves its 60px whether or not anyone can talk, so the offset is always right. */
.lobby-spectating {
  left: calc(50% + var(--voice-rail-w) / 2);
}

/* ---------- voice settings ----------

   A flyout to the RIGHT of the rail, anchored to its bottom so it grows up from the button that
   opened it. Absolutely positioned and pointer-events-gated rather than display-toggled: the
   rail is a fixed 60px column and a panel that participated in its layout would widen it. */
.voice-settings {
  position: absolute;
  left: calc(100% + 10px);
  bottom: var(--voice-gap);
  width: 220px;
  padding: 10px;
  box-sizing: border-box;
  border-radius: 12px;
  background: #16181d;
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
  opacity: 0;
  transform: translateX(-6px);
  pointer-events: none;
  transition: opacity 160ms ease, transform 160ms ease;
}

.voice-settings-open {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.voice-settings-title {
  margin: 6px 4px 4px;
  font-family: var(--lobby-body, "Inter", sans-serif);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: #6f7488;
}

.voice-settings-note {
  margin: 2px 4px;
  font-family: var(--lobby-body, "Inter", sans-serif);
  font-size: 12px;
  color: #8b8fa3;
}

.voice-settings-row {
  display: block;
  width: 100%;
  padding: 7px 8px;
  border: none;
  border-radius: 8px;
  background: none;
  font-family: var(--lobby-body, "Inter", sans-serif);
  font-size: 12px;
  text-align: left;
  color: #d5d8e4;
  cursor: pointer;
  /* Device names run long and the panel may not grow: one line, clipped. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.voice-settings-row:hover {
  background: rgba(255, 255, 255, 0.06);
}

.voice-settings-row-on {
  color: var(--voice-accent);
  background: rgba(255, 255, 255, 0.12);
}

/* ---------- the dock on the pages that are not a game ----------

   SHELL_PLAN.md §3b (Jordan, 2026-08-28): the rail is site furniture, so it stands on / and
   /leaderboard/ too — in a DISABLED state, with no voice.js, no LiveKit and no session behind it.
   The shell renders the same nodes (web/shell/src/VoiceDock.tsx) and adds this one class to the
   root.

   ADDITIVE, and that is the whole contract of this block: every selector below is scoped inside
   .voice-dock-disabled, a class no game page ever carries, so nothing a game computes changes.
   The rules are the dormant rail's, said again here rather than borrowed from
   .voice-rail-dormant — a dock with nothing behind it is not the same state as a rail waiting for
   a roster, and coupling them would make a change to one silently change the other. */
.voice-dock-disabled .voice-btn {
  opacity: 0.4;
  cursor: default;
  /* Belt and braces with the `disabled` attribute the markup carries: no hover ink, no press. */
  pointer-events: none;
}

.voice-dock-disabled .voice-tip {
  display: none;
}

/* ---- the leaderboard modal (2026-09-03, leaderboardOverlay.ts) ----
   Pressing the rail's trophy on a game page opens /leaderboard/ in a modal instead of navigating
   away, so the lobby (and the voice session) stays alive underneath. The game switcher's grammar
   (lobby.css .lobby-modal-veil / .lobby-game-modal), the numbers RESTATED rather than borrowed —
   this sheet must stand on a page whose lobby.css did not load: a dimmed, blurred veil whose 24px
   padding is the only inset, and inside it one card at the full width and height that leaves.
   z-index 8000 — above the rail's 55 (the rail's own trophy is what opened it) but below the auth
   modal's 9000: the sign-in card inside the FRAME is the one in play, and the page-level one must
   still win if something opens it. */
.voice-lb-veil {
  position: fixed;
  inset: 0;
  z-index: 8000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  box-sizing: border-box;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  /* The switcher's arrival (lobby.css lobby-game-veil-in, restated): tint, blur and card fade in
     as one. The blur is animated explicitly because element opacity does not scale a
     backdrop-filter. */
  animation: voice-lb-veil-in 0.15s ease;
}

@keyframes voice-lb-veil-in {
  from {
    opacity: 0;
    backdrop-filter: blur(0);
    -webkit-backdrop-filter: blur(0);
  }
}

/* The card: .lobby-modal's surface at the veil's full width and height. position:relative is the
   X's anchor; overflow hidden is what clips the square iframe to the card's corners. The
   background is the leaderboard page's own (#030309, its index.html pre-paint), so the frame's
   arrival doesn't flash a differently-lit panel. */
.voice-lb-modal {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: #030309;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}

/* Transparent until its document has actually loaded (leaderboardOverlay.ts adds the class), so
   the board fades up over the card's matching background instead of popping mid-boot. */
.voice-lb-frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.voice-lb-frame-ready {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .voice-lb-veil {
    animation: none;
  }

  .voice-lb-frame {
    opacity: 1;
    transition: none;
  }
}

/* The back arrow (Jordan, 2026-09-14 — was a top-right X) — the game modal's 34px round close
   (.lobby-game-modal-close's numbers), floated over the frame's top-LEFT corner so it reads as
   "back", like the site's corner back button; ?overlay=1 pads the page under it clear
   (src/styles.css). */
.voice-lb-close {
  position: absolute;
  top: 14px;
  left: 14px;
  width: 34px;
  height: 34px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.07);
  color: var(--voice-icon);
  cursor: pointer;
}

.voice-lb-close:hover {
  background: rgba(255, 255, 255, 0.15);
  color: #ffffff;
}

.voice-lb-close svg {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
}

/* ---------- the embed rail (Jordan, 2026-09-11) ----------

   A partner embed shows the rail again, curated: the in-match Exit, the game-volume toggle and
   the Leaderboard stay; everything that navigates home, edits identity or touches VOICE goes.
   Voice AUDIO never runs in an embed regardless (the LobbyVoice.update guard in the voice
   bundle) — these rules only stop the dormant controls from being drawn. Keyed off the
   `data-ig-embed` attribute markEmbedDocument() writes before first paint. */
[data-ig-embed] .voice-logo,
[data-ig-embed] .voice-top a[href="/character/"],
[data-ig-embed] .voice-stack,
[data-ig-embed] .voice-controls,
[data-ig-embed] .voice-settings,
[data-ig-embed] .voice-account,
[data-ig-embed] .voice-account-menu {
  display: none;
}
