/*
 * Copyright (C) 2025-2026 Tim Riker <timriker@gmail.com>
 * Licensed under the GNU Affero General Public License v3.0 (AGPLv3).
 * Source: https://github.com/timriker/bzo
 * See LICENSE or https://www.gnu.org/licenses/agpl-3.0.html
 */

html, body {
  margin: 0;
  padding: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: #000;
  font-family: 'Arial', sans-serif;
  color: #fff;
  touch-action: none;
  overscroll-behavior: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -webkit-app-region: no-drag;
}

:root {
  --color-bg: #000;
  --color-fg: #fff;
  --color-accent: #4CAF50;
  --color-accent-light: #66bb6a;
  --color-state-on: #66bb6a;
  --color-state-off: #e57373;
  --color-btn-bg: rgba(76,175,80,0.15);
  --color-btn-bg-active: #4CAF50;
  --color-btn-bg-hover: rgba(76,175,80,0.35);
  --color-btn-bg-focus: #81c784;
  --color-btn-bg-shadow: #4CAF5088;
  --color-panel-bg: rgba(0,0,0,0.5);
  --color-panel-bg-solid: rgba(0,0,0,0.8);

  /* BZFlag's targeting box, in its own units: MaxMotionSize 37 and NoMotionSize
     10 half-extents (global.h:88-91) scaled by min(width / 256, height / 192),
     which is what HUDRenderer::resize computes at the default mouseboxsize of
     5, where its effScale equals that scale. The mouse mapping reads its
     geometry back off these boxes, so the drawn edge is where the input
     saturates. */
  /* The radar's own size, shared so the debug HUD can stop short of it rather
     than guess. `resizeRadar` in client.js sets it to the size it gives the
     radar, which grows into the room the targeting box leaves; this is only
     what it starts as. */
  --radar-size: clamp(120px, min(37.5vw, 37.5vh), 390px);
  --hud-edge-gap: 10px;

  --motion-unit: min(calc(100vw / 256), calc(100vh / 192));
  --motion-box: calc(74 * var(--motion-unit));
  --motion-dead-box: calc(10 * var(--motion-unit));

  /* Upstream sizes its whole HUD off that one scale rather than off breakpoints,
     and bzo used it only for the targeting box. The panels use it too now, so a
     screen that is small in either direction gets a HUD in proportion instead of
     one chosen by a width breakpoint it may fall between: a phone in landscape
     is 857x411, which no width rule reads as small.

     Clamped, because upstream is scaling geometry and this is scaling text. The
     unit is 5.63px on a 1920x1080 desktop and 2.14px on that phone, so a linear
     20px becomes 7.6px -- the right proportion and unreadable. The ceilings are
     what the desktop already had, so nothing there changes; the floors are
     legibility. Between them the size follows the screen continuously. */
  --hud-unit: var(--motion-unit);
  --hud-font-title: clamp(13px, calc(3.6 * var(--hud-unit)), 20px);
  --hud-font-body: clamp(11px, calc(2.5 * var(--hud-unit)), 14px);
  --hud-font-small: clamp(10px, calc(2.1 * var(--hud-unit)), 12px);
  --hud-gap: clamp(4px, calc(2 * var(--hud-unit)), 12px);
  --hud-pad: clamp(4px, calc(1.8 * var(--hud-unit)), 10px);
  /* Was 4vmin, which is 43px on a desktop and 16px on a phone. In the box's own
     unit it keeps its desktop width and stays legible on a small screen. */
  --altimeter-width: clamp(14px, calc(8 * var(--hud-unit)), 48px);

  /* The gap from a HUD panel to the edge of the screen. One value so the radar,
     the scoreboard, the debug HUD and the chat cannot drift apart.

     Deliberately not env(safe-area-inset-*), which every one of these used to
     read. On a phone in landscape the cutout inset costs an edge of a 411px-tall
     screen, and that is real estate bzo cannot spare: the camera hole covering a
     corner of the scoreboard is a better trade than the whole panel moving in to
     avoid it, and the covered part is still touchable. `viewport-fit=cover`
     stays, because drawing under the cutout is now the point. */
  --hud-edge: 10px;

  /* The tallest the chat folder is allowed to get, and so the strip along the
     bottom of the screen that no other panel may reach into. Chat is centred at
     95vw, which passes under the scoreboard's corner of the screen, so the
     roster has to stop above this rather than run to the bottom edge. Named
     because both of them subtract it and a second copy would drift.

     This is the fallback the roster reserves before anything has measured chat.
     What it normally subtracts is `--chat-height`, chat's real height, written
     by client.js -- chat is at its maximum only when it is full, and reserving
     the maximum the rest of the time leaves a band of dead screen between the
     two panels. */
  --chat-max-height: 33vh;

  /* The chat's palette. Named here rather than written at each rule because the
     XR chat panel paints the same chat onto a canvas and reads these back with
     getComputedStyle -- two lists of colours would drift the moment one of them
     changed, and a message that is yellow on the desktop and white in the
     headset is the same message telling you two different things. A kind with no
     colour of its own falls back to --chat-text, which is what `chat` does. */
  --chat-bg: rgba(0, 0, 0, 0.5);
  --chat-text: #fff;
  --chat-kind-server: #ffe082;
  --chat-kind-misc: #b3e5fc;
  --chat-kind-debug: #b0bec5;
  --chat-kind-action: #ffccbc;
  --chat-kind-direct-in: #80deea;
  --chat-kind-direct-out: #b39ddb;
  /* Team chat, for the part of the line that is neither the team nor the sender:
     the message itself. The `[Team]` label takes the team's own colour and the
     callsign takes the sender's own, both set per run in `formatNetworkMessage`,
     so this is only the fallback the rest of the line inherits. Upstream paints
     the whole line the team's ANSI colour, which it can because its team mates
     all share one; bzo shades them apart, so it has a second colour to spend and
     spends it on saying who is talking. */
  --chat-kind-team: #a5d6a7;
  /* The admin channel. Upstream marks it in yellow, which is the colour it also
     gives a ServerPlayer callsign (playing.cxx:1332). */
  --chat-kind-admin: #ffd54f;
  /* The timestamp in front of a line. Dimmer than any kind's colour, because
     it is the same nine characters on every line and the line is what is being
     read; upstream draws it in the line's own colour, which it can afford when
     it is off by default. */
  --chat-time: #8b9aa5;
  --chat-tab-idle: #ddd;
  --chat-tab-active: #fff;
  --chat-tab-unread: #ff8a80;
}

/* Scrollbars follow the theme rather than the platform's grey. `scrollbar-color`
   and `scrollbar-width` are the standard properties and are inherited, so
   setting them on the root reaches every panel that scrolls; the WebKit
   pseudo-elements below cover the Chrome and Safari versions that predate
   them. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--color-accent) var(--color-panel-bg);
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: var(--color-panel-bg);
  border-radius: 5px;
}

::-webkit-scrollbar-thumb {
  background: var(--color-btn-bg-hover);
  border: 1px solid var(--color-accent);
  border-radius: 5px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--color-accent);
}

::-webkit-scrollbar-corner {
  background: transparent;
}

#operatorOverlay, #operatorOverlay *, #audioOverlay, #audioOverlay *, #helpPanel, #helpPanel *, #viewOverlay, #viewOverlay * {
  pointer-events: auto !important;
}

canvas {
  display: block;
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
}

/* Chat window responsive wrapping and scrolling */
#chatTarget,
#chatInput {
  height: clamp(24px, calc(12 * var(--hud-unit)), 32px);
  font-size: 1em;
  border-radius: 8px;
  border: none;
  background: var(--color-panel-bg-solid);
  color: var(--color-fg);
  padding: 6px 8px;
  outline: none;
  z-index: 101;
  display: inline-block;
  box-sizing: border-box;
}
#chatTarget {
  margin-right: 6px;
  min-width: 80px;
  max-width: 160px;
}
#chatInput {
  width: 100%;
  margin-top: 2px;
  display: block;
  flex: 1 1 auto;
  min-width: 0;
}
/* The frame only: it positions the panel and carries the font the rows are
   sized in. The dark background belongs to #chatBody below, so the tab strip
   above it is transparent and the battlefield shows through between the tabs
   -- the tabs stand up off the folder rather than sitting on a band that runs
   the whole width of the screen. */
#chatWindow {
  position: fixed;
  left: 50%;
  bottom: var(--hud-edge);
  transform: translateX(-50%);
  width: 95vw;
  max-height: var(--chat-max-height);
  color: var(--chat-text);
  /* The line count is sized off this, so a fixed 16px asked for 120px of
     a 135px budget on a short screen and squeezed the tab row out entirely. */
  font: var(--hud-font-body) monospace;
  z-index: 100;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  box-sizing: border-box;
  align-items: stretch;
  min-height: 0;
}

/* The folder itself: everything below the tabs, and the only part with a
   background. */
#chatBody {
  background: var(--chat-bg);
  padding: var(--hud-pad) var(--hud-pad) calc(var(--hud-pad) * 0.75) var(--hud-pad);
  border-radius: 12px 12px 0 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 4px;
  box-sizing: border-box;
  overflow: hidden;
  align-items: stretch;
  min-height: 0;
}

/* The messages are the part that gives. A min-height of six lines meant the
   messages refused to shrink and pushed the tabs off a short screen instead.
   Six lines is a ceiling now, not a floor: on a tall screen it fills to six,
   on a short one it shows fewer and the tabs and the input row keep their
   place. Plain block flow rather than a flex column, so the whole scrollback
   scrolls the ordinary way -- a flex column packed to its own bottom edge is
   exactly the layout that made `overflow-y: auto` behave surprisingly once
   there was more content than fit, which stopped mattering the moment the
   window stopped rendering only a fixed slice of it. The themed scrollbar
   (`::-webkit-scrollbar` above) and wheel/touch scrolling both come from this
   `overflow-y`, the same as `#debugHud` and every dialog already using it. */
#chatMessages {
  /* A container, so the stamp rule below can ask how wide the transcript
     actually is. Viewport width is the wrong question: at the same 900px the
     panel is the width of the screen on its own, 600 of it with the virtual
     controls out and 500 with the debug panel open, and it is the panel's
     width that decides whether a clock in front of every line costs a wrap.
     Safe on this box because its width comes from the column it is in, never
     from the lines inside it -- they wrap to it. */
  container-type: inline-size;
  flex: 1 1 auto;
  min-height: 0;
  max-height: calc(6 * 1.25em + 0.4em);
  overflow-y: auto;
  overflow-x: hidden;
  padding-bottom: 0.2em;
}

#chatMessages .chat-line {
  line-height: 1.25;
  overflow-wrap: break-word;
  word-break: break-word;
  white-space: pre-wrap;
  max-width: 100%;
  box-sizing: border-box;
  user-select: text;
  -webkit-user-select: text;
  -webkit-touch-callout: default;
}

#chatMessages .chat-line.chat-kind-team {
  color: var(--chat-kind-team);
}

#chatMessages .chat-line.chat-kind-admin {
  color: var(--chat-kind-admin);
}

#chatMessages .chat-line.chat-kind-server {
  color: var(--chat-kind-server);
}

#chatMessages .chat-line.chat-kind-misc {
  color: var(--chat-kind-misc);
}

#chatMessages .chat-line.chat-kind-debug {
  color: var(--chat-kind-debug);
}

#chatMessages .chat-line.chat-kind-action {
  color: var(--chat-kind-action);
}

#chatMessages .chat-line.chat-kind-direct-in {
  color: var(--chat-kind-direct-in);
}

#chatMessages .chat-line.chat-kind-direct-out {
  color: var(--chat-kind-direct-out);
}

/* When a line arrived, in front of it. The window is monospace, so the stamps
   line up into a column on their own and the words start at the same place on
   every line. `pre` rather than the line's own `pre-wrap`, so a line long
   enough to wrap breaks in its words and not through the middle of a clock. */
#chatMessages .chat-line .chat-time {
  color: var(--chat-time);
  white-space: pre;
}

/* Nine characters of clock in front of every line is most of a phone's chat
   panel and a good part of a narrowed one, and what it costs is a wrap on
   lines that fitted -- so under this width the clock goes. The transcript is
   worth more than the second each line landed on, and `/savemsgs -t` still
   writes it.

   The date stays at every width. A line from another day is the one thing the
   stamp is there for -- chat restored from the cache, which otherwise only
   looks current -- and there are a handful of those at the top of a
   scrollback, not one per line, so they can have the room. */
@container (max-width: 640px) {
  .chat-line .chat-time:not(.chat-time-date) {
    display: none;
  }
}

/* /highlight (ControlPanel.cxx:546-556): upstream redraws a matched line
   pulsating, underlined and cyan instead of its usual colour. Placed after
   every chat-kind-* rule so it wins on source order at equal specificity. */
#chatMessages .chat-line.chat-highlight {
  color: #00e5ff;
  text-decoration: underline;
  animation: chat-highlight-pulse 1.2s ease-in-out infinite;
}

@keyframes chat-highlight-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

.chat-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  /* Neither of these gives up room; the messages above do. */
  flex: 0 0 auto;
  /* Against the folder below, so an active tab's missing bottom border meets
     the body's own edge and the two read as one shape. The side padding lines
     the strip up with the body's content rather than with its edge. */
  padding: 0 var(--hud-pad);
  margin-bottom: -1px;
}

/* Square at the bottom and rounded at the top, standing on the folder rather
   than floating above it. Each tab carries its own background, which is what
   leaves the strip between them transparent. */
.chat-tab {
  border: 1px solid #777;
  border-bottom: none;
  background: rgba(24, 24, 24, 0.9);
  color: var(--chat-tab-idle);
  border-radius: 6px 6px 0 0;
  font: var(--hud-font-small) monospace;
  padding: calc(var(--hud-pad) / 2) var(--hud-pad);
  cursor: pointer;
}

/* The tab whose page is showing is the front of the folder: it takes the
   body's own fill so the two read as one surface, where an idle tab stays
   darker and behind it. */
.chat-tab.active {
  border-color: var(--chat-tab-active);
  color: var(--chat-tab-active);
  background: var(--chat-bg);
}

.chat-tab.unread {
  border-color: #c62828;
  color: var(--chat-tab-unread);
}

/* The folder shuts and the tabs stay: an unread mark is the one thing chat
   still owes a player who is not reading it, and the tab carrying the mark is
   also what opens the folder again. With nothing behind them to stand on they
   close up into buttons rather than tabs -- all four corners rounded, and the
   bottom edge they lend to the body when it is open drawn back in. */
body.chat-collapsed #chatBody {
  display: none;
}

body.chat-collapsed .chat-tabs {
  margin-bottom: 0;
  padding-bottom: var(--hud-pad);
}

body.chat-collapsed .chat-tab {
  border-bottom: 1px solid #777;
  border-radius: 6px;
}

body.chat-collapsed .chat-tab.active {
  border-bottom-color: var(--chat-tab-active);
}

body.chat-collapsed .chat-tab.unread {
  border-bottom-color: #c62828;
}

#chatWindow .chat-input-row {
  display: flex;
  align-items: center;
  flex-grow: 0;
  flex-shrink: 0;
  min-height: clamp(30px, calc(15 * var(--hud-unit)), 40px);
  width: 100%;
  background: transparent;
}

/* The panel covers the bottom of the screen, which is exactly where mouse
   control puts the cursor to drive backwards and to fire while doing it, so
   the transcript stays out of the hit-test even though it is wheel-scrollable
   (see the wheel listener in client.js): pointer-events would take the click
   with it, and there is no CSS way to want one event type there and not the
   other. Only the tabs, the Send button and the destination dropdown take the
   pointer while chat is idle -- small, deliberate targets rather than the
   whole width of the panel -- so a click anywhere else, including on the
   transcript and the input, is a click on the battlefield until the input
   itself is activated. */
#chatWindow,
#chatWindow .chat-tabs,
#chatWindow .chat-input-row {
  pointer-events: none;
}

#chatWindow .chat-tab,
#sendBtn,
#chatTarget {
  pointer-events: auto;
}

body.chat-active #chatMessages,
body.chat-active #chatInput {
  pointer-events: auto;
}

/* Shaped like a chat tab, in the UI's green rather than the tab strip's grey,
   because it acts on chat rather than selecting one of its views. */
#sendBtn {
  flex: 0 0 auto;
  /* Send comes first in the row -- `[Send] [target] [input]` -- so the gap it
     owns is the one on its right. A left margin here would only inset the row
     from the edge of the panel. */
  margin-right: 6px;
  border: 1px solid var(--color-accent);
  border-radius: 5px;
  background: var(--color-btn-bg);
  color: var(--color-accent);
  font: 12px monospace;
  padding: 3px 7px;
  cursor: pointer;
  box-sizing: border-box;
}

#sendBtn.active {
  border-color: var(--color-accent-light);
  background: var(--color-btn-bg-active);
  color: var(--color-fg);
}

#chatTarget {
  height: 32px;
  font-size: 1em;
  margin-right: 6px;
  border-radius: 8px;
  border: none;
  background: rgba(0,0,0,0.8);
  color: #fff;
  padding: 6px 8px;
  outline: none;
  z-index: 101;
  display: inline-block;
  box-sizing: border-box;
  min-width: 80px;
  max-width: 160px;
}
/* Responsive chat area for small screens */
@media (orientation: landscape) and (min-width: 801px) {
  body.virtual-controls-active #chatWindow {
    left: calc(2vw + 120px + 10px);
    right: calc(2vw + 120px + 10px);
    width: auto;
    transform: none;
  }
}

/* The debug panel sits in the bottom right corner, so chat gives up that much
   of its right edge while the panel is open. Its left edge does not move: only
   the side the panel is on has anything to avoid. 2.5vw is where the centered
   95vw panel already starts, so the edge stays exactly where it was. */
@media (orientation: landscape) and (min-width: 901px) {
  body.chat-avoid-debug #chatWindow {
    left: 2.5vw;
    right: calc(10px + 340px);
    width: auto;
    max-width: none;
    transform: none;
  }

  body.virtual-controls-active.chat-avoid-debug #chatWindow {
    left: calc(2vw + 120px + 10px);
  }
}

@media (max-width: 800px) {
  #chatWindow {
    width: 98vw;
    max-width: 98vw;
    min-width: 0;
    max-height: var(--chat-max-height);
    font-size: 12px;
    left: 50%;
    right: auto !important;
    top: auto !important;
    bottom: 0 !important;
    transform: translateX(-50%) !important;
    margin: 0 !important;
    position: fixed !important;
    box-sizing: border-box;
    z-index: 100;
  }
  #chatBody {
    padding: 4px 4px 2px 4px;
    border-radius: 10px 10px 0 0;
  }
  #chatWindow input,
  #chatWindow textarea {
    font-size: 12px;
    padding: 2px 4px;
  }
  #chatWindow div {
    font-size: 12px;
    margin-bottom: 2px;
    max-width: 100%;
  }

  #chatMessages {
    min-height: calc(6 * 1.2em + 0.35em);
    max-height: calc(6 * 1.2em + 0.35em);
  }
}

/* A phone held in landscape is around 430px tall, and the HUD's font is at its
   clamp floor there -- so six lines of transcript plus the tabs and the input
   row stand a third of the way up the screen, into the targeting box, which is
   centred. Three lines keep the box clear; the scrollback is all still there
   behind the scrollbar, and now under a finger as well. Stated as a height
   rather than a device width because it is height the panel is spending, and a
   short desktop window is spending it the same way. */
@media (max-height: 500px) {
  #chatMessages {
    min-height: 0;
    max-height: calc(3 * 1.2em + 0.35em);
  }
}
/* Mobile Controls Overlay Styles */
#controlsOverlay {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
  z-index: 9999;
  display: none;

  /* The button stack has to fit a phone held in landscape, where the visible
     viewport is only a few hundred pixels tall. A fixed 80px button on a 90px
     pitch needs 368px for four, which pushed the top of the ladder off a Pixel
     and left the one below it stranded near the top corner. Sizes scale with
     the viewport instead, and every button's offset is derived from one pitch,
     so they keep an equal margin whatever they scale to. On a desktop the
     minimums win and the layout is unchanged. */
  --mobile-btn-size: min(80px, 15vh);
  --mobile-btn-gap: min(10px, 2vh);
  /* svh, not vh: on mobile Chrome `vh` is the *large* viewport, the one with the
     URL bar retracted, so a stack sized in vh is taller than what is actually on
     screen. svh is the viewport with the browser UI showing, which is the height
     that has to fit. Declared second so a browser without svh keeps the vh pair
     above rather than dropping the size entirely. */
  --mobile-btn-size: min(80px, 15svh);
  --mobile-btn-gap: min(10px, 2svh);
  --mobile-btn-pitch: calc(var(--mobile-btn-size) + var(--mobile-btn-gap));
  --mobile-btn-base: 2vw;
}

.mobile-joystick {
  position: absolute;
  left: 2vw;
  bottom: 5vh;
  width: 120px;
  height: 120px;
  background: rgba(60,60,60,0.18);
  border-radius: 50%;
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
}

@media (orientation: landscape) {
  .mobile-joystick {
    left: 2vw;
  }
}

.joystick-knob {
  width: 60px;
  height: 60px;
  background: rgba(200,200,200,0.7);
  border-radius: 50%;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  pointer-events: auto;
  transition: background 0.1s;
}

.mobile-btn {
  position: absolute;
  right: 2vw;
  width: var(--mobile-btn-size);
  height: var(--mobile-btn-size);
  background: rgba(76,175,80,0.85);
  color: #fff;
  font-size: calc(var(--mobile-btn-size) * 0.45);
  border: none;
  border-radius: 50%;
  pointer-events: auto;
  touch-action: manipulation;
  box-shadow: 0 2px 8px rgba(0,0,0,0.18);
  display: flex;
  align-items: center;
  justify-content: center;
  user-select: none;
}

.mobile-btn.pressed {
  filter: brightness(1.2) drop-shadow(0 0 12px #fff8);
  transform: scale(0.93);
  opacity: 0.85;
  transition: filter 0.1s, transform 0.1s, opacity 0.1s;
}

/* Repeated addition rather than `pitch * 2`. Chrome on Android drops a `bottom`
   built from a number multiplied by a parenthesised length -- the rule is simply
   discarded, and an absolutely positioned button with no offset falls to its
   static position at the top of the overlay, which is the bug this ladder had.
   Addition is the one form nothing argues with. */
#identifyBtn {
  bottom: calc(var(--mobile-btn-base) + var(--mobile-btn-pitch) + var(--mobile-btn-pitch) + var(--mobile-btn-pitch));
}
#dropBtn {
  bottom: calc(var(--mobile-btn-base) + var(--mobile-btn-pitch) + var(--mobile-btn-pitch));
}
#jumpBtn { bottom: calc(var(--mobile-btn-base) + var(--mobile-btn-pitch)); }
#fireBtn { bottom: var(--mobile-btn-base); background: rgba(244,67,54,0.85); }

@media (min-width: 900px) {
  #controlsOverlay { display: none; }
}

.hudButtons button {
  background: var(--color-btn-bg);
  color: var(--color-accent);
  border: 2px solid var(--color-accent);
  border-radius: 5px;
  font-weight: bold;
  padding: 6px 6px;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
  outline: none;
  cursor: pointer;
}
.hudButtons button.active {
  background: var(--color-btn-bg-active);
  color: var(--color-fg);
  border-color: var(--color-accent-light);
  box-shadow: 0 0 6px var(--color-btn-bg-shadow);
}
.hudButtons button:focus {
  border-color: var(--color-btn-bg-focus);
}
.hudButtons button:hover {
  background: var(--color-btn-bg-hover);
  color: var(--color-fg);
}

body {
  font-family: 'Arial', sans-serif;
  overflow: hidden;
  background: var(--color-bg);
  color: var(--color-fg);
}

#mainhud {
  position: fixed;
  top: var(--hud-edge);
  left: var(--hud-edge);
  padding: 1vw;
  border-radius: 10px;
  box-sizing: border-box;
  background: var(--color-panel-bg);
  /* The roster is as long as the server is busy, and on a full server that is
     longer than the screen. Bounding the panel to the viewport and letting the
     list inside it scroll keeps the name row, the gear and the team scores --
     everything a player operates -- on screen at any player count, instead of
     the panel growing off the bottom edge and taking them with it. The column
     is what gives the list a height to be `flex: 1` of. */
  display: flex;
  flex-direction: column;
  max-height: calc(100vh
    - var(--chat-height, var(--chat-max-height))
    - (2 * var(--hud-edge))
    - var(--hud-edge-gap));
}

@media (max-width: 900px), (max-width: 600px) {
  #mainhud {
    width: 35vw;
    height: auto;
    padding: 5px;
    font-size: 5px;
  }
}

#info h1 {
  font-size: 24px;
  margin-bottom: 15px;
  color: #4CAF50;
}

#stats {
  margin-bottom: var(--hud-gap);
  font-size: var(--hud-font-body);
  line-height: 1.6;
  display: flex;
  align-items: center;
  gap: var(--hud-gap);
  justify-content: space-between;
}

/* The title gives before the controls do: a long callsign wraps or ellipses
   rather than pushing the chevron off the end of a narrow panel. */
#stats .playerLabel {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The whole label opens Settings, so the whole label says so: the tooltip and
   the pointer belong to it rather than to the name alone. The flag takes the
   name's size, because upstream draws the pair as one run of text and a smaller
   flag beside a larger name reads as two things. */
#playerLabel {
  cursor: pointer;
}

#playerName,
#playerFlag {
  color: #4CAF50;
  font-weight: bold;
  font-size: var(--hud-font-title);
}

.settingsIcon {
  font-size: 1.5em;
}

.xrTextField {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

#xrQuickBtn {
  display: none;
}

#xrQuickBtn.xrAvailable {
  display: inline-block;
}

#xrQuickBtn .settingsIcon {
  font-size: 1.3em;
}

.hudButtons {
  display: flex !important;
  gap: 6px;
  justify-content: center;
  flex-wrap: wrap;
}

/* Green transmitting, red not, on the button itself: what the panel that used
   to sit beside the targeting box said, in the corner where the rest of the
   HUD's controls already are. The state is the border rather than a word, so
   it reads at a glance and costs no width on a phone -- and the hover text
   names the channel the microphone would be speaking on. */
.hudButtons button.micBtn--on {
  border-color: var(--color-accent-light);
  color: var(--color-accent-light);
  box-shadow: 0 0 0.45rem rgba(102, 187, 106, 0.55);
}

.hudButtons button.micBtn--off {
  border-color: #f44336;
  color: #f44336;
  box-shadow: 0 0 0.45rem rgba(244, 67, 54, 0.4);
}
#stats span {
  font-weight: bold;
  color: #4CAF50;
}

#scoreboard {
  border-top: 1px solid #444;
  padding-top: 1vw;
  /* Passes the panel's bounded height down to the list: a flex item's default
     `min-height: auto` would let the list win the argument and overflow the
     panel anyway. */
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Collapsed by the chevron in the button row above (issue #145). What stays is
   the row itself -- the name, the gear and the chevron -- so Settings is still
   one tap away and the control that put the roster away is where it was. The
   width clamp has to go with it, or the strip would keep the 35vw a phone
   reserves for a roster that is no longer there. */
body.scoreboard-collapsed #scoreboard {
  display: none;
}

body.scoreboard-collapsed #stats {
  margin-bottom: 0;
}

body.scoreboard-collapsed #mainhud {
  width: auto;
}

#matchClock {
  text-align: center;
  font-size: var(--hud-font-body);
  font-weight: bold;
  color: #4CAF50;
  margin-bottom: 5px;
}

#matchClock.matchClockHidden {
  display: none;
}

#scoreboard h3 {
  font-size: 16px;
  margin-bottom: 10px;
  color: #4CAF50;
}

/* The header and every row are the same flex track list, so a label sits over
   the cells it names. Widths come from SCOREBOARD_COLUMNS in `ch`, written
   inline by makeScoreboardCell; the padding here has to match a row's so the
   two line up. */
#scoreboardHeader {
  display: flex;
  /* The rows' font, not the smaller one the other headers use: the column
     widths are in `ch`, which is font-relative, so a header set one size down
     would reserve narrower tracks than the rows it labels and every number
     would sit left of its own heading. The muted colour is what separates it
     from the rows instead. */
  font-size: var(--hud-font-body);
  color: #888;
  margin-bottom: 5px;
  padding: 0 var(--hud-pad) 5px;
  border-bottom: 1px solid #333;
}

/* The one part of the panel that scrolls. The rows take pointer events already
   -- clicking one watches that player -- so the wheel and a touch drag scroll
   it natively here, with none of the coordinate hit-testing the chat transcript
   needs to work around its `pointer-events: none`. */
#scoreboardList {
  font-size: var(--hud-font-body);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* The one place on the HUD where a vertical drag is a scroll rather than the
     game's. `body` is `touch-action: none` so that a drag anywhere else aims. */
  touch-action: pan-y;
}

#teamScoreboard {
  font-size: var(--hud-font-body);
  margin-bottom: var(--hud-gap);
}

.teamScoreboardEmpty {
  display: none;
}

.teamScoreHeader {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: #888;
  margin-bottom: 5px;
  padding-bottom: 5px;
  border-bottom: 1px solid #333;
}

.teamScoreEntry {
  margin: 3px 0;
  padding: 3px 8px;
}

.scoreboardEntry {
  display: flex;
  margin: calc(var(--hud-gap) / 2) 0;
  padding: calc(var(--hud-pad) / 2) var(--hud-pad);
  border-radius: 3px;
}

/* A fixed-width column is the width its entry in SCOREBOARD_COLUMNS asks for;
   the name column has none and takes what is left. `min-width: 0` is what lets
   the name actually shrink to its ellipsis inside a flex row. */
.scoreboardCell {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

/* The panel is as wide as its content, and Firefox counts a cell with
   `min-width: 0` and hidden overflow as no width at all when it sizes it, so
   the names went with nothing to show. A floor keeps them; a longer name still
   shrinks to its ellipsis. */
.scoreboardCell-player {
  flex: 1 1 auto;
  min-width: 8ch;
}

/* Every cell but the first stands off the one before it, so a right-aligned
   number never runs into the column on its left -- which is what the header's
   labels do first, being wider than the values under them. */
.scoreboardCell + .scoreboardCell {
  padding-left: 6px;
}

.scoreboardEntry.current {
  color: #4CAF50;
  font-weight: bold;
  background: rgba(76, 175, 80, 0.25);
}

.scoreboardName {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The team board is still two items, a name and a score, rather than the
   player board's column track list -- so its name takes the free space and
   pushes the score out to the right edge, under the heading that names it.
   A player row does not: there the name sits in a column of its own and the
   numbers have columns of theirs. */
.teamScoreHeader .scoreboardName,
.teamScoreEntry .scoreboardName {
  flex: 1;
}

/* Name and flag are one item wherever they appear, so a row's spacing pushes
   the score away from the pair instead of the flag away from the name. Upstream
   writes them as one string with no space at all -- callsign, "/", abbreviation
   -- and only the colour changes at the slash. */
.playerLabel {
  display: inline-flex;
  align-items: baseline;
  min-width: 0;
}

.scoreboardFlag {
  white-space: nowrap;
  font-weight: bold;
}

/* The authentication indicator sits tight against the name it belongs to, as
   upstream's does -- there is no space between them, only a colour change. */
.scoreboardStatus {
  white-space: nowrap;
  font-weight: bold;
}

/* A tight suffix like the flag and status glyph, not a word off to the side
   like the rabbit mark. */
.scoreboardPaused,
.scoreboardMic {
  white-space: nowrap;
}

/* Dimmed, because a tagline is something the player wrote rather than
   something the game decided, and it sits between the name and the marks that
   describe the row. It may be long, so it is the part that gives way first. */
.scoreboardMotto {
  opacity: 0.65;
  font-style: italic;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* ScoreboardRenderer.cxx:562 drops a blank line in front of the first observer.
   Sorting them last says where they are; the gap says they are a separate group
   rather than the worst players on the board. */
.scoreboardEntry.startsObservers {
  margin-top: 0.55em;
}

/* Over a replay's live viewers, who are not in the recording being played. */
.scoreboardGroupLabel {
  margin-top: 0.55em;
  font-size: 0.85em;
  opacity: 0.7;
}

/* The rabbit mark stands off the name it follows, unlike the flag and the
   authentication indicator, because it is a word rather than a suffix. */
.scoreboardRabbit:not(:empty) {
  white-space: nowrap;
  margin-left: 6px;
}

/* The hunted bullseye and the cursor's open ring, hard against the stats column the way
   upstream draws them (ScoreboardRenderer.cxx:845). They stand off the name for
   the rabbit mark's reason: they are something said about the row, not a suffix
   on the callsign. */
/* The two hunt rings, and only them. `unicode-range` limits this face to the
   two codepoints, so naming it first on an element pins the rings without
   touching the callsign beside them -- which is why the Settings row can use it
   too, where swapping the whole value to another family would restyle the name.

   They have to come from one face or they stop reading as one ring in two
   states: Arial's usual Linux substitute, Liberation Sans, carries neither, so
   both fall through together there, but a platform whose Arial has the plain
   ring and not the bullseye would draw the pair at two different sizes -- which
   is exactly what the dotted U+25CC did before this pair replaced it. Every
   face named here has both, and if none of them is installed the browser falls
   back to what it would have done anyway. */
@font-face {
  font-family: 'HuntRings';
  src: local('DejaVu Sans'), local('Segoe UI Symbol'), local('Apple Symbols'), local('Arial Unicode MS');
  unicode-range: U+25CB, U+25CE;
}

.scoreboardHunt:not(:empty) {
  white-space: nowrap;
  margin-left: 6px;
  font-weight: bold;
  font-family: 'HuntRings', 'Arial', sans-serif;
}

/* Upstream's PulsatingColor, which is what separates the row the cursor is on
   -- about to be marked -- from the rows already marked. Both the cursor arrow
   and the `*SEL*` at the head of the board wear it. */
.scoreboardHunt.huntCursor,
.scoreboardHuntSel {
  animation: huntPulse 1s ease-in-out infinite;
}

.scoreboardHuntSel {
  white-space: nowrap;
  font-weight: bold;
}

@keyframes huntPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

/* A player who would rather not have a pulse on screen keeps the mark and
   loses the blink; the arrow still says which row the cursor is on. */
@media (prefers-reduced-motion: reduce) {
  .scoreboardHunt.huntCursor,
  .scoreboardHuntSel {
    animation: none;
  }
}

.scoreboardStats {
  margin-left: 10px;
  white-space: nowrap;
}

#helpHint {
  margin-top: 15px;
  padding-top: 15px;
  border-top: 1px solid #444;
  font-size: 14px;
  color: #4CAF50;
}

#helpHint strong {
  color: #fff;
  font-size: 16px;
}

#helpPanel {
  z-index: 200;
  display: none;
}

#helpPanel .dialogTitle {
  margin: 0 0 20px 0;
  color: #4CAF50;
  font-size: 2rem;
  font-weight: bold;
}

#helpPanel .dialogContent {
  display: flex;
  flex-direction: column;
}

#helpPanel h1 {
  font-size: 2rem;
  margin-bottom: 20px;
  color: #4CAF50;
  text-align: center;
}

#helpPanel h2 {
  font-size: 1.3em;
  margin-top: 20px;
  margin-bottom: 10px;
  color: #4CAF50;
  border-bottom: 1px solid #444;
  padding-bottom: 5px;
}

#helpPanel h2:first-child {
  margin-top: 0;
}

#helpPanel p {
  margin: 8px 0;
  font-size: 1em;
  line-height: 1.6;
}

#helpPanel strong {
  color: #fff;
}

.closeBtn {
  border: 2px solid #4CAF50;
  border-radius: 5px;
  background: rgba(76, 175, 80, 0.2);
  color: #4CAF50;
  font-size: 1.15em;
  font-weight: bold;
  line-height: 1;
  cursor: pointer;
  padding: 2px 10px;
  transition: all 0.2s;
}

.closeBtn:hover {
  background: rgba(76, 175, 80, 0.4);
  border-color: #66bb6a;
}

.closeBtn:active {
  background: rgba(76, 175, 80, 0.6);
}

/* Operator Panel Styling */
#operatorOverlay {
  z-index: 300;
  display: none;
}

#viewOverlay {
  z-index: 300;
  display: none;
}

/* Each table scrolls inside the dialog rather than the dialog scrolling as one
   piece. With the server list running to hundreds of rows, a single scroll put
   "Local maps" below all of them -- the shorter list became the harder one to
   reach. Both sections stay on screen, and each gives up room to the other
   through flex rather than by a fixed split. */
#viewOverlay .dialogContent {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
}

#viewOverlay .viewTableScroll {
  overflow-y: auto;
  /* A zero basis, not `auto`. On content size the server list -- hundreds of
     rows against the maps' handful -- takes nearly all the height and leaves
     the other table a couple of lines, which is the problem the scroll areas
     were added to solve. Sharing the free space evenly gives each list a
     usable window whatever is in it, and each scrolls its own overflow. */
  flex: 1 1 0;
  min-height: 6em;
}

/* The row is the control, so it says so on hover and on focus. `/list` uses
   the same class for the same reason (`tbody tr.clickable`, server.js). */
#viewOverlay .viewTable tbody tr.clickable {
  cursor: pointer;
}

#viewOverlay .viewTable tbody tr.clickable:hover,
#viewOverlay .viewTable tbody tr.clickable:focus-visible {
  background: rgba(76, 175, 80, 0.18);
  outline: none;
}

#viewOverlay .viewTable thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: rgba(0, 0, 0, 0.92);
}

#audioOverlay {
  z-index: 310;
  display: none;
}

#audioOverlay .dialogContent {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

#motd {
  margin-bottom: 10px;
  font-size: 1.1em;
  color: #4CAF50;
}

.operatorRow {
  margin-bottom: 10px;
}

.operatorConfigRow {
  display: grid;
  grid-template-columns: 1fr;
  gap: 6px;
}

.operatorToggle {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

#listServerKeyStatus {
  color: #999;
  font-size: 0.9em;
  margin-left: 6px;
}

#operatorOverlay input[type="number"],
#viewOverlay input[type="number"] {
  width: 100%;
  padding: 10px;
  font-size: 1em;
  border: 2px solid #4CAF50;
  border-radius: 5px;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  margin-bottom: 10px;
  box-sizing: border-box;
}

#operatorOverlay .dialogTitle,
#viewOverlay .dialogTitle {
  margin: 0 0 20px 0;
  color: #4CAF50;
  font-size: 2rem;
}

#operatorOverlay label,
#viewOverlay label {
  color: #fff;
  font-weight: bold;
}

#viewOverlay h3 {
  color: #4CAF50;
  margin: 18px 0 8px;
}

#operatorOverlay input[type="text"],
#viewOverlay input[type="text"] {
  width: 100%;
  padding: 10px;
  font-size: 1em;
  border: 2px solid #4CAF50;
  border-radius: 5px;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  margin-bottom: 10px;
  box-sizing: border-box;
}

#operatorOverlay input[type="text"]:focus,
#viewOverlay input[type="text"]:focus {
  outline: none;
  border-color: #66bb6a;
}

#operatorOverlay button,
#viewOverlay button {
  padding: 10px 18px;
  font-size: 1em;
  font-weight: bold;
  border: 2px solid #4CAF50;
  border-radius: 5px;
  background: rgba(76, 175, 80, 0.2);
  color: #4CAF50;
  cursor: pointer;
  transition: all 0.2s;
  margin-right: 6px;
}

#operatorOverlay button:hover,
#viewOverlay button:hover {
  background: rgba(76, 175, 80, 0.4);
  border-color: #66bb6a;
}

#operatorOverlay button:active,
#viewOverlay button:active {
  background: rgba(76, 175, 80, 0.6);
}

.viewTable {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 16px;
  color: #fff;
}

.viewTable th, .viewTable td {
  padding: 4px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  text-align: left;
  font-size: 0.9em;
}

.viewTable th {
  cursor: pointer;
  user-select: none;
  color: #4CAF50;
  white-space: nowrap;
}

.viewTable th:hover {
  color: #66bb6a;
}

.viewTable tbody tr:nth-child(even) {
  background: rgba(255, 255, 255, 0.05);
}

.viewTable tbody tr:hover {
  background: rgba(76, 175, 80, 0.15);
}

.viewTable a {
  color: #66bb6a;
}

#settingsHud button:focus-visible,
#settingsHud input:focus-visible,
#settingsHud select:focus-visible,
#audioOverlay button:focus-visible,
#audioOverlay input:focus-visible,
#audioOverlay select:focus-visible,
#helpPanel button:focus-visible,
#helpPanel input:focus-visible,
#helpPanel select:focus-visible,
#operatorOverlay button:focus-visible,
#operatorOverlay input:focus-visible,
#operatorOverlay select:focus-visible,
#viewOverlay button:focus-visible,
#viewOverlay input:focus-visible,
#viewOverlay select:focus-visible,
#entryDialog button:focus-visible,
#entryDialog input:focus-visible,
#entryDialog select:focus-visible {
  outline: 2px solid var(--color-btn-bg-focus);
  outline-offset: 2px;
}

#entryDialog {
  z-index: 300;
  display: none;
}

#loadingOverlay {
  position: fixed;
  inset: 0;
  z-index: 290;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.62);
  backdrop-filter: blur(4px);
}

.loadingPanel {
  width: min(420px, calc(100vw - 32px));
  padding: 22px 24px;
  border: 2px solid #4CAF50;
  border-radius: 14px;
  background: rgba(0, 0, 0, 0.9);
  box-shadow: 0 0 24px rgba(0, 0, 0, 0.35);
}

.loadingTitle {
  color: #4CAF50;
  font-size: 24px;
  font-weight: bold;
  text-align: center;
  margin-bottom: 12px;
}

#loadingStatus {
  color: #f5f8f0;
  font-size: 15px;
  text-align: center;
}

#loadingDetail {
  color: rgba(255, 255, 255, 0.72);
  font-size: 12px;
  text-align: center;
  min-height: 18px;
  margin-top: 6px;
}

.loadingBarTrack {
  width: 100%;
  height: 14px;
  margin-top: 16px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(76, 175, 80, 0.45);
}

#loadingBarFill {
  width: 0%;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, #2d7d31 0%, #4CAF50 50%, #83d588 100%);
  transition: width 0.18s ease-out;
}

#serverInfo {
  margin-bottom: 10px;
  font-size: 1.1em;
  color: #333;
}

#serverMotd {
  color: #4CAF50;
}

#entryDialog h2 {
  margin: 0 0 20px 0;
  color: #4CAF50;
  text-align: center;
}

/* What the game is, and where to read more: for a newcomer, and the first
   words a search engine or an AI finds on the page. */
#entryAbout {
  margin: -12px 0 14px 0;
  font-size: 0.9em;
  text-align: center;
  color: #888;
}

#entryAbout a {
  color: #4CAF50;
}

/* The dialog's two text fields, name and motto. One rule rather than two
   copies: they are the same control asked twice, and a second copy is how
   they drift apart. */
#entryInput,
#entryMottoInput {
  width: 100%;
  padding: 10px;
  font-size: 1em;
  border: 2px solid #4CAF50;
  border-radius: 5px;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  margin-bottom: 20px;
  box-sizing: border-box;
}

/* Tighter to the name above it: the two belong together, where the selector
   rows below are separate choices. */
#entryMottoInput {
  margin-top: -10px;
}

#entryInput:focus,
#entryMottoInput:focus {
  outline: none;
  border-color: #66bb6a;
}

#entryInput:disabled,
#entryMottoInput:disabled {
  cursor: not-allowed;
  opacity: 0.65;
}

/* Dimmer than typed text, so an empty field reads as optional rather than
   as something not filled in yet. */
#entryMottoInput::placeholder {
  color: rgba(255, 255, 255, 0.45);
}

.entryTeamSelector {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(10rem, 1fr);
  align-items: center;
  gap: 24px;
  width: min(100%, 560px);
  min-height: 58px;
  margin: 0 auto 20px;
  padding: 7px 10px;
  border: 1px solid transparent;
  border-radius: 3px;
  background: transparent;
  color: var(--color-fg);
  font: inherit;
  cursor: pointer;
}

.entryTeamLabel {
  font-weight: bold;
  text-align: right;
}

.entryTeamValue {
  color: var(--color-accent-light);
  font-weight: bold;
  text-align: left;
}

.entryTeamSelector[data-team="rogue"] .entryTeamValue { color: #ffff00; }
.entryTeamSelector[data-team="observer"] .entryTeamValue { color: #ffffff; }
.entryTeamSelector[data-team="red"] .entryTeamValue { color: #ff3333; }
.entryTeamSelector[data-team="blue"] .entryTeamValue { color: #6680ff; }
.entryTeamSelector[data-team="green"] .entryTeamValue { color: #33dd55; }
.entryTeamSelector[data-team="purple"] .entryTeamValue { color: #ff55ff; }

/* Each half of a choice row is a click that steps the list that way -- the
   title steps back, the current option steps forward -- so each half carries
   the chevron for its own direction. A row with no list to walk takes neither,
   which is why this is keyed to the row kind: the global login row wears the
   same class and is an action. */
.entryTeamSelector[data-menu-kind="choice"] .entryTeamLabel::before,
.settingsMenuRow[data-menu-kind="choice"] .settingsMenuLabel::before {
  content: "‹ ";
}

.entryTeamSelector[data-menu-kind="choice"] .entryTeamValue::after,
.settingsMenuRow[data-menu-kind="choice"] .settingsMenuValue::after {
  content: " ›";
}

/* A `pick` row wears both chevrons around its *value*, where a choice row
   spreads them to the row's two edges. That difference is the point rather than
   decoration: on a choice row sideways and select do the same thing, and on
   this one they do not, so it must not look like one. The chevrons flank the
   player the row is pointed at, which is what they step. */
.settingsMenuRow[data-menu-kind="pick"] .settingsMenuValue::before {
  content: "‹ ";
}

.settingsMenuRow[data-menu-kind="pick"] .settingsMenuValue::after {
  content: " ›";
}

/* The row's mark is the scoreboard's mark, so it is pinned to the same face --
   and only the two ring codepoints are, so the callsign keeps the menu's own
   font. Without it, toggling could swap the ring for a bullseye of a different
   size on a platform whose body font has one and not the other. */
.settingsMenuRow[data-menu-kind="pick"] .settingsMenuValue {
  font-family: 'HuntRings', 'Arial', sans-serif;
}

/* The hit zones getMenuClickZone measures: a chevron's width at each end steps,
   everything between them marks. Only the cursor says so -- drawing a boundary
   would put two vertical rules through a row that is one control. */
.settingsMenuRow[data-menu-kind="pick"]:not(:disabled) {
  cursor: pointer;
}

/* Nobody to point at. The row stays in place and stops answering, rather than
   leaving and coming back as players join. */
.settingsMenuRow[data-menu-kind="pick"]:disabled .settingsMenuValue::before,
.settingsMenuRow[data-menu-kind="pick"]:disabled .settingsMenuValue::after {
  content: "";
}

.entryTeamSelector:hover,
.entryTeamSelector:focus-visible {
  border-color: var(--color-accent);
}

.entryTeamSelector:disabled {
  cursor: not-allowed;
  opacity: 0.65;
}

#tankSelector {
  margin: 0 0 16px 0;
}

#tankSelectorTitle {
  color: #4CAF50;
  font-weight: bold;
  margin: 0 0 8px 0;
  text-align: center;
}

#tankSelectorCarousel {
  display: flex;
  gap: 10px;
  justify-content: center;
  align-items: center;
}

.tankOption {
  /* Fills whatever the dialog leaves between the two arrows, rather than a
     fixed 140px card: the preview is the only thing in this row worth space,
     and the dialog is laid out for phones through to headsets. */
  flex: 1 1 auto;
  min-width: 0;
  border: 2px solid #4CAF50;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.45);
  color: #4CAF50;
  padding: 8px;
  cursor: pointer;
  transition: all 0.2s;
}

.tankOption.selected {
  border-color: #66bb6a;
  background: rgba(76, 175, 80, 0.28);
  box-shadow: 0 0 14px rgba(76, 175, 80, 0.4);
}

/* The confirm sits apart from the rows it commits, so it does not read as one
   more setting. */
.operatorConfirmRow {
  margin-top: 0.9em;
  padding-top: 0.7em;
  border-top: 1px solid rgba(76, 175, 80, 0.35);
  gap: 0.6em;
}

/* Reads Restart rather than Apply when a staged change starts a new game, and
   says so in the one place an operator is looking. */
#operatorApplyBtn.startsNewGame {
  border-color: #ffb347;
  color: #ffb347;
}

.tankArrowBtn {
  width: 46px;
  height: 46px;
  border: 2px solid #4CAF50;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  color: #4CAF50;
  font-size: 1em;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.2s;
}

.tankArrowBtn:hover {
  border-color: #66bb6a;
  background: rgba(76, 175, 80, 0.2);
}

.tankArrowBtn:disabled {
  opacity: 0.5;
  cursor: default;
}

.tankPreviewCanvas {
  /* The drawing buffer follows this, not the other way round -- see
     `resizeTankPreview`. `aspect-ratio` keeps the frame the shape the preview
     camera was framed for, and the cap stops it eating a tall screen. */
  width: 100%;
  aspect-ratio: 3 / 2;
  max-height: 34vh;
  display: block;
  margin: 0 auto 6px auto;
  border-radius: 6px;
  background: radial-gradient(circle at 50% 65%, rgba(76, 175, 80, 0.2), rgba(0, 0, 0, 0.5));
}

.tankOptionLabel {
  display: block;
  text-align: center;
  font-weight: bold;
}

.entryDialogButtons {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.entryDialogButtons button {
  padding: 10px 20px;
  font-size: 1em;
  font-weight: bold;
  border: 2px solid #4CAF50;
  border-radius: 5px;
  background: rgba(76, 175, 80, 0.2);
  color: #4CAF50;
  cursor: pointer;
  transition: all 0.2s;
}

.entryDialogButtons button:hover {
  background: rgba(76, 175, 80, 0.4);
  border-color: #66bb6a;
}

.entryDialogButtons button:active {
  background: rgba(76, 175, 80, 0.6);
}

#messages {
  margin-top: 15px;
  font-size: 14px;
  max-height: 100px;
  overflow-y: auto;
  border-top: 1px solid #444;
  padding-top: 10px;
}

.message {
  margin: 5px 0;
  padding: 5px;
  background: rgba(76, 175, 80, 0.2);
  border-radius: 3px;
  animation: fadeIn 0.3s;
}

.message.kill {
  background: rgba(255, 87, 34, 0.3);
  color: #ff5722;
}

#debugHud {
  position: fixed;
  bottom: var(--hud-edge);
  right: var(--hud-edge);
  z-index: 100;
  background: var(--color-panel-bg);
  padding: var(--hud-pad);
  border-radius: 10px;
  /* A phone override set this to 90vw, which on an 857px landscape screen is
     771px and stretched the panel across from its right anchor. As a ceiling on
     the one width it wants, it does that override's job on a narrow phone and
     nothing anywhere else. */
  width: min(320px, 90vw);
  font-family: 'Courier New', monospace;
  font-size: var(--hud-font-small);
  box-sizing: border-box;
  overflow-x: auto;
  /* Both are pinned to the right edge and the radar is on top, so the HUD stops
     below it instead of scrolling away underneath. It scrolls already, so the
     rows this costs are still reachable. */
  max-height: max(120px, calc(100vh
    - var(--hud-edge)
    - var(--radar-size)
    - var(--hud-edge-gap)
    - var(--hud-edge)));
  overflow-y: auto;
  display: none;
}

#debugHud h3 {
  font-size: 14px;
  margin-bottom: 10px;
  color: #FFD700;
  border-bottom: 1px solid #444;
  padding-bottom: 5px;
}

#debugContent {
  line-height: 1.6;
}

#debugContent div {
  margin: 3px 0;
  display: flex;
  justify-content: space-between;
}

#debugContent .label {
  color: #AAA;
}

#debugContent .value {
  color: #0F0;
  font-weight: bold;
}

.message.death {
  background: rgba(244, 67, 54, 0.3);
  color: #f44336;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.player-label {
  color: #fff;
  font-family: Arial, sans-serif;
  font-size: 14px;
  font-weight: bold;
  padding: 4px 8px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 4px;
  white-space: nowrap;
  text-align: center;
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
  pointer-events: none;
  user-select: none;
}

/* An observer aims at nothing and reloads nothing, so the mouse motion box,
   the shot status and the altimeter all go. HUDRenderer::renderRoaming draws
   none of them either. */
body.observing #controlBox,
body.observing #shotStatus,
body.observing #altimeter {
  display: none;
}

/* A tank that cannot leave the ground has no altitude to read, so the tape goes
   with the ability. updateFlag() sets the same rule upstream: the tape is up
   while the world allows jumping or the Jumping flag is in hand. */
body.no-jumping #altimeter {
  display: none;
}

/* Identify picks the roaming target for an observer and locks a guided missile
   for a tank, so the touch button is offered to both. Fire and Drop keep their
   own labels, because an observer spends them on the view and on descending. */

/* renderRoaming() draws both of these: renderStatus() puts the roaming label at
   the very top and renderAlerts() stacks the alert slots just under it. They are
   separate things -- the status line persists, an alert times out -- so they
   share a column rather than an element. Both deliberately sit over everything,
   dialogs included: an alert lasts a few seconds and reading it matters more
   than what it covers, and opening a menu is itself what starts the pause
   countdown that has to be read from in front of that menu. */
#hudNotices {
  position: fixed;
  top: max(1vh, 12px);
  left: 0;
  width: 100%;
  z-index: 1100;
  pointer-events: none;
  text-align: center;
}

.alertHudLine,
#firingStatus,
#roamStatus {
  font-size: clamp(1.2rem, 3.2vmin, 2.1rem);
  font-weight: bold;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.9), 0 0 3px rgba(0, 0, 0, 0.9);
  line-height: 1.25;
}

/* messageColor, which is what renderStatus uses for the roaming label. */
#roamStatus {
  color: #cfd8e6;
  display: none;
}

/* warningColor, which is what renderStatus leaves the status in for Sealed --
   upstream's is pure red, and this is the softer red bzo already warns in
   (HUD_ALERT_WARNING_COLOR in hud.js), so the HUD has one warning colour
   rather than two. Shown by hud.js, which is
   also why there is no rule making it visible here. */
#firingStatus {
  color: #ff5a4a;
  display: none;
}

body.observing #roamStatus,
body.autopiloting #roamStatus {
  display: block;
}

/* ScoreboardRenderer::drawRoamTarget brackets the row being watched. */
.scoreboardEntry.selectable {
  cursor: pointer;
}

.scoreboardEntry.roamTarget {
  background: rgba(255, 255, 255, 0.16);
  border-radius: 3px;
}

#shotStatus {
  position: fixed;
  top: 50%;
  left: 50%;
  pointer-events: none;
  z-index: 50;
}

#controlBox {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--motion-box);
  height: var(--motion-box);
  max-width: 100vw;
  max-height: 100vh;
  border: 2px solid rgba(76, 175, 80, 0.6);
  pointer-events: none;
  z-index: 49;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
  transition: border-color 0.3s;
}

/* The flag help goes where HUDRenderer puts it (HUDRenderer.cxx:1787): the
   bottom edge of the targeting box, which is half a motion box below centre,
   with the lines running down from there. Upstream wraps at 75% of the window
   width; here that is a width the browser wraps against, so the breaks land in
   the same places without measuring text. An empty element is an empty line
   box, so nothing has to hide it when no flag is held. */
#flagHelp {
  position: fixed;
  top: calc(50% + var(--motion-box) / 2 + 0.5rem);
  left: 50%;
  transform: translateX(-50%);
  width: 75vw;
  z-index: 50;
  pointer-events: none;
  text-align: center;
  /* messageColor, which is what upstream draws this in -- the colour the
     roaming status line already wears here. */
  color: #cfd8e6;
  font-size: clamp(0.8rem, 2vmin, 1.15rem);
  line-height: 1.3;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.9), 0 0 3px rgba(0, 0, 0, 0.9);
}

/* The dead zone: inside it the mouse asks for no motion at all. */
#noMotionBox {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--motion-dead-box);
  height: var(--motion-dead-box);
  border: 2px solid;
  border-color: inherit;
}

#controlBox.keyboard-mode {
  border-color: rgba(255, 152, 0, 0.6);
}

@media (max-width: 900px), (max-width: 600px) {
  #shotStatus {
  }
}

#radar {
  position: fixed;
  top: var(--hud-edge);
  right: var(--hud-edge);
  z-index: 120;
  border: none;
  border-radius: 6px;
  box-shadow: 0 0 16px #222b, 0 0 0 4px rgba(0,0,0,0.5);
  background: none;
  opacity: 1;
  width: var(--radar-size);
  height: var(--radar-size);
  aspect-ratio: 1 / 1;
  box-sizing: border-box;
  pointer-events: none;
}

@media (max-width: 600px) {
  #radar {
    width: 48vw;
    height: 48vw;
    max-width: calc(100vw - 8vw);
    max-height: calc(100vw - 8vw);
    right: 2vw;
    min-width: 60px;
    min-height: 60px;
  }
}

#settingsHud,
#helpPanel,
#audioOverlay,
#operatorOverlay,
#viewOverlay,
#entryDialog {
  /* `fixed`, not `absolute`. Body is not a positioned ancestor, so an absolute
     dialog centres on the *document* rather than on the window -- and a document
     taller than the window puts `top: 50%` below the middle of what is on
     screen, which is a dialog that opens off the bottom. Only `#settingsHud`
     had this right. */
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(820px, calc(100vw - 32px));
  height: min(86vh, 900px);
  /* svh, not vh, for the same reason `--mobile-btn-size` takes it: on mobile
     Chrome `vh` is the *large* viewport, the one with the URL bar retracted, so
     a dialog sized in vh is taller than the window it has to fit in. Declared
     second so a browser without svh keeps the vh line above. */
  height: min(86svh, 900px);
  padding: 24px;
  border: 1px solid rgba(76, 175, 80, 0.72);
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.82);
  box-shadow: 0 12px 36px #0009;
  box-sizing: border-box;
  overflow-y: auto;
  overflow-x: hidden;
  font-size: var(--hud-font-title);
  line-height: 1.45;
}

/* After the shared block above, not before: that rule names this dialog too
   and at equal specificity the later one wins. */
#viewOverlay {
  /* Wider and taller than the dialogs it shares a block with. They hold prose
     or a form, which wants a comfortable reading width; this one holds two
     tables -- a seven-column list of every running BZFlag server, which is
     hundreds of rows -- and at 820px the titles were the column that paid for
     it. The ceilings only matter on a large screen: both already collapse to
     the viewport on a small one. */
  width: min(1400px, calc(100vw - 24px));
  height: min(94vh, 1200px);
  /* svh for the same reason the shared rule takes it: on mobile Chrome `vh` is
     the viewport with the URL bar retracted, which is taller than the window. */
  height: min(94svh, 1200px);
}

#settingsHud {
  z-index: 1000;
  text-align: left;
  display: none;
  /* The other dialogs hold prose or a form and want the width they are given.
     This one is a list of short label-and-value rows, and stretched to 820px it
     was mostly gap. It takes the width of its widest row instead, with the
     padding as the margin -- still centred, because the transform does that
     rather than the width. The floor keeps a short list from looking like a
     tooltip; the ceiling is the width the others use. A small screen has its own
     rule further down and still fills. */
  width: fit-content;
  /* A floor low enough that the rows decide the width rather than this does; it
     is here so a menu trimmed down to a couple of entries still reads as a
     dialog. */
  min-width: min(260px, calc(100vw - 32px));
  max-width: min(820px, calc(100vw - 32px));
}

#settingsHud .dialogTitle,
#helpPanel .dialogTitle,
#audioOverlay .dialogTitle,
#operatorOverlay .dialogTitle,
#viewOverlay .dialogTitle,
#entryDialog h2 {
  margin: 0 0 18px;
  color: var(--color-accent-light);
  font-size: 2rem;
  font-weight: bold;
  text-align: center;
}

/* Every dialog closes the same way, so every heading lays out the same way:
   the label, then the [X] against the right edge. */
#settingsHud .dialogTitle,
#helpPanel .dialogTitle,
#audioOverlay .dialogTitle,
#operatorOverlay .dialogTitle,
#viewOverlay .dialogTitle,
#entryDialog h2 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--hud-gap);
}

.settingsHudContent {
  width: 100%;
}

.settingsMenuList {
  display: grid;
  gap: 3px;
  width: min(100%, 680px);
  max-width: 100%;
  margin: 0 auto;
}

.settingsMenuRow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: 24px;
  align-items: center;
  width: 100%;
  min-height: 58px;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: 3px;
  background: transparent;
  color: var(--color-fg);
  font: inherit;
  text-align: left;
}

.settingsMenuRow:hover,
.settingsMenuRow:focus-visible {
  border-color: var(--color-accent);
  background: transparent;
}

.settingsMenuRow:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

/* A HUD button for something this player may not do: plainly unavailable rather
   than missing, which is the same rule the renderer's capability gating uses for
   a feature the context cannot draw. */
.settingsHudBtn:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.settingsMenuLabel,
.settingsMenuValue {
  min-width: 0;
  overflow-wrap: anywhere;
}

.settingsMenuLabel {
  text-align: right;
}

.settingsMenuValue {
  color: var(--color-accent-light);
  font-weight: bold;
  text-align: left;
}

.settingsMenuRow.active .settingsMenuValue {
  color: var(--color-fg);
}

/* On and Off read as state, not as decoration, so they are the only values that
   get a colour. Set after the .active rule so an enabled toggle stays green. */
.settingsMenuRow[data-menu-state="off"] .settingsMenuValue {
  color: var(--color-state-off);
}

.settingsMenuRow[data-menu-state="on"] .settingsMenuValue {
  color: var(--color-state-on);
}

/* The one compact block keyed to height as well as width. Short counts as small
   here: a phone held in landscape is wide and short -- a Pixel 10 reports
   857x411 -- so a width-only rule hands it desktop-sized dialogs against the 411
   pixels that actually bind.

   Only here. The other compact blocks reflow for a narrow row -- a radar sized
   off vw alone, buttons at a 60vw minimum, audio rows stacking -- and a wide
   short screen wants none of it: 48vw there is 411px, the whole height. Being
   short and being narrow ask for different things, and the size of a dialog is
   what they have in common. */
@media (max-width: 600px), (max-height: 500px) {
  #settingsHud,
  #helpPanel,
  #audioOverlay,
  #operatorOverlay,
  #viewOverlay,
  #entryDialog {
    height: min(88vh, 900px);
    padding: var(--hud-gap);
    /* Not a fixed 20px: this block also catches a screen that is small because
       it is short, where 20px is what made a dialog outsized. */
    font-size: var(--hud-font-title);
  }

  .settingsMenuRow {
    column-gap: 16px;
    width: 100%;
    min-height: 58px;
    padding: 10px 8px;
  }

  .settingsMenuList {
    width: 100%;
  }
}

/* No column override anywhere. The base rule gives the label and the value an
   equal half each, and since the label is right aligned and the value left
   aligned the pair straddles the middle -- which is what reads as centred. There
   used to be a narrow-screen variant that let the value shrink to its content
   and handed the label all the slack; a right aligned label then hugs the value
   and the empty space piles up on the far side, in portrait as much as in
   landscape. The labels fit an equal half at every width bzo runs at, and
   overflow-wrap on them covers the rest. */
@media (max-width: 600px) {
  /* Filling the screen is worth it for a dialog holding prose or a form that
     would otherwise be squeezed. Settings is not one: it is a column of short
     rows, and it takes its content's width here as it does everywhere, so it is
     only as wide as its widest row needs. */
  #helpPanel,
  #audioOverlay,
  #operatorOverlay,
  #viewOverlay,
  #entryDialog {
    width: calc(100vw - 16px);
  }
}

.audioSettings {
  flex: 1 1 100%;
  width: 100%;
  max-width: 760px;
  margin-top: 1.25rem;
  padding: 1rem;
  border: 1px solid rgba(76, 175, 80, 0.55);
  border-radius: 0.75rem;
  background: rgba(0, 0, 0, 0.35);
  box-sizing: border-box;
  text-align: left;
}

.audioSettings h2 {
  margin: 0 0 0.5rem;
  color: var(--color-accent-light);
  font-size: 1.25rem;
}

.audioSettingsDescription,
.audioSettingHint,
.voicePermissionStatus {
  color: rgba(255, 255, 255, 0.75);
  font-size: 1em;
  line-height: 1.5;
}

.audioSettingsDescription {
  margin: 0 0 1rem;
}

.audioSettingRow {
  display: grid;
  grid-template-columns: minmax(8rem, 0.45fr) minmax(11rem, 1fr);
  gap: 0.35rem 0.75rem;
  align-items: center;
  margin: 0 0 0.85rem;
}

.audioSettingRow > label,
.audioSettingLabel {
  color: var(--color-fg);
  font-weight: bold;
}

.audioSettingRow select {
  width: 100%;
  min-height: 2.25rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid rgba(76, 175, 80, 0.7);
  border-radius: 0.35rem;
  background: rgba(0, 0, 0, 0.75);
  color: var(--color-fg);
  font: inherit;
  box-sizing: border-box;
}

.volumeControl {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  min-width: 0;
}

.volumeSlider {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 2.25rem;
  margin: 0;
  accent-color: var(--color-accent);
  cursor: pointer;
  /* The dialog owns Left/Right on a focused row, so the browser must not also
     scroll the page or pan the canvas when a finger drags the thumb. */
  touch-action: none;
}

.volumeValue {
  flex: 0 0 3.5rem;
  color: var(--color-accent-light);
  font-weight: bold;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.audioSettingRow select:disabled {
  opacity: 0.75;
  cursor: not-allowed;
}

.audioSettingRow select:focus,
.volumeSlider:focus-visible,
.audioCheckboxLabel input:focus-visible,
.audioSettingActions button:focus-visible {
  outline: 2px solid var(--color-btn-bg-focus);
  outline-offset: 2px;
}

.audioSettingHint {
  grid-column: 2;
  margin: 0;
}

.audioProcessingOptions {
  grid-template-columns: 1fr;
  gap: 0.45rem;
  padding-top: 0.25rem;
}

.audioProcessingOptions .audioSettingHint {
  grid-column: 1;
}

.audioCheckboxLabel {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: rgba(255, 255, 255, 0.9);
  font-weight: normal;
  cursor: pointer;
}

.audioCheckboxLabel input {
  accent-color: var(--color-accent);
  inline-size: 1.3rem;
  block-size: 1.3rem;
  margin: 0;
}

.audioSettingActions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.65rem;
  align-items: center;
  margin-top: 0.5rem;
}

.audioSettingActions .settingsHudBtn {
  min-height: 3rem;
  padding: 0.55rem 0.9rem;
  border: 2px solid var(--color-accent);
  border-radius: 0.45rem;
  background: var(--color-btn-bg);
  color: var(--color-fg);
}

.audioSettingActions .settingsHudBtn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.audioSettingActions .voiceMicToggle[aria-pressed="true"] {
  border-color: var(--color-accent-light);
  background: var(--color-btn-bg-active);
}

.voicePermissionStatus {
  min-height: 1.2em;
  margin: 0.75rem 0 0;
}

.settingsHudBtn {
  font-size: 1em;
  border-radius: 4px;
  border: none;
  background: #444;
  color: #fff;
  cursor: pointer;
  transition: background 0.2s;
  box-sizing: border-box;
}

#settingsHud .settingsMenuRow,
#settingsHud .settingsMenuRow:hover,
#settingsHud .settingsMenuRow:focus-visible,
#settingsHud .settingsMenuRow.active {
  background: transparent;
}
@media (max-width: 600px) {
  .settingsHudBtn:not(.settingsMenuRow) {
    min-width: 60vw;
    max-width: 98vw;
    min-height: 40px;
    font-size: 1em;
  }
}

/* Attached to the targeting box's right edge, whatever the screen. The offset
   used to be a fixed 40px against a width in vmin, so the two only agreed at one
   size: 4vmin is 43px on a 1920x1080 desktop and the tape sat against the box,
   but 16px on a 857x411 phone, leaving a 24px gap. Both terms are the box's own
   unit now, so `right` puts the tape's left edge exactly on the box's right edge
   and it cannot drift again. */
#altimeter {
  position: fixed;
  top: 50%;
  right: calc(50% - var(--motion-box) / 2 - var(--altimeter-width));
  transform: translateY(-50%);
  width: var(--altimeter-width);
  height: var(--motion-box);
  z-index: 50;
  pointer-events: none;
  background: transparent;
}
#degreeBar {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  height: 4vmin;
  width: var(--motion-box);
  z-index: 51;
  pointer-events: none;
  background: transparent;
}

@media (max-width: 600px) {
  .audioSettings {
    padding: 0.75rem;
  }

  .audioSettingRow {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0.35rem;
  }

  .audioSettingHint,
  .audioProcessingOptions .audioSettingHint {
    align-self: stretch;
  }

  .audioSettingActions {
    flex-direction: column;
    align-items: stretch;
  }

  .audioSettingActions .settingsHudBtn {
    width: 100%;
    min-height: 2.75rem;
  }

  .entryTeamSelector {
    grid-template-columns: minmax(0, 1fr) minmax(8rem, 1fr);
    gap: 16px;
  }
}

/* The world viewer's panel (public/inspect-panel.mjs): beside the game, not
   over it, so the camera can keep moving while it is open: under the radar,
   and down to chat (`fitPanel`). */
.inspectPanel {
  position: fixed;
  top: calc(var(--radar-size) + 2 * var(--hud-edge) + env(safe-area-inset-top, 0px));
  right: calc(var(--hud-edge) + env(safe-area-inset-right, 0px));
  z-index: 150;
  width: min(420px, calc(100vw - 2 * var(--hud-edge)));
  box-sizing: border-box;
  overflow: auto;
  padding: 8px 10px;
  background: var(--color-panel-bg-solid);
  color: var(--color-fg);
  border: 1px solid var(--color-accent);
  border-radius: 6px;
  font-size: 13px;
}

.inspectPanel[hidden] {
  display: none;
}

.inspectTitle {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  color: var(--color-accent);
  font-weight: bold;
  overflow-wrap: anywhere;
}

.inspectRows {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 10px;
  margin: 6px 0;
}

.inspectRows dt {
  color: var(--color-accent-light);
}

.inspectRows dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.inspectSourceLabel {
  color: var(--color-accent-light);
  margin-top: 6px;
}

.inspectSource pre {
  margin: 2px 0 0;
  padding: 4px 6px;
  background: rgba(255, 255, 255, 0.06);
  tab-size: 4;
  white-space: pre;
  overflow-x: auto;
  font-size: 12px;
}

.inspectButtons {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.inspectWorldBtn,
.inspectPick {
  border: 1px solid var(--color-accent);
  border-radius: 5px;
  background: var(--color-btn-bg);
  color: var(--color-fg);
  cursor: pointer;
  font: inherit;
}

.inspectRows dd {
  white-space: pre-line;
}

.inspectMissing {
  color: var(--color-state-off);
}

.inspectPicks {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 2px;
}

.inspectPick {
  text-align: left;
  white-space: pre;
  overflow: hidden;
  text-overflow: ellipsis;
}

.inspectPanel pre.inspectWarnings {
  margin: 2px 0 0;
  padding: 4px 6px;
  background: rgba(255, 255, 255, 0.06);
  white-space: pre-wrap;
  font-size: 12px;
}
