/* Isomania — UI chrome over the WebGL canvas. Bright, saturated palette to
   match the storybook world; system fonts, zero external assets. */

* { margin: 0; padding: 0; box-sizing: border-box; }

/* The `hidden` attribute must actually hide. The browser implements it as
   `[hidden] { display: none }`, which ANY id rule setting `display` silently
   outranks — and then `el.hidden = true` looks like a no-op. Every overlay
   here is an id with `display: flex`, so state this once and forcefully. */
[hidden] { display: none !important; }

/* NO PINCHING THE PAGE, AND `none` WOULD BE THE WRONG WAY TO SAY IT.
   Players were zooming the interface by accident and had no idea how to get
   back — a real complaint, and on a fixed HUD over a canvas there is nothing
   page zoom could usefully do anyway.

   `pan-x pan-y` is the whole trick: it is the default set MINUS `pinch-zoom`,
   so pinching and double-tap zoom stop while ordinary panning survives.
   `touch-action: none` here would have read as the obvious answer and quietly
   killed scrolling inside the settings sheet, the chat log and the admin
   tables — the value is intersected down the ancestor chain, so a `none` on
   the root is a `none` on everything under it. The canvas states `none` for
   itself further down, which is correct there: its two-finger gesture is the
   camera's, not the browser's.

   `overscroll-behavior` is the same class of accident and a worse one: a pull
   at the top of the page is Chrome's reload gesture, and reloading in the
   middle of a game costs the session rather than the zoom level. And
   `text-size-adjust` stops Safari inflating text of its own accord when the
   phone is turned. */
html {
  touch-action: pan-x pan-y;
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}
body { touch-action: pan-x pan-y; overscroll-behavior: none; }

/* A game, not a document: no text selection / copy anywhere. Inputs keep
   selection so typing and caret editing still behave. */
* {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
input, textarea {
  user-select: text;
  -webkit-user-select: text;
}

/* Admin data is operational content: names, emails, coordinates and SMTP
   values must remain copyable even though the game chrome is not. Controls
   keep the game-like non-selectable behaviour while their surrounding text
   can be selected normally. */
#admin-modal .sheet * {
  user-select: text;
  -webkit-user-select: text;
  -webkit-touch-callout: default;
}
#admin-modal .sheet button,
#admin-modal .sheet select,
#admin-modal .sheet option {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

:root {
  /* Sunny, saturated chrome to match the world. The old palette was olive
     drab, which made even a bright scene feel overcast. */
  --bg: #2f4a2a;
  --panel: rgba(30, 48, 28, 0.80);
  --panel-line: rgba(255, 255, 255, 0.14);
  --text: #f4f7ea;
  --text-dim: #c6d6ac;
  --accent: #b6e86a;
  --accent-dark: #7cc043;
  --danger: #ff8a6a;
  /* Microphone silhouette, used as a mask so it takes the colour of
     whatever it marks. Same shape as the HUD button, flattened to one
     filled path because a mask has no strokes. */
  --mic-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.6a3.1 3.1 0 0 1 3.1 3.1v5.6a3.1 3.1 0 0 1-6.2 0V5.7A3.1 3.1 0 0 1 12 2.6z'/%3E%3Cpath d='M5.7 10.2a.9.9 0 0 1 1.8 0 4.5 4.5 0 0 0 9 0 .9.9 0 0 1 1.8 0 6.3 6.3 0 0 1-5.4 6.2v2.4h2.2a.9.9 0 0 1 0 1.8H8.9a.9.9 0 0 1 0-1.8h2.2v-2.4a6.3 6.3 0 0 1-5.4-6.2z'/%3E%3C/svg%3E");
  --card: #35502e;
  --field: #294224;
  font-size: 16px;
}

html, body { height: 100%; overflow: hidden; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

#game { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }

/* ------------------------------------------------------------ boot screen */

/* Above EVERYTHING, including the sheets at 40: it is the only thing on
   screen until the village is standing. */
#boot {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg);
  transition: opacity 520ms ease;
}
/* Fading out uncovers the village already drifting behind it, which is the
   point of fading rather than switching: the world was built while you read
   this, and the reveal should say so. */
#boot.is-gone { opacity: 0; pointer-events: none; }

/* ENTRY CROSSFADE. Entering the game cuts the camera onto the character; a
   hard cut reads as an abrupt edit, and the old alternative — a long fly-over
   from the plaza — was rejected. So it is a true scene-to-scene DISSOLVE: JS
   captures the outgoing frame (the lobby view) as a still and sets it as this
   sheet's background-image at full opacity, then the camera snaps and the sheet
   fades to transparent over the LIVE incoming frame beneath. Because the still
   is a pixel-copy of what was already on screen, showing it is invisible —
   there is no flash and no solid colour, the plaza simply dissolves into the
   character. (data: image is allowed by img-src.)
   - No background COLOUR by default: the still is set on background-image by
     JS. The one time capture fails, JS sets a BLACK background as the fallback
     — black, never white, which is easier on the eyes.
   - z-index 55: above the HUD (<=30) and the sign-in/resume sheets (40) so
     world and chrome resolve together and the sheet teardown is masked, but
     under #boot / #consent (60).
   - pointer-events: none at EVERY opacity — a sheet stuck opaque could never
     trap a click; the live game runs behind it. This is the deliberate
     difference from the tutorial's is-tour-blur, which sat on the canvas and
     gated input.
   - Baseline opacity 1, so unhiding shows the still instantly (a transition
     never runs on the display:none -> block frame); .is-clear fades it to 0. */
#enter-fade {
  position: fixed; inset: 0; z-index: 55;
  background-size: cover; background-position: center;
  opacity: 1; pointer-events: none;
  transition: opacity 620ms ease;
}
#enter-fade.is-clear { opacity: 0; }

.boot-card { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 20px; }
/* No cream plate behind the mark — same rule as the sign-in card. A little
   smaller here than on the card: this one has to be on screen before the
   scripts are, so it is the one place the file size is felt. */
.boot-card .brand-logo { width: min(248px, 66vw); }

.boot-track {
  position: relative; overflow: hidden;
  width: min(280px, calc(100vw - 72px));
  height: 6px; border-radius: 999px;
  background: rgba(0, 0, 0, 0.22);
}
/* Starts visibly non-zero, because the first stage cannot report until the
   scripts it is waiting for have arrived. */
.boot-fill {
  width: 8%; height: 100%; border-radius: 999px;
  background: var(--accent-dark);
  transition: width 380ms ease;
}
/* The width says how many stages are done; this says it is still working.
   Without it a slow stage reads as a stalled bar.

   ON THE TRACK, NOT ON THE FILL, and that is not tidiness. The longest wait
   of the lot is the first one — the scripts themselves — and nothing can
   report during it, so the fill is still sitting at its opening eight per
   cent. A shimmer confined to the fill would be twenty pixels wide exactly
   when it is the only sign of life on the page. Across the track it is the
   full width throughout. */
.boot-track::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(182, 232, 106, 0.55), transparent);
  animation: boot-sweep 1.35s ease-in-out infinite;
}
@keyframes boot-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .boot-track::after { animation: none; }
  #boot { transition: none; }
  /* No fade for reduced-motion: the sheet shows for a frame or two and clears
     instantly, so there is no animation state that could leak. */
  #enter-fade { transition: none; }
}

.boot-step { color: var(--text-dim); font-size: 14px; text-align: center; min-height: 1.3em; }
/* A boot that cannot finish has to say so HERE. The toast it used to raise
   was underneath this screen and nobody ever saw it. */
.boot-step.is-error { color: #f2b8b8; }


/* ------------------------------------------------ overlay (labels/bubbles) */

#overlay { position: fixed; inset: 0; pointer-events: none; overflow: hidden; }

.pwrap {
  position: absolute; left: 0; top: 0;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  will-change: transform;
}

.plabel {
  font-size: 12px; font-weight: 600; letter-spacing: 0.02em;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 8px rgba(0, 0, 0, 0.5);
  white-space: nowrap;
}

/* Whoever is talking right now: a small speaker above the name.
 *
 * Above rather than beside, because beside makes the name jump sideways every
 * time somebody starts and stops a sentence — the wrapper is centred, so an
 * inline marker shifts the whole label. Stacked, the name holds still and only
 * the space above it grows. */
.pvoice {
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  color: #9ae04f;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.85));
}
.pvoice svg { width: 100%; height: 100%; display: block; }
.pvoice .pvoice-cone { fill: currentColor; stroke: none; }
.pvoice .pvoice-wave {
  fill: none; stroke: currentColor; stroke-width: 2.1; stroke-linecap: round;
  animation: pvoice-pulse 1s ease-in-out infinite;
}
/* The outer wave trails the inner one, so it reads as sound travelling
   outward rather than the whole glyph blinking. */
.pvoice .pvoice-wave:last-child { animation-delay: 0.16s; }

@keyframes pvoice-pulse {
  0%, 100% { opacity: 0.35; }
  45% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .pvoice .pvoice-wave { animation: none; opacity: 1; }
}

.plabel.is-speaking {
  color: #d6ffb0;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 9px rgba(154, 224, 79, 0.7);
}

.pbubble {
  max-width: 220px;
  background: rgba(255, 253, 245, 0.97);
  color: #22331c;
  font-size: 13px; line-height: 1.35;
  padding: 6px 10px;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  word-wrap: break-word;
  position: relative;
}
.pbubble::after {
  content: ""; position: absolute; left: 50%; bottom: -5px;
  width: 10px; height: 10px;
  background: rgba(255, 253, 245, 0.97);
  transform: translateX(-50%) rotate(45deg);
}
.pbubble.pop { animation: bubble-pop 0.18s ease-out; }
@keyframes bubble-pop {
  from { transform: scale(0.7); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* A line that was spoken, not typed. Marked because it is a weaker claim:
 * recognition mishears, and a reader — a moderator above all — should know
 * whether somebody chose those words or a machine guessed them out of a noisy
 * room. A quiet glyph, not a badge: it must not shout over the message. */
.chat-line.is-spoken .chat-name::after {
  content: '';
  display: inline-block;
  width: 9px; height: 9px;
  margin-left: 4px;
  vertical-align: -1px;
  background: currentColor;
  opacity: 0.72;
  /* A microphone silhouette as a mask, so it inherits the name's colour and
     needs no second copy of the icon in the markup. */
  -webkit-mask: var(--mic-mask) center/contain no-repeat;
  mask: var(--mic-mask) center/contain no-repeat;
}

/* Still being spoken: the recogniser rewrites this several times a second, so
 * it is deliberately dimmer, italic and without the pop animation a settled
 * message gets. It reads as "being said" rather than "said". */
.pbubble.tentative {
  opacity: 0.72;
  font-style: italic;
  border-style: dashed;
}

/* ----------------------------------------------------------------- HUD */

/* One bar, bottom right. Groups are separated by hairlines rather than by
   floating apart, so the whole thing reads as a single control cluster. */
#hud-bar {
  position: fixed; bottom: 14px; right: 16px;
  display: flex; align-items: stretch;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 12px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.32);
  overflow: hidden;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.hud-group {
  display: flex; align-items: center; gap: 8px;
  padding: 0 14px; height: 40px;
  font-size: 13px; color: var(--text-dim);
  white-space: nowrap;
}

.hud-sep { width: 1px; background: var(--panel-line); }

.hud-btn {
  background: none; border: none;
  color: var(--text-dim);
  cursor: pointer;
  padding: 0 13px;
  transition: background 0.12s ease, color 0.12s ease;
}
.hud-btn:hover { background: rgba(255, 255, 255, 0.09); color: var(--text); }
.hud-btn:active { background: rgba(255, 255, 255, 0.15); }

.ico { width: 19px; height: 19px; display: block; }
.ico path { fill: currentColor; stroke: none; }
/* Waves are the only stroked part — a filled arc would just be a blob. */
.ico .wave { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
.ico-gear { width: 18px; height: 18px; }
.ico-admin path,
.ico-admin circle {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
#admin-btn { color: var(--text); }
#admin-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.09); }
/* An anchor wearing a button's clothes: everything else in this bar is a
   <button>, which carries none of a link's decoration. */
#editor-btn { color: var(--text); text-decoration: none; }
#editor-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.09); }
/* A door-and-arrow glyph only reads as line art; filled it turns to mush. */
.ico-exit path { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
#logout-btn:hover { color: var(--danger); }

/* Three glyphs in one button; only the live view's is shown. Same trick as
   the speaker below, one state wider. */
.ico-view path, .ico-view circle {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
#view-btn .ico-view { display: none; }
#view-btn[data-view="0"] .ico-view-iso,
#view-btn[data-view="1"] .ico-view-tps,
#view-btn[data-view="2"] .ico-view-fps { display: block; }
#view-btn[data-view="1"], #view-btn[data-view="2"] { color: #fff; }

/* Corner brackets: outward to go full screen, inward to come back. */
.ico-fs path {
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
#fullscreen-btn .ico-fs-exit { display: none; }
#fullscreen-btn[data-full="1"] .ico-fs-enter { display: none; }
#fullscreen-btn[data-full="1"] .ico-fs-exit { display: block; }
#fullscreen-btn[data-full="1"] { color: #fff; }

/* The speaker swaps between two glyphs rather than being drawn twice. */
#sound-btn .ico-sound-off { display: none; }
#sound-btn[data-muted="1"] .ico-sound-on { display: none; }
#sound-btn[data-muted="1"] .ico-sound-off { display: block; }
#sound-btn[data-muted="1"] { color: var(--danger); }

/* The capsule is filled like the speaker's cone; the cradle, stem and base are
 * stroked like its waves. Left to the default `.ico path { fill: currentColor }`
 * the cradle arc fills into a solid crescent and the glyph reads as a lollipop
 * rather than a microphone. */
.ico-mic .mic-line, .ico-mic .mic-slash {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}

/* Four states off one attribute, and the two that matter are unmistakable at a
 * glance: someone who forgets the microphone is open is broadcasting their
 * room. Off borrows the muted speaker's red so the pair beside each other read
 * as one idea; live is the same green as the online dot. */
#mic-btn .mic-slash { display: none; }
#mic-btn[data-mic="0"] { color: var(--danger); }
#mic-btn[data-mic="0"] .mic-slash { display: block; }
#mic-btn[data-mic="1"] { color: #9ae04f; }
#mic-btn[data-mic="2"] { color: #cdff9a; background: rgba(154, 224, 79, 0.2); }
#mic-btn[data-mic="2"] .ico-mic { animation: mic-live 1.1s ease-in-out infinite; }
/* Unavailable is grey rather than red: red says "you switched this off", and
 * a browser without an encoder, or a player nobody has granted a microphone,
 * did not switch anything. */
#mic-btn[data-mic="3"] { opacity: 0.38; cursor: not-allowed; }
#mic-btn[data-mic="3"] .mic-slash { display: block; }

/* Webcam tracking. Same four-state grammar as the microphone, and the same
   reason for it: the button is the only feedback there is, because the design
   deliberately has no preview window. State 1 — camera open, nobody detected —
   is what tells a player their shoulders are out of frame. */
.ico-cam .cam-slash {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
#cam-btn .cam-slash { display: none; }
#cam-btn[data-cam="0"] { color: var(--danger); }
#cam-btn[data-cam="0"] .cam-slash { display: block; }
#cam-btn[data-cam="1"] { color: #e8c987; }
#cam-btn[data-cam="2"] { color: #9ae04f; background: rgba(154, 224, 79, 0.2); }
#cam-btn[data-cam="3"] { opacity: 0.38; cursor: not-allowed; }
#cam-btn[data-cam="3"] .cam-slash { display: block; }

@keyframes mic-live {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.13); }
}
@media (prefers-reduced-motion: reduce) {
  #mic-btn[data-mic="2"] .ico-mic { animation: none; }
}

#status { width: 9px; height: 9px; border-radius: 50%; background: #888; flex-shrink: 0; }
#status[data-state="online"] { background: #9ae04f; box-shadow: 0 0 8px rgba(154, 224, 79, 0.75); }
#status[data-state="connecting"] { background: #ffd24a; }
#status[data-state="offline"], #status[data-state="replaced"] { background: var(--danger); }

#online { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------- settings sheet */

#settings-modal,
#admin-modal {
  position: fixed; inset: 0; z-index: 25;
  display: flex; align-items: flex-start; justify-content: center;
  padding: clamp(16px, 3vh, 28px) 14px;
  overflow: hidden;
  background: rgba(18, 36, 16, 0.5);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
}

.sheet {
  width: min(440px, calc(100vw - 28px));
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--panel-line);
  border-radius: 16px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.5);
}

/* Admin stays a dialog but fills the viewport like a page. Navigation has
   dedicated rows; only the active panel scrolls, regardless of header height. */
#admin-modal { padding: 0; height: 100dvh; }
#admin-modal .sheet {
  width: 100%; height: 100%; min-width: 0; min-height: 0;
  border: 0; border-radius: 0; box-shadow: none; overflow: hidden;
  display: grid; grid-template-rows: auto auto minmax(0, 1fr);
}
#admin-modal .sheet > .sheet-head {
  position: static;
  padding-top: max(16px, env(safe-area-inset-top));
  padding-left: max(18px, env(safe-area-inset-left));
  padding-right: max(18px, env(safe-area-inset-right));
}
#admin-modal .sheet > .tabs {
  position: static; min-width: 0; overflow-x: auto; overflow-y: hidden; flex-shrink: 0;
  padding-left: max(18px, env(safe-area-inset-left));
  padding-right: max(18px, env(safe-area-inset-right));
}
#admin-modal .tabs > .tab { flex: 1 0 auto; white-space: nowrap; margin-bottom: 0; }
#admin-modal .sheet > .tab-panel {
  min-height: 0; min-width: 0; overflow: auto; overscroll-behavior: contain;
  padding-left: max(18px, env(safe-area-inset-left));
  padding-right: max(18px, env(safe-area-inset-right));
  padding-bottom: max(18px, env(safe-area-inset-bottom));
}
#admin-modal fieldset { min-width: 0; }
#admin-modal .cloudflare-form-actions { flex-wrap: wrap; }
#admin-modal .admin-action { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
#admin-modal #world-resets { justify-content: flex-start; }
#settings-modal .sheet,
#admin-modal .sheet { max-height: 100%; }

.sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 18px 12px;
  border-bottom: 1px solid var(--panel-line);
  position: sticky; top: 0;
  background: var(--card);
}
.sheet-head h2 { font-size: 19px; }
/* Wider than the wordmark would need at the same height: the lockup spends a
   third of its width on the cottage, so matching the old width would have
   shrunk the letters by a third with it. Sized so the WORD stays the size it
   was, not so the image does. */
.sheet-head h2 .brand-logo { width: 170px; }
.sheet-close {
  background: none; border: none; cursor: pointer;
  color: var(--text-dim); font-size: 17px; line-height: 1;
  padding: 6px 8px; border-radius: 8px;
}
.sheet-close:hover { background: rgba(255, 255, 255, 0.1); color: var(--text); }

/* Tabs. One row, equal widths, the active one marked by an underline rather
   than a filled pill so the header stays quiet. */
.tabs {
  display: flex;
  padding: 0 18px;
  border-bottom: 1px solid var(--panel-line);
  position: sticky; top: 53px;
  background: var(--card);
  z-index: 1;
}
.tab {
  flex: 1;
  background: none; border: none; cursor: pointer;
  color: var(--text-dim);
  font-size: 13px; font-weight: 600;
  padding: 11px 6px 10px;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.tab:hover { color: var(--text); }
.tab.is-on { color: var(--accent); border-bottom-color: var(--accent); }

.tab-panel { padding: 14px 18px 18px; }

.admin-placeholder { padding: 8px 0 4px; }
.admin-placeholder strong { display: block; font-size: 14px; margin-bottom: 5px; }
.admin-placeholder p { color: var(--text-dim); font-size: 13px; line-height: 1.5; }

/* -------------------------------------------------------- admin players */

.player-admin-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  margin-bottom: 12px;
}
.player-admin-head h3 { font-size: 16px; margin-bottom: 4px; }
.player-admin-head p { color: var(--text-dim); font-size: 13px; line-height: 1.45; max-width: 480px; }
.player-admin-tools { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.player-admin-tools input {
  width: min(230px, 32vw);
  border: 1px solid var(--panel-line); border-radius: 8px; outline: none;
  background: var(--field); color: var(--text);
  font: inherit; font-size: 12px; padding: 8px 10px;
}
.player-admin-tools input:focus { border-color: var(--accent-dark); box-shadow: 0 0 0 2px rgba(124, 192, 67, 0.14); }
.player-admin-tools input::placeholder { color: rgba(198, 214, 172, 0.5); }

.player-admin-message {
  border: 1px solid var(--panel-line); border-radius: 9px;
  margin-bottom: 12px; padding: 9px 11px;
  color: var(--text-dim); font-size: 12px; line-height: 1.45;
}
.player-admin-message[data-kind="success"] {
  border-color: rgba(182, 232, 106, 0.42);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.player-admin-message[data-kind="error"] {
  border-color: rgba(255, 105, 145, 0.48);
  background: rgba(255, 82, 128, 0.1); color: #ffabc0;
}
.player-admin-summary {
  display: flex; gap: 18px; margin: 0 2px 11px;
  color: var(--text-dim); font-size: 11px;
}
.player-admin-summary strong { color: var(--text); font-variant-numeric: tabular-nums; }

.player-admin-editor { margin-bottom: 14px; }
.player-admin-form-grid {
  display: grid;
  grid-template-columns: minmax(120px, 1fr) minmax(190px, 1.45fr) minmax(110px, 0.8fr) minmax(90px, 0.62fr) minmax(90px, 0.62fr);
  gap: 10px; padding-top: 4px;
}
.player-admin-editor-note { margin-top: 9px; color: var(--text-dim); font-size: 10px; line-height: 1.4; }
.player-admin-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }

.player-admin-table-wrap {
  position: relative; overflow: hidden;
  border: 1px solid var(--panel-line); border-radius: 11px;
  background: rgba(255, 255, 255, 0.025);
}
.player-admin-table {
  width: 100%; table-layout: fixed; border-collapse: collapse;
  font-size: 11px;
}
/* NINE columns, and the percentages must add up to exactly 100.
 *
 * The table is `table-layout: fixed` inside a wrapper with `overflow: hidden`,
 * so a column with no width here gets nothing and is clipped away silently.
 * That is what happened when the voice column was inserted: every later column
 * shifted one place, the actions fell off the end of this list, and the
 * buttons — still perfectly present in the markup — were squeezed into a strip
 * of zero width. The only visible symptom was rows growing taller, one button
 * height at a time. Add a column, add a line here. */
/* …and the camera column arrived without one, which is exactly the failure
 * this note warns about: ten columns against nine rules left every width from
 * the fifth onward attached to the wrong column and the last one with no rule
 * at all. That, rather than the checkbox itself, is most of why the two grant
 * columns did not look like a pair. */
.player-admin-table th:nth-child(1) { width: 13%; }   /* name */
.player-admin-table th:nth-child(2) { width: 15%; }   /* email */
.player-admin-table th:nth-child(3) { width: 8%; }    /* role */
.player-admin-table th:nth-child(4) { width: 6%; }    /* voice — a checkbox */
.player-admin-table th:nth-child(5) { width: 6%; }    /* camera — the same */
.player-admin-table th:nth-child(6) { width: 9%; }    /* presence */
.player-admin-table th:nth-child(7) { width: 11%; }   /* coordinates */
.player-admin-table th:nth-child(8) { width: 10%; }   /* last seen */
.player-admin-table th:nth-child(9) { width: 10%; }   /* created */
.player-admin-table th:nth-child(10) { width: 12%; }  /* actions */
.player-admin-table th {
  text-align: left; white-space: nowrap;
  color: var(--accent); font-size: 9px; font-weight: 800;
  letter-spacing: 0.065em; text-transform: uppercase;
  padding: 9px 8px; border-bottom: 1px solid var(--panel-line);
  background: rgba(31, 52, 27, 0.96);
}
.player-admin-table td {
  padding: 9px 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  color: var(--text-dim); vertical-align: middle; white-space: nowrap;
}
.player-admin-table tbody tr:last-child td { border-bottom: 0; }
.player-admin-table tbody tr:hover { background: rgba(255, 255, 255, 0.045); }
.player-admin-table tbody tr.is-self { background: rgba(182, 232, 106, 0.045); }
.player-admin-table td:first-child strong {
  display: block; overflow: hidden; color: var(--text); font-size: 11px;
  text-overflow: ellipsis;
}
.player-admin-table td:first-child small { display: block; margin-top: 2px; color: var(--text-dim); font-size: 9px; }
.player-admin-table td:nth-child(2) {
  overflow: hidden; text-overflow: ellipsis;
}
.player-admin-coordinates { font-variant-numeric: tabular-nums; }
.player-admin-role,
.player-admin-presence {
  display: inline-flex; align-items: center; gap: 5px;
  border: 1px solid var(--panel-line); border-radius: 999px;
  padding: 3px 7px; font-size: 9px; font-weight: 800; white-space: nowrap;
}
.player-admin-role[data-role="admin"] {
  border-color: rgba(255, 105, 145, 0.5); color: #ffabc0;
  background: rgba(255, 82, 128, 0.08);
}
.player-admin-role[data-role="user"] { color: var(--text-dim); }
.player-admin-presence i { width: 6px; height: 6px; border-radius: 50%; background: #819078; }
.player-admin-presence[data-online="1"] { color: var(--accent); border-color: rgba(182, 232, 106, 0.35); }
.player-admin-presence[data-online="1"] i { background: var(--accent); box-shadow: 0 0 6px rgba(182, 232, 106, 0.65); }
.player-admin-table th:last-child { text-align: center; }
.player-admin-actions { padding: 7px 8px; }
.player-admin-action-list {
  display: flex; flex-direction: column; align-items: stretch; gap: 5px;
  width: 100%;
}
.player-admin-actions .admin-action {
  width: 100%; padding: 5px 7px; font-size: 10px;
  line-height: 1.25; white-space: normal;
}
.player-admin-actions .player-admin-teleport { color: var(--accent); }
.player-admin-actions .player-admin-teleport:hover { color: #17300f; background: var(--accent); }
.player-admin-empty { padding: 26px 14px; text-align: center; color: var(--text-dim); font-size: 12px; }

/* ----------------------------------------------------------- admin chat */

.chat-admin-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  margin-bottom: 12px;
}
.chat-admin-head h3 { font-size: 16px; margin-bottom: 4px; }
.chat-admin-head p { color: var(--text-dim); font-size: 13px; line-height: 1.45; }
.chat-admin-tools { display: flex; justify-content: flex-end; gap: 8px; }
.chat-admin-filter[data-on="1"] {
  border-color: rgba(255, 105, 145, 0.62);
  background: rgba(255, 82, 128, 0.14);
  color: #ffabc0;
}
.chat-admin-message {
  border: 1px solid var(--panel-line); border-radius: 9px;
  margin-bottom: 12px; padding: 9px 11px;
  color: var(--text-dim); font-size: 12px; line-height: 1.45;
}
.chat-admin-message[data-kind="success"] {
  border-color: rgba(182, 232, 106, 0.42);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.chat-admin-message[data-kind="error"] {
  border-color: rgba(255, 105, 145, 0.48);
  background: rgba(255, 82, 128, 0.1); color: #ffabc0;
}
.chat-admin-summary { margin: 0 2px 11px; color: var(--text-dim); font-size: 11px; }
.chat-admin-summary strong { color: var(--text); font-variant-numeric: tabular-nums; }
.chat-admin-table-wrap {
  overflow: hidden; border: 1px solid var(--panel-line); border-radius: 11px;
  background: rgba(255, 255, 255, 0.025);
}
.chat-admin-table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 11px; }
.chat-admin-table th:nth-child(1) { width: 14%; }
.chat-admin-table th:nth-child(2) { width: 15%; }
.chat-admin-table th:nth-child(3) { width: 21%; }
.chat-admin-table th:nth-child(4) { width: 42%; }
.chat-admin-table th:nth-child(5) { width: 8%; text-align: center; }
.chat-admin-table th {
  padding: 9px 8px; border-bottom: 1px solid var(--panel-line);
  background: rgba(31, 52, 27, 0.96); color: var(--accent);
  font-size: 9px; font-weight: 800; letter-spacing: 0.065em;
  text-align: left; text-transform: uppercase; white-space: nowrap;
}
.chat-admin-table td {
  padding: 9px 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  color: var(--text-dim); vertical-align: top;
}
.chat-admin-table tbody tr:last-child td { border-bottom: 0; }
.chat-admin-table tbody tr:hover { background: rgba(255, 255, 255, 0.045); }
.chat-admin-time { font-variant-numeric: tabular-nums; white-space: nowrap; }
.chat-admin-author strong { display: block; color: var(--text); }
.chat-admin-author small { display: block; margin-top: 2px; font-size: 9px; }
.chat-admin-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-admin-text { color: var(--text) !important; line-height: 1.4; overflow-wrap: anywhere; white-space: pre-wrap; }
.chat-admin-actions { text-align: center; }
.chat-admin-actions .admin-action { padding: 5px 7px; font-size: 10px; white-space: normal; }
.chat-admin-empty { padding: 30px 14px; text-align: center; color: var(--text-dim); font-size: 12px; }
.chat-admin-more-wrap { display: flex; justify-content: center; padding-top: 12px; }

.chat-blocked-word {
  color: #ff789b;
  font-weight: 750;
  text-decoration: underline;
  text-decoration-color: rgba(255, 120, 155, 0.45);
  text-underline-offset: 2px;
}

/* ----------------------------------------------------- admin Cloudflare */

.cloudflare-admin-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  margin-bottom: 14px;
}
.cloudflare-admin-head h3 { font-size: 16px; margin-bottom: 4px; }
.cloudflare-admin-head p {
  color: var(--text-dim); font-size: 13px; line-height: 1.45; max-width: 600px;
}
.cloudflare-status {
  flex: 0 0 auto;
  border: 1px solid var(--panel-line); border-radius: 999px;
  padding: 5px 9px;
  color: var(--text-dim); font-size: 10px; font-weight: 800;
}
.cloudflare-status[data-enabled="1"] {
  border-color: rgba(182, 232, 106, 0.48);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.cloudflare-message {
  border: 1px solid var(--panel-line); border-radius: 9px;
  margin-bottom: 14px; padding: 9px 11px;
  color: var(--text-dim); font-size: 12px; line-height: 1.45;
}
.cloudflare-message[data-kind="success"] {
  border-color: rgba(182, 232, 106, 0.42);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.cloudflare-message[data-kind="error"] {
  border-color: rgba(255, 105, 145, 0.48);
  background: rgba(255, 82, 128, 0.1); color: #ffabc0;
}
.cloudflare-settings { max-width: 820px; margin: 0 auto; }
.cloudflare-help { margin: 3px 0 12px; color: var(--text-dim); font-size: 11px; line-height: 1.5; }
.cloudflare-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.cloudflare-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 15px; }

/* ---------------------------------------------------------- admin Google */

.google-admin-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  margin-bottom: 14px;
}
.google-admin-head h3 { font-size: 16px; margin-bottom: 4px; }
.google-admin-head p {
  color: var(--text-dim); font-size: 13px; line-height: 1.45; max-width: 650px;
}
.google-status {
  flex: 0 0 auto;
  border: 1px solid var(--panel-line); border-radius: 999px;
  padding: 5px 9px;
  color: var(--text-dim); font-size: 10px; font-weight: 800;
}
.google-status[data-enabled="1"] {
  border-color: rgba(182, 232, 106, 0.48);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.google-message {
  border: 1px solid var(--panel-line); border-radius: 9px;
  margin-bottom: 14px; padding: 9px 11px;
  color: var(--text-dim); font-size: 12px; line-height: 1.45;
}
.google-message[data-kind="success"] {
  border-color: rgba(182, 232, 106, 0.42);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.google-message[data-kind="error"] {
  border-color: rgba(255, 105, 145, 0.48);
  background: rgba(255, 82, 128, 0.1); color: #ffabc0;
}
.google-settings { max-width: 820px; margin: 0 auto; }
.google-help { margin: 3px 0 12px; color: var(--text-dim); font-size: 11px; line-height: 1.5; }
.google-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.google-field-wide { grid-column: 1 / -1; }
.google-switches { display: grid; gap: 8px; margin-top: 14px; }
.google-switch {
  display: grid; grid-template-columns: auto 1fr; align-items: flex-start; gap: 10px;
  border: 1px solid var(--panel-line); border-radius: 9px;
  padding: 10px 11px; background: rgba(255, 255, 255, 0.025); cursor: pointer;
}
.google-switch input { width: 17px; height: 17px; margin-top: 1px; accent-color: var(--accent-dark); }
.google-switch strong { display: block; color: var(--text); font-size: 12px; }
.google-switch small { display: block; margin-top: 3px; color: var(--text-dim); font-size: 10px; line-height: 1.4; }
.google-switch:has(input:disabled) { cursor: default; opacity: 0.58; }
.google-console-link { margin: 12px 2px 0; font-size: 11px; }
.google-console-link a { color: var(--accent); }
.google-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 15px; }

/* ------------------------------------------------------- admin Telegram */

.telegram-admin-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  margin-bottom: 14px;
}
.telegram-admin-head h3 { font-size: 16px; margin-bottom: 4px; }
.telegram-admin-head p {
  color: var(--text-dim); font-size: 13px; line-height: 1.45; max-width: 650px;
}
.telegram-status {
  flex: 0 0 auto;
  border: 1px solid var(--panel-line); border-radius: 999px;
  padding: 5px 9px;
  color: var(--text-dim); font-size: 10px; font-weight: 800;
}
.telegram-status[data-enabled="1"] {
  border-color: rgba(182, 232, 106, 0.48);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.telegram-message {
  border: 1px solid var(--panel-line); border-radius: 9px;
  margin-bottom: 14px; padding: 9px 11px;
  color: var(--text-dim); font-size: 12px; line-height: 1.45;
}
.telegram-message[data-kind="success"] {
  border-color: rgba(182, 232, 106, 0.42);
  background: rgba(182, 232, 106, 0.08); color: var(--accent);
}
.telegram-message[data-kind="error"] {
  border-color: rgba(255, 105, 145, 0.48);
  background: rgba(255, 82, 128, 0.1); color: #ffabc0;
}
.telegram-settings { max-width: 820px; margin: 0 auto; }
.telegram-help { margin: 3px 0 12px; color: var(--text-dim); font-size: 11px; line-height: 1.5; }
.telegram-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.telegram-field-wide { grid-column: 1 / -1; }
.telegram-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 15px; }

/* The event switches. Two columns on a wide panel because the list is long
 * enough that one column pushes the buttons off the screen, and grouped by
 * heading because "Дії адміністратора" and "У грі" are watched by different
 * moods. */
.telegram-events { margin-top: 16px; }
.admin-hint { margin: 0 0 10px; color: var(--text-dim); font-size: 11px; line-height: 1.5; }
.tg-events { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 22px; }
.tg-events-group { grid-column: 1 / -1; margin: 10px 0 2px; color: var(--accent);
  font-size: 9px; font-weight: 800; letter-spacing: 0.065em; text-transform: uppercase; }
.tg-events-group:first-child { margin-top: 0; }
.tg-event { display: flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 3px 0; color: var(--text); font-size: 12px; }
.tg-event input { width: 15px; height: 15px; cursor: pointer; accent-color: #9ae04f; flex: none; }
.tg-event:has(input:disabled) { cursor: default; opacity: 0.58; }
@media (max-width: 720px) { .tg-events { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------- admin SMTP */

.smtp-admin-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  margin-bottom: 14px;
}
.smtp-admin-head h3 { font-size: 16px; margin-bottom: 4px; }
.smtp-admin-head p { color: var(--text-dim); font-size: 13px; line-height: 1.45; max-width: 570px; }

.admin-action {
  border: 1px solid var(--panel-line);
  border-radius: 8px;
  background: var(--field);
  color: var(--text);
  cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 700;
  line-height: 1.2;
  padding: 8px 11px;
  white-space: nowrap;
}
.admin-action:hover { background: rgba(255, 255, 255, 0.1); }
.admin-action:disabled { cursor: default; opacity: 0.52; }
.admin-action-primary { border-color: transparent; background: var(--accent-dark); color: #17300f; }
.admin-action-primary:hover { background: var(--accent); }
.admin-action-danger { color: #ffabc0; }

.smtp-message {
  border: 1px solid var(--panel-line);
  border-radius: 9px;
  margin-bottom: 14px;
  padding: 9px 11px;
  color: var(--text-dim);
  font-size: 12px; line-height: 1.45;
}
.smtp-message[data-kind="success"] {
  border-color: rgba(182, 232, 106, 0.42);
  background: rgba(182, 232, 106, 0.08);
  color: var(--accent);
}
.smtp-message[data-kind="error"] {
  border-color: rgba(255, 105, 145, 0.48);
  background: rgba(255, 82, 128, 0.1);
  color: #ffabc0;
}

.smtp-admin-layout { display: grid; grid-template-columns: 1fr; gap: 14px; }
.smtp-admin-left {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start; gap: 14px; min-width: 0;
}
.smtp-admin-left .fgroup { margin-bottom: 0; }

/* The overview uses the whole sheet: profiles on the left and test tools on
   the right. Only while creating/editing does that side become the editor. */
.smtp-admin-layout.is-editing { grid-template-columns: minmax(270px, 0.85fr) minmax(360px, 1.15fr); }
.smtp-admin-layout.is-editing .smtp-admin-left { display: block; }
.smtp-admin-layout.is-editing .smtp-admin-left .fgroup:first-child { margin-bottom: 14px; }
.smtp-profile-list { display: grid; gap: 8px; padding-top: 4px; }
.smtp-empty { padding: 11px 2px 3px; color: var(--text-dim); font-size: 12px; line-height: 1.5; }

.smtp-profile-row {
  border: 1px solid var(--panel-line);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.035);
  padding: 10px;
}
.smtp-profile-row.is-active { border-color: rgba(182, 232, 106, 0.48); background: rgba(182, 232, 106, 0.06); }
.smtp-profile-title { display: flex; align-items: center; gap: 7px; min-width: 0; }
.smtp-profile-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.smtp-profile-meta { margin-top: 5px; color: var(--text-dim); font-size: 11px; line-height: 1.45; overflow-wrap: anywhere; white-space: pre-line; }
.smtp-profile-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
.smtp-profile-actions .admin-action { padding: 6px 8px; }
.smtp-chip {
  border: 1px solid var(--panel-line);
  border-radius: 999px;
  padding: 2px 6px;
  color: var(--text-dim);
  font-size: 9px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase;
  white-space: nowrap;
}
.smtp-chip-active { border-color: rgba(182, 232, 106, 0.48); color: var(--accent); }
.smtp-chip-disabled { border-color: rgba(255, 138, 106, 0.45); color: var(--danger); }

.smtp-editor { margin-bottom: 0; align-self: start; }
.smtp-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 11px 12px; padding-top: 4px; }
.smtp-field-wide { grid-column: 1 / -1; }
.admin-field { display: grid; align-content: start; gap: 5px; min-width: 0; color: var(--text-dim); font-size: 11px; font-weight: 700; }
.admin-field input,
.admin-field select {
  width: 100%; min-width: 0;
  border: 1px solid var(--panel-line);
  border-radius: 8px;
  outline: none;
  background: var(--field);
  color: var(--text);
  font: inherit; font-size: 13px; font-weight: 400;
  padding: 8px 9px;
}
.admin-field input:focus,
.admin-field select:focus { border-color: var(--accent-dark); box-shadow: 0 0 0 2px rgba(124, 192, 67, 0.14); }
.admin-field input::placeholder { color: rgba(198, 214, 172, 0.5); }
.admin-field small { color: var(--text-dim); font-size: 10px; font-weight: 400; line-height: 1.35; }
.admin-toggle { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: 12px; cursor: pointer; }
.admin-toggle input { width: 16px; height: 16px; accent-color: var(--accent-dark); cursor: pointer; }
.smtp-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 15px; }
.smtp-active-summary { margin: 5px 0 10px; color: var(--text-dim); font-size: 12px; }
.smtp-active-summary strong { color: var(--text); }
.smtp-test-btn { width: 100%; margin-top: 9px; }
.smtp-help { margin-top: 8px; color: var(--text-dim); font-size: 10px; line-height: 1.45; }

/* Named group of form controls. The legend sits in the gap in the top border,
   which is exactly what fieldset is for — and it means stacking a second
   group under this tab costs one more <fieldset> and nothing else. */
.fgroup {
  border: 1px solid var(--panel-line);
  border-radius: 12px;
  padding: 4px 14px 12px;
  margin: 0 0 14px;
  min-width: 0;                 /* fieldsets refuse to shrink without this */
}
.fgroup:last-child { margin-bottom: 0; }
.fgroup > legend {
  padding: 0 7px;
  margin-left: -3px;
  font-size: 11px; font-weight: 700;
  letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--accent);
}

/* Reference tables */
.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
.tbl th {
  text-align: left; font-size: 11px; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--accent); font-weight: 700;
  padding: 0 8px 8px; border-bottom: 1px solid var(--panel-line);
}
.tbl td { padding: 7px 8px; color: var(--text-dim); vertical-align: middle; }
.tbl tbody tr:nth-child(odd) { background: rgba(255, 255, 255, 0.04); }
.tbl td:first-child { color: var(--text); white-space: nowrap; width: 1%; }
.tbl tbody tr:first-child td { padding-top: 9px; }

.players-table th:nth-child(2),
.players-table td:nth-child(2) { font-variant-numeric: tabular-nums; white-space: nowrap; }
.players-table th:last-child,
.players-table td:last-child { text-align: right; }
.players-table tbody tr[data-self="1"] td:first-child { color: var(--accent); font-weight: 700; }
.players-table tfoot td {
  border-top: 1px solid var(--panel-line);
  padding-top: 10px;
  color: var(--text);
  font-weight: 700;
}
.player-role {
  display: inline-block;
  border: 1px solid var(--panel-line);
  border-radius: 999px;
  padding: 2px 7px;
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 700;
}

/* Silencing a neighbour sits beside their name because that is the moment it
 * is wanted. Local to this browser: nobody is told, and the server is not
 * involved. */
/* THE TWO GRANT COLUMNS, styled as ONE rule rather than two nearly identical
 * ones — the same reasoning the script uses for its shared `GRANTS` table. The
 * camera column was added with no rule of its own and inherited the plain
 * browser checkbox, so the pair read as two different kinds of control sitting
 * next to each other. A third permission should be a name in these selectors,
 * not a third copy.
 *
 * Unticked is the resting state for everyone, so it must not look like
 * something is wrong — no red, no warning. */
.player-admin-table th:nth-child(4),
.player-admin-table th:nth-child(5),
.player-admin-voice,
.player-admin-mocap { text-align: center; }
.player-admin-voice input,
.player-admin-mocap input {
  width: 15px; height: 15px; cursor: pointer; accent-color: #9ae04f;
}
.player-admin-voice input:disabled,
.player-admin-mocap input:disabled { cursor: progress; opacity: 0.5; }

.player-mute {
  margin-left: 6px;
  border: 1px solid var(--panel-line);
  border-radius: 999px;
  padding: 2px 8px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.player-mute:hover { border-color: var(--text-dim); color: var(--text); }
.player-mute[data-muted="1"] { border-color: var(--danger); color: var(--danger); }
.player-role[data-role="admin"] {
  color: #ffabc0;
  border-color: rgba(255, 105, 145, 0.58);
  background: rgba(255, 82, 128, 0.13);
}

kbd {
  display: inline-block;
  font: inherit; font-size: 11px; font-weight: 700;
  background: var(--field);
  border: 1px solid var(--panel-line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  margin-right: 3px;
  color: var(--text);
  white-space: nowrap;
}

.row-toggle {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 14px; padding: 5px 0; cursor: pointer;
}
.row-toggle input { width: 17px; height: 17px; accent-color: var(--accent-dark); cursor: pointer; }
/* A hint under its label, not welded to it: the subtitles and analytics rows
 * both carry one. */
.row-toggle span small { display: block; color: var(--text-dim); font-size: 12px; }

.row-range {
  display: grid; grid-template-columns: 1fr 150px 42px;
  align-items: center; gap: 10px;
  font-size: 14px; padding: 5px 0;
}
.row-range input { width: 100%; accent-color: var(--accent-dark); cursor: pointer; }
.row-range b { font-weight: 600; font-size: 12px; color: var(--text-dim); text-align: right; font-variant-numeric: tabular-nums; }

/* Player profile editor: the compact preview and controls share the ordinary
   settings sheet without making the sound group feel like a second dialog. */
.player-settings-layout {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr);
  align-items: center;
  gap: 14px;
  padding-top: 3px;
}
.player-settings-preview {
  width: 132px; height: 190px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 11px;
  background: radial-gradient(ellipse at 50% 62%, rgba(160, 214, 108, 0.2), transparent 56%);
}
#player-settings-canvas { display: block; width: 132px; height: 190px; }
.player-settings-form { display: grid; gap: 9px; min-width: 0; }
.player-settings-field {
  display: grid; gap: 5px;
  color: var(--text-dim);
  font-size: 11px; font-weight: 700;
}
.player-settings-field input {
  width: 100%; min-width: 0;
  border: 1px solid var(--panel-line);
  border-radius: 8px;
  outline: none;
  background: var(--field);
  color: var(--text);
  font: inherit; font-size: 13px; font-weight: 400;
  padding: 8px 9px;
}
.player-settings-field input:focus { border-color: var(--accent-dark); }
#player-settings-swatches { display: grid; gap: 10px; }
.player-settings-form .swatch-row {
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
}
.player-settings-form .swatch-row > span {
  width: auto;
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 600;
}
.player-settings-form .swatch-row .opts { width: 100%; gap: 5px; }
.player-settings-form .sw { width: 19px; height: 19px; border-radius: 5px; }
.player-settings-save { margin-top: 2px; padding: 9px; font-size: 13px; }
.player-settings-message {
  min-height: 17px;
  color: var(--text-dim);
  font-size: 11px; line-height: 1.4;
  text-align: center;
}
.player-settings-message[data-kind="success"] { color: var(--accent); }
.player-settings-message[data-kind="error"] { color: #ffabc0; }

.keys { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; font-size: 13px; }
.keys dt { color: var(--text); font-weight: 600; white-space: nowrap; }
.keys dd { color: var(--text-dim); }

/* ----------------------------------------------------------------- chat */

/* A neighbour's health, under their name. Narrower than the label so it reads
   as an attachment to it rather than as a second widget. */
.phealth {
  width: 52px; height: 4px; margin: 3px auto 0;
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.55);
  overflow: hidden;
}
.phealth i {
  display: block; height: 100%;
  background: var(--accent);
  transition: width 0.18s ease;
}
.phealth i[data-state="hurt"] { background: #ffd24a; }
.phealth i[data-state="low"] { background: var(--danger); }

/* ----------------------------------------------------- keyboard focus */

/* THE BROWSER'S OWN FOCUS RING DOES NOT BELONG IN THIS WORLD. It is white on
   blue, it is drawn in whatever the platform feels like, and against a sunlit
   village it reads as a rendering fault rather than as a cursor. Every control
   that can take focus says so in the game's own accent instead — the same
   green the tour draws its frames in and the same green the chat box turns
   when you type in it.
       
   :focus-visible, NOT :focus. A ring that appeared on every mouse click would
   be worse than the one being replaced: the villager clicks these buttons
   constantly and would leave a green box behind on each of them. The browser
   shows :focus-visible only when focus arrived by keyboard, which is the case
   the ring exists for.
       
   THE OFFSET IS NEGATIVE INSIDE #hud-bar, and that is the actual bug the owner
   photographed. The bar has overflow: hidden so its buttons sit flush in one
   rounded strip, and an outline drawn OUTSIDE a button is clipped by that —
   which is exactly why the default ring showed up on the left and right of a
   button and nowhere along the top and bottom. Drawn inset, it survives the
   clip and goes all the way round. */
:where(button, [href], [tabindex]:not([tabindex="-1"]), summary):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 8px;
}

/* Inside the strip: inset, so the clip cannot eat half of it. */
#hud-bar :focus-visible {
  outline-offset: -3px;
  border-radius: 9px;
}

/* A weapon slot is a transparent column holding a tile and a caption, so a ring
   around the BUTTON would box in the number as well and sit oddly wide. It
   belongs on the tile, which is the part that looks like a control. */
.slot:focus-visible { outline: none; }
.slot:focus-visible .slot-tile {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Text fields already answer focus by turning their border accent-green, which
   is the behaviour this ring was modelled on; a second indicator on top would
   be saying the same thing twice. */

/* ------------------------------------------------------- combat readouts */

/* Opposite corners on purpose. What is in your hands is something you change;
   how much of you is left is something you watch. Putting them in one strip
   would make the second one an afterthought of the first. */
/* THREE SEPARATE BLOCKS, not one panel with three things on it.
   A shared tray reads as a single control that happens to be subdivided; three
   standalone tiles read as three choices, which is what they are. It also
   leaves the key numbers somewhere honest to live — outside the tiles, under
   them, where they label rather than decorate. */
#loadout {
  position: fixed; top: 14px; left: 16px;
  display: flex; gap: 8px;
}

.slot {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  background: none; border: none; padding: 0;
  cursor: pointer;
}

/* LIGHT ENOUGH TO SIT ON GRASS. The tray shade this started from is meant for
   a panel at the bottom of a dark HUD; three small dark squares against a
   bright green field read as holes punched in the picture, which is exactly
   what the owner saw. Halved the opacity and lifted the tint: the tile still
   separates the icon from whatever is behind it, without shouting. */
.slot-tile {
  display: flex; align-items: center; justify-content: center;
  width: 54px; height: 54px;
  background: rgba(46, 66, 42, 0.42);
  border: 2px solid rgba(255, 255, 255, 0.22);
  border-radius: 11px;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.22);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  transition: background 0.12s ease, border-color 0.12s ease, transform 0.12s ease;
}
.slot:hover .slot-tile { background: rgba(255, 255, 255, 0.16); }

.slot-art {
  width: 46px; height: 46px; display: block;
  /* Barely muted, not dimmed. The unchosen ones only have to recede a little;
     the border, the tint and the lift below are what actually say which is
     live, and leaning on darkness for it made the whole row heavy. */
  filter: brightness(0.86) saturate(0.88);
  transition: filter 0.12s ease;
  -webkit-user-drag: none; user-select: none;
}

/* THE LIVE SLOT IS SAID THREE WAYS AT ONCE, because one was not enough to read
   at a glance: an accent border, a lit tile, and the icon coming up to full
   colour while its neighbours stay dimmed. The contrast between chosen and
   unchosen does more work than anything done to the chosen one alone. */
.slot.is-on .slot-tile {
  background: rgba(182, 232, 106, 0.30);
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: 0 0 12px rgba(182, 232, 106, 0.35), 0 3px 10px rgba(0, 0, 0, 0.25);
}
.slot.is-on .slot-art { filter: none; }

/* The key that picks it, centred under its own tile and on a plate of its own.
   Outside the tile because it captions the thing rather than being part of the
   picture of it — and ON something, because bare digits over a sunlit field
   are legible for exactly as long as the field stays one colour. */
.slot-key {
  min-width: 17px; padding: 2px 5px;
  font-size: 11px; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: rgba(24, 38, 22, 0.66);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 5px;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.slot.is-on .slot-key {
  background: var(--accent); color: #16240f; border-color: transparent;
}

/* A DIAL, not a bar — the same shape language as the weapon tiles opposite it,
   so the two top corners read as one interface rather than two. The ring IS
   the reading: full circle at full health, half a circle at half, which is a
   quantity you can take in without reading anything. The number under it says
   the same thing exactly, for when you want the digits. */
#health {
  position: fixed; top: 14px; right: 16px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
}

.hp-dial {
  position: relative;
  /* Never shrink. It is a circle, and a circle that a flex parent has squashed
   * to 54×30 is a pill with a heart in it — which is exactly what happened the
   * first time this was laid out inside a row. */
  flex: none;
  width: 54px; height: 54px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(46, 66, 42, 0.42);
  border-radius: 50%;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.22);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.hp-ring {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  /* Starts at twelve o'clock and runs clockwise, which is where every dial a
     person has ever read starts. SVG arcs begin at three o'clock. */
  transform: rotate(-90deg);
  overflow: visible;
}
.hp-track {
  fill: none; stroke: rgba(255, 255, 255, 0.20); stroke-width: 4;
}
.hp-arc {
  fill: none; stroke: var(--accent); stroke-width: 4; stroke-linecap: round;
  /* The dash pattern is one full circumference, so the offset alone draws the
     fraction. Both numbers are written from JS off the circle's own r, rather
     than hard-coded here: a radius changed in the markup would otherwise leave
     a ring that quietly stops at ninety-something per cent. */
  transition: stroke-dashoffset 0.22s ease, stroke 0.22s ease;
}

.hp-heart {
  width: 30px; height: 30px; display: block;
  position: relative;
  -webkit-user-drag: none; user-select: none;
}

#health-value {
  min-width: 17px; padding: 2px 5px;
  font-size: 11px; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: rgba(24, 38, 22, 0.66);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 5px;
  transition: background 0.12s ease, color 0.12s ease;
}

/* Hurt and nearly gone are colour changes rather than a second widget: the
   ring already says how much, and this says how worried to be. */
#health[data-state="hurt"] .hp-arc { stroke: #ffd24a; }
#health[data-state="low"] .hp-arc { stroke: var(--danger); }
#health[data-state="low"] .hp-heart { animation: hp-beat 0.9s ease-in-out infinite; }
@keyframes hp-beat {
  0%, 100% { transform: scale(1); }
  18% { transform: scale(1.16); }
  36% { transform: scale(1); }
}

/* Taking a hit flashes the dial, because the ring moving by a hundredth is not
   something anybody notices while being chased. */
#health.is-hurt .hp-dial { animation: health-hit 0.32s ease; }
@keyframes health-hit {
  0% { box-shadow: 0 0 0 5px rgba(255, 138, 106, 0.75); }
  100% { box-shadow: 0 3px 10px rgba(0, 0, 0, 0.22); }
}
/* Newly arrived and briefly untouchable — says why a hit did nothing without
   needing a second message to explain it. */
#health.is-safe .hp-dial { box-shadow: 0 0 0 2px rgba(182, 232, 106, 0.85); }

#chat {
  position: fixed; left: 16px; bottom: 14px;
  width: min(330px, calc(100vw - 32px));
  display: flex; flex-direction: column; gap: 6px;
}

#chat-log {
  max-height: min(310px, calc(100vh - 105px)); overflow-y: auto;
  display: flex; flex-direction: column; gap: 3px;
  padding: 8px 10px;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 10px;
  scrollbar-width: thin;
  scrollbar-color: rgba(198, 214, 172, 0.45) transparent;
}
#chat-log:empty { display: none; }
#chat-log::-webkit-scrollbar { width: 6px; }
#chat-log::-webkit-scrollbar-track { background: transparent; }
#chat-log::-webkit-scrollbar-thumb { background: rgba(198, 214, 172, 0.42); border-radius: 999px; }

.chat-line { font-size: 13px; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; }
.chat-name { font-weight: 700; margin-right: 6px; color: var(--accent); }
.chat-line.me .chat-name { color: #ffd24a; }

#chat-form { position: relative; }

#chat-input {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 10px;
  color: var(--text);
  font-size: 14px;
  padding: 9px 76px 9px 12px;
  outline: none;
}
#chat-input:focus { border-color: var(--accent-dark); }
#chat-input::placeholder { color: var(--text-dim); }

#chat-history-toggle,
#chat-sound-toggle {
  position: absolute; top: 50%; right: 4px; transform: translateY(-50%);
  display: grid; place-items: center;
  width: 32px; height: 30px;
  border: 0; border-radius: 7px;
  background: transparent; color: var(--text-dim);
  cursor: pointer;
  transition: color 0.12s ease, background 0.12s ease;
}
#chat-history-toggle { right: 36px; }
#chat-history-toggle:hover,
#chat-history-toggle:focus-visible,
#chat-sound-toggle:hover,
#chat-sound-toggle:focus-visible { color: var(--text); background: rgba(255, 255, 255, 0.09); outline: none; }
#chat-history-toggle[data-hidden="1"] { color: #ffabc0; }
#chat-sound-toggle[data-muted="1"] { color: #ffabc0; }
.chat-history-icon { width: 18px; height: 18px; }
.chat-history-icon .chat-history-bubble,
.chat-history-icon .chat-history-lines,
.chat-history-icon .chat-history-slash {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.chat-history-icon .chat-history-slash { display: none; stroke-width: 2.2; }
#chat-history-toggle[data-hidden="1"] .chat-history-slash { display: block; }
.chat-sound-icon { width: 17px; height: 17px; }
.chat-sound-icon .chat-sound-note { fill: currentColor; }
.chat-sound-icon .chat-sound-slash {
  display: none; fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round;
}
#chat-sound-toggle[data-muted="1"] .chat-sound-slash { display: block; }

/* ----------------------------------------------------------- join modal */

#join-modal {
  position: fixed; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(18, 36, 16, 0.48);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: opacity 0.4s ease;
  z-index: 10;
}
#join-modal.gone { opacity: 0; pointer-events: none; }

.card {
  /* The language switch is absolutely positioned in the card's top-right
     corner, so the card has to be its containing block. */
  position: relative;
  width: min(640px, calc(100vw - 28px));
  max-height: calc(100vh - 28px);
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--panel-line);
  border-radius: 16px;
  padding: 26px 28px;
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55);
}

.card h1 {
  font-size: 44px;
  line-height: 1.05;
  letter-spacing: 0.005em;
  text-align: center;
}
.auth-brand {
  display: flex; align-items: center; justify-content: center; gap: 13px;
}
/* The mark stands on its own — no cream plate behind it. It is already three
   solid shapes in the game's palette, and a rounded rectangle around them just
   turns it into an app-store tile.

   THE NAME IS PART OF THE PICTURE NOW, so this is one wide image rather than a
   mark beside a heading: the wordmark has its own letterforms and its own
   green cube over the i, and a system-font "Isomania" next to the cottage was
   only ever standing in for it until one existed.

   Sized in `min()` against the viewport because it is 3.55:1 — the widest
   thing on the card by some way — and on a 320 px phone a fixed 272 would put
   it through the padding. `height: auto` so the ratio is the file's, never
   this stylesheet's guess at it. */
.brand-logo { width: min(272px, 72vw); height: auto; display: block; }
/* The wordmark on its own. NOTHING USES IT TODAY and it is kept anyway: the
   owner drew three files and named the job of each, and this is the one for
   places that want the name without the cottage. Same standing as the unused
   helpers in `lib/email.js` — a spare with a stated purpose, not a leftover. */
.brand-word { width: min(150px, 42vw); height: auto; display: block; }
.card .sub {
  color: var(--text-dim);
  font-size: 15px;
  line-height: 1.5;
  text-align: center;
  margin: 10px auto 22px;
  max-width: 46ch;
}

.auth-step { width: 100%; }
.auth-email-step { max-width: 420px; margin: 0 auto; }
.auth-email-step form { display: grid; gap: 11px; }
.auth-turnstile {
  width: 100%; min-height: 65px;
  display: flex; align-items: center; justify-content: center;
}
.auth-turnstile > div,
.auth-turnstile iframe { width: 100% !important; max-width: 100%; }
.auth-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
#auth-email,
.auth-code-input,
.join-form input {
  background: var(--field);
  border: 1px solid var(--panel-line);
  border-radius: 9px;
  color: var(--text);
  outline: none;
}
#auth-email {
  width: 100%;
  padding: 12px 14px;
  font-size: 15px;
  text-align: center;
}
#auth-email::placeholder { color: rgba(198, 214, 172, 0.68); }
#auth-email:focus,
.auth-code-input:focus,
.join-form input:focus { border-color: var(--accent-dark); }

.auth-primary {
  width: 100%;
  background: var(--accent-dark);
  color: #17300f;
  font-size: 16px; font-weight: 700;
  border: none; border-radius: 10px;
  padding: 12px;
  cursor: pointer;
  transition: background 0.15s ease;
}
.auth-primary:hover { background: var(--accent); }
.auth-primary:disabled { opacity: 0.6; cursor: default; }

.auth-google { margin-top: 12px; }
.auth-separator {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 11px; color: var(--text-dim);
  font-size: 11px; text-transform: uppercase;
}
.auth-separator::before,
.auth-separator::after { content: ''; flex: 1; height: 1px; background: var(--panel-line); }
.auth-google-button {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; min-height: 44px; padding: 10px 14px;
  border: 1px solid var(--panel-line); border-radius: 10px;
  background: rgba(255, 255, 255, 0.92); color: #253522;
  font-size: 14px; font-weight: 750; text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.auth-google-button:hover { background: #fff; border-color: rgba(255, 255, 255, 0.95); }
.auth-google-button svg { flex: 0 0 20px; width: 20px; height: 20px; }

.auth-step-title {
  margin: 0 0 5px;
  color: var(--text);
  font-size: 18px; font-weight: 800;
  text-align: center;
}
.auth-step-copy {
  margin: 0 auto 16px;
  color: var(--text-dim);
  font-size: 13px; line-height: 1.5;
  text-align: center;
}
.auth-step-copy strong { color: var(--text); overflow-wrap: anywhere; }
.auth-code-step { max-width: 430px; margin: 0 auto; }
#auth-code-form { display: grid; gap: 14px; }
/* `min-width: 0` IS WHAT MAKES THE SHRINKING BELOW HAPPEN AT ALL. This row is
   a grid item of `#auth-code-form`, and a grid item's default `min-width: auto`
   is its min-content width — six boxes plus five gaps — so the track simply
   grew to 323px and the flex line was never short of space. The inputs then
   never shrank and the row was drawn past the card, which has `overflow-y:
   auto` and therefore clips horizontally: at 360px the sixth box was cut in
   half, at 320px it was gone. Measured in Chrome, not reasoned about. Six
   other rules in this file carry the same declaration for the same reason. */
.auth-code-inputs { display: flex; justify-content: center; gap: 10px; min-width: 0; }
.auth-code-input {
  /* SHRINKABLE, and that is what keeps six of these on a phone. Six boxes plus
     five gaps at a fixed 56 px is 386 px, and the card gives 276 px inside on
     a 360 px screen — at four boxes the fixed size fitted, at six it does not.
     `flex: 0 1` lets them give way evenly, but only once the row above is
     allowed to be narrower than its contents — see the note there. */
  flex: 0 1 56px;
  min-width: 0;
  width: 56px; height: 62px;
  padding: 0;
  font-size: 28px; font-weight: 800;
  line-height: 62px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  caret-color: var(--accent);
}
.auth-code-inputs.is-error .auth-code-input,
.auth-code-input[aria-invalid="true"] {
  border-color: #ff6f91;
  background: rgba(255, 82, 128, 0.1);
  box-shadow: 0 0 0 2px rgba(255, 82, 128, 0.12);
}
.auth-code-actions {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 8px 18px;
  margin-top: 6px;
}
.auth-link {
  border: 0; background: none; color: var(--text-dim);
  font: inherit; font-size: 12px; font-weight: 700;
  padding: 5px 0; cursor: pointer;
}
.auth-link:hover { color: var(--accent); }
.auth-link:disabled { opacity: 0.5; cursor: default; }
.auth-error {
  min-height: 18px;
  margin-top: 7px;
  color: #ffabc0;
  font-size: 13px; line-height: 1.4;
  text-align: center;
}
.auth-profile-step > .auth-step-title { margin-bottom: 14px; }

.join-row { display: flex; gap: 22px; }

.preview-wrap {
  flex: 0 0 220px;
  background: radial-gradient(ellipse at 50% 62%, rgba(160, 214, 108, 0.18), transparent 46%);
  border-radius: 12px;
  /* Centre, not flex-end: bottom-aligning the canvas is half of why the
     figure looked pushed out of its box. */
  display: flex; align-items: center; justify-content: center;
}
#preview-canvas { display: block; width: 220px; height: 300px; }

.join-form { flex: 1; display: flex; flex-direction: column; gap: 12px; min-width: 0; }

.join-form label { font-size: 13px; color: var(--text-dim); display: flex; flex-direction: column; gap: 5px; }
.join-form input { font-size: 15px; padding: 9px 12px; }

.swatch-row { display: flex; align-items: center; gap: 10px; }
.swatch-row > span { font-size: 12px; color: var(--text-dim); width: 62px; flex-shrink: 0; }
.swatch-row .opts { display: flex; flex-wrap: wrap; gap: 6px; }

.sw {
  width: 22px; height: 22px;
  border-radius: 6px;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
}
.sw.on { border-color: #fff; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.25); }

#join-btn { margin-top: 6px; }

/* --------------------------------------------------------- context menu */

#ctx-menu {
  position: fixed;
  z-index: 30;
  min-width: 170px;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 10px;
  padding: 5px;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
#ctx-menu button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: 7px;
  color: var(--text);
  font-size: 14px;
  padding: 8px 12px;
  cursor: pointer;
}
#ctx-menu button:hover { background: rgba(255, 255, 255, 0.08); }
#ctx-menu .ctx-hint {
  padding: 4px 12px 6px;
  font-size: 11px;
  color: var(--text-dim);
}

/* --------------------------------------------------------- update prompt */

#resume-modal,
#update-modal {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center;
  background: rgba(18, 36, 16, 0.5);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
}
.update-sheet { width: min(400px, calc(100vw - 28px)); padding: 22px 24px; text-align: center; }
.update-sheet h2 { font-size: 20px; margin-bottom: 8px; }
.update-sheet p { font-size: 14px; line-height: 1.5; color: var(--text-dim); }
.update-sheet b { color: var(--accent); font-variant-numeric: tabular-nums; }
/* The consent banner: bottom sheet over everything, and the two buttons are
 * DELIBERATELY the same class, size and weight — an accept that outdresses
 * its decline is not consent. */
#consent { position: fixed; left: 0; right: 0; bottom: 0; z-index: 60; display: flex; justify-content: center; padding: 14px; }
.consent-sheet { max-width: 720px; background: var(--panel, rgba(24, 34, 20, 0.96)); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 14px; padding: 14px 18px; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); }
.consent-sheet p { margin: 0 0 10px; font-size: 14px; line-height: 1.5; }
.consent-sheet a { color: var(--accent); }
.consent-actions { display: flex; gap: 10px; }
.consent-actions button { flex: 1 1 0; padding: 9px 14px; border-radius: 9px; border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.10); color: inherit; font: inherit; font-weight: 700; cursor: pointer; }
.consent-actions button:hover { background: rgba(255, 255, 255, 0.18); }

.ctl-legal { display: flex; gap: 16px; margin-top: 10px; flex-wrap: wrap; }
.ctl-legal a { color: var(--text-dim); font-size: 13px; }
.ctl-legal a:hover { color: var(--accent); }

.update-blog { margin-top: 10px; }
/* The link row a sheet carries under its buttons: the sign-in card's
 * separator and door row, re-homed. */
.sheet .sheet-or { margin: 18px 0 0; }
.sheet .auth-links { margin-top: 10px; }
.update-blog a { color: var(--accent); font-size: 14px; }

.update-actions { display: flex; gap: 10px; margin-top: 18px; }
.update-actions button {
  flex: 1; border: none; border-radius: 10px; cursor: pointer;
  font-size: 15px; font-weight: 700; padding: 11px;
}
#update-later { background: var(--field); color: var(--text-dim); }
#update-later:hover { color: var(--text); }
#update-now { background: var(--accent-dark); color: #17300f; }
#update-now:hover { background: var(--accent); }

/* The postponed-update badge quietly pulses so it is noticeable but not loud. */
#update-btn { color: var(--accent); animation: update-pulse 2.2s ease-in-out infinite; }
.ico-refresh path { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
@keyframes update-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

/* ---------------------------------------------------------------- toast */

#toast {
  position: fixed; top: 16px; left: 50%;
  transform: translateX(-50%);
  background: var(--panel);
  border: 1px solid var(--panel-line);
  color: var(--text);
  font-size: 14px;
  padding: 9px 16px;
  border-radius: 10px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
  z-index: 20;
  max-width: calc(100vw - 40px);
}

/* --------------------------------------------------------------- mobile */

/* Below tablet width the eight data columns cannot remain legible. Desktop
   keeps the table fully visible; compact screens get a deliberate local
   fallback instead of squeezing labels and dates into unreadable slivers. */
@media (max-width: 900px) {
  .player-admin-table-wrap,
  .chat-admin-table-wrap { overflow-x: auto; }
  .player-admin-table { min-width: 900px; }
  .chat-admin-table { min-width: 880px; }
}

@media (max-width: 560px) {
  .join-row { flex-direction: column; align-items: center; }
  /* No `.auth-brand img` override here. One lived on this line from the era
     when .auth-brand held a SQUARE favicon mark beside a text h1 — 46 × 46
     with a FIXED height. The mark since became the wide wordmark PNG
     (272 × 77), and on any phone the stale rule crushed it into that square:
     specificity (.auth-brand img beats .brand-logo) plus an explicit height
     killed `height: auto`, and nothing on desktop ever showed it. Narrow
     screens are already `.brand-logo`'s own job: min(272px, 72vw). */
  .card h1 { font-size: 36px; }
  .auth-code-inputs { gap: 7px; }
  .auth-code-input { flex-basis: 48px; width: 48px; height: 56px; line-height: 56px; }
  .player-settings-layout { grid-template-columns: 1fr; justify-items: center; }
  .player-settings-form { width: 100%; }
  #chat-log { max-height: 120px; }
  /* The bar stays — it is the only way to reach settings — but tightens up. */
  .hud-group { padding: 0 10px; height: 38px; font-size: 12px; }
  .hud-btn { padding: 0 11px; }
  .row-range { grid-template-columns: 1fr 96px 38px; }
  .smtp-admin-head { align-items: stretch; flex-direction: column; }
  .smtp-admin-head .admin-action { align-self: flex-start; }
  .smtp-form-grid { grid-template-columns: 1fr; }
  .smtp-field-wide { grid-column: auto; }
  .cloudflare-form-grid { grid-template-columns: 1fr; }
  .cloudflare-form-actions { align-items: stretch; flex-direction: column-reverse; }
  .cloudflare-form-actions .admin-action { width: 100%; }
  .google-form-grid { grid-template-columns: 1fr; }
  .google-field-wide { grid-column: auto; }
  .google-form-actions { align-items: stretch; flex-direction: column-reverse; }
  .google-form-actions .admin-action { width: 100%; }
  .telegram-form-grid { grid-template-columns: 1fr; }
  .telegram-field-wide { grid-column: auto; }
  .telegram-form-actions { align-items: stretch; flex-direction: column-reverse; }
  .telegram-form-actions .admin-action { width: 100%; }
  .player-admin-form-grid { grid-template-columns: 1fr; }
  .player-admin-name,
  .player-admin-email { grid-column: auto; }
}

@media (max-width: 760px) {
  .smtp-admin-layout,
  .smtp-admin-layout.is-editing { grid-template-columns: 1fr; }
  .smtp-admin-left,
  .smtp-admin-layout.is-editing .smtp-admin-left { display: block; }
  .smtp-admin-left .fgroup:first-child { margin-bottom: 14px; }
  .player-admin-head,
  .chat-admin-head,
  .cloudflare-admin-head,
  .google-admin-head,
  .telegram-admin-head { flex-direction: column; }
  .chat-admin-tools { width: 100%; justify-content: flex-start; }
  .player-admin-tools { width: 100%; justify-content: space-between; }
  .player-admin-tools input { width: min(100%, 280px); }
  .player-admin-form-grid { grid-template-columns: 1fr 1fr; }
  .player-admin-name,
  .player-admin-email { grid-column: 1 / -1; }
}

@media (max-width: 560px) {
  .player-admin-form-grid { grid-template-columns: 1fr; }
  .player-admin-name,
  .player-admin-email { grid-column: auto; }
}

/* -------------------------------------------------------- language switch */

/* A pill with a sliding thumb, not two loose buttons: one control with a
   position reads as a switch, two independently-coloured buttons read as two
   buttons that happen to sit together.

   Labels are words, not flags — a flag is a country, and neither of these
   languages belongs to one. Both stay in the Latin alphabet so the switch is
   legible to somebody who cannot read the interface they are looking at,
   which is the entire point of it. */
.lang-switch {
  position: relative;
  display: inline-flex;
  padding: 3px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.26);
  border: 1px solid var(--panel-line);
}
.lang-thumb {
  position: absolute;
  top: 3px; bottom: 3px; left: 3px;
  width: calc(50% - 3px);
  border-radius: 999px;
  background: var(--accent);
  transition: transform 0.18s ease;
}
.lang-switch[data-lang="en"] .lang-thumb { transform: translateX(100%); }
@media (prefers-reduced-motion: reduce) { .lang-thumb { transition: none; } }

.lang-opt {
  position: relative;              /* above the thumb */
  min-width: 38px;
  background: none; border: none;
  color: var(--text-dim);
  font: inherit; font-size: 11px; font-weight: 800; letter-spacing: 0.06em;
  padding: 4px 8px;
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.14s ease;
}
.lang-opt:hover { color: var(--text); }
/* The selected half sits on the accent thumb, so it takes the dark ink. */
.lang-switch[data-lang="uk"] .lang-opt[data-lang="uk"],
.lang-switch[data-lang="en"] .lang-opt[data-lang="en"] { color: #17300f; }

/* Under the sign-in button rather than over the logo: the last thing on the
   card, so it reads as a setting and not as a step of signing in. */
/* Only 8px of its own, because the gap above it is mostly not its doing:
   `.auth-error` reserves 25px whether or not there is an error, so that a
   message appearing does not shove the card around. Removing that reservation
   to close the gap would trade a tidy space for a jumping layout. */
.auth-lang { display: flex; justify-content: center; margin-top: 8px; }

/* The card's footer links, under the language pill. A row, not a line: the
 * owner has more external links coming, and they should land here without
 * relaying out the card. */
.auth-links { display: flex; justify-content: center; gap: 18px; margin-top: 12px; }
.auth-links a { color: var(--text-dim); font-size: 13px; text-decoration: underline; text-underline-offset: 3px; }
.auth-links a:hover { color: var(--accent); }
.auth-lang .lang-opt { font-size: 12px; min-width: 44px; padding: 5px 10px; }

/* In the bar the language is a word, not a glyph, so it needs the type
   treatment the icons do not — but the same box, so the row stays even. */
.hud-lang {
  font: inherit; font-size: 12px; font-weight: 800; letter-spacing: 0.06em;
  min-width: 46px;
}

/* --- admin: world tab ------------------------------------------------- */

/* A stepper rather than a number input. The range is 0..8 and the whole
   interaction is "one more" or "one fewer", which a spinner does in one click
   and a text field does in select-type-tab. It also cannot be handed a value
   the server will reject. */
.world-counter {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 0 14px;
}

.world-counter .world-step {
  width: 38px;
  padding: 0;
  font-size: 20px;
  line-height: 34px;
  font-weight: 600;
}

.world-count {
  min-width: 52px;
  text-align: center;
  font-size: 22px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.world-counter small { color: var(--text-dim); font-size: 12px; }

/* --- first-run tour ---------------------------------------------------- */

/* Above the HUD (20) and below the modals (25/30/40): the tour explains the
   bar, so it has to sit over it, but a settings sheet opened during the tour
   must still come out on top. */
#tour-layer {
  position: fixed;
  inset: 0;
  z-index: 22;
  /* NOT a backdrop. The layer only exists to hold the bubble; the game stays
     clickable underneath, because the tour is about the world the player is
     already standing in. */
  pointer-events: none;
}

.tour-bubble {
  position: absolute;
  width: min(360px, calc(100vw - 24px));
  pointer-events: auto;
  background: var(--panel);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  border: 1px solid var(--panel-line);
  border-radius: 14px;
  padding: 14px 16px 12px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.42);
  color: var(--text);
}

/* A LEADER LINE from the bubble to the frame, not a speech-bubble tail.

   A tail has to be painted in the panel's own fill, and this panel does not
   really have one: it is translucent over a blurred canvas. A second
   translucent square at the seam double-darkens where it overlaps and shows
   its own borders where it does not — it rendered as a dark diamond floating
   loose beneath the bubble. No arrangement of a second element fixes that.

   A solid line in the highlight's colour has no transparency to get wrong, and
   it reads as belonging to the frame, which is the relationship being drawn. */
.tour-link {
  position: absolute;
  width: 2px;
  margin-left: -1px;
  background: var(--accent);
  box-shadow: 0 0 10px rgba(182, 232, 106, 0.5);
  border-radius: 1px;
  pointer-events: none;
}

.tour-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.tour-head h3 { margin: 0; font-size: 15px; font-weight: 700; }
.tour-count { font-size: 11px; color: var(--text-dim); font-variant-numeric: tabular-nums; }

.tour-bubble p { margin: 0 0 7px; font-size: 13px; line-height: 1.45; color: var(--text-dim); }
/* A key name and the punctuation after it. There is no space between them to
   collapse, so only a nowrap run removes the break opportunity that an
   inline-block leaves behind — otherwise a sentence ending in a key drops its
   full stop onto the next line on its own. */
.tour-nb { white-space: nowrap; }
.tour-bubble kbd {
  display: inline-block;
  padding: 0 5px;
  border: 1px solid var(--panel-line);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: rgba(0, 0, 0, 0.22);
  color: var(--text);
  font: 600 11px/18px ui-monospace, SFMono-Regular, Menlo, monospace;
}

.tour-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 10px;
}
.tour-actions button {
  border-radius: 9px;
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid var(--panel-line);
}
.tour-skip { background: transparent; color: var(--text-dim); }
.tour-skip:hover { color: var(--text); }
.tour-next { background: var(--accent-dark); border-color: var(--accent-dark); color: #10240c; }
.tour-next:hover { background: var(--accent); border-color: var(--accent); }

/* ONE frame round the whole group the step is about, drawn as its own box
   rather than as an outline on each control.

   Ringing them individually turned a row of square buttons into a row of green
   ellipses with the dividers caught between them — it read as damage rather
   than as emphasis. A single rounded rectangle also glides between steps
   instead of blinking from one place to another. */
.tour-frame {
  position: absolute;
  border: 2px solid var(--accent);
  border-radius: 14px;
  box-shadow: 0 0 0 4px rgba(182, 232, 106, 0.14), 0 0 22px rgba(182, 232, 106, 0.25);
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .tour-frame { transition: left 180ms ease, top 180ms ease, width 180ms ease, height 180ms ease; }
}

/* The world recedes while the tour runs; the interface does not.

   Applied to the CANVAS rather than as a frosted sheet over the page, and that
   is the whole trick: a sheet would blur the HUD too, and the HUD is the
   subject. Everything the tour points at stays perfectly sharp. */
#game.is-tour-blur {
  filter: blur(5px) brightness(0.72) saturate(0.9);
}
@media (prefers-reduced-motion: no-preference) {
  #game { transition: filter 220ms ease; }
}

@media (prefers-reduced-motion: no-preference) {
  .tour-bubble { animation: tour-in 160ms ease-out; }
  @keyframes tour-in {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: none; }
  }
}

.ctl-replay { display: flex; align-items: center; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
.ctl-replay small { color: var(--text-dim); font-size: 12px; }

/* The development-blog row under the replay button: a link, not a button —
 * it leaves the game (in a new tab), and controls that leave should not
 * dress like controls that act here. */
.ctl-blog { display: flex; align-items: center; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
.ctl-blog a { color: var(--accent); font-weight: 600; text-decoration: none; }
.ctl-blog a:hover { text-decoration: underline; }
.ctl-blog small { color: var(--text-dim); font-size: 12px; }

/* --- aiming reticle ----------------------------------------------------- */

#crosshair {
  position: fixed;
  left: 0; top: 0;
  /* translate() moves it; the -50% centres the glyph on the point it names. */
  margin: -13px 0 0 -13px;
  pointer-events: none;
  z-index: 15;              /* over the world, under the HUD and every sheet */
  will-change: transform;
}

/* Stroked, not filled — and the halo underneath is not decoration. A reticle
   in one colour disappears against whichever surface happens to match it, and
   this village has both dark roofs and pale cobbles. */
#crosshair .cross-halo,
#crosshair .cross-line { fill: none; stroke-linecap: round; }
#crosshair .cross-halo { stroke: rgba(8, 18, 6, 0.55); stroke-width: 3.4; }
#crosshair .cross-line { stroke: #eaf7d8; stroke-width: 1.5; }
#crosshair .cross-dot { fill: #eaf7d8; }

/* Something is in the way. The reticle changes colour rather than growing or
   spinning: it has to stay the same size to keep meaning the same point, and a
   colour swap is legible at 26 px where a shape change is not. */
#crosshair.is-hot .cross-line { stroke: #ff6a4a; }
#crosshair.is-hot .cross-dot { fill: #ff6a4a; }

/* The range readout, for the admin who is testing the aim.

   Placed below and to the right so it never sits ON the point the reticle
   names — a number over the crosshair would hide the one pixel that matters.
   Same halo trick as the glyph itself: this has to be legible over a dark
   roof and over pale cobbles, and a text-shadow is the cheap way to get that
   without a plate behind it. */
.cross-range {
  position: absolute;
  left: 34px; top: 14px;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: #eaf7d8;
  text-shadow: 0 0 3px rgba(8, 18, 6, 0.9), 0 1px 2px rgba(8, 18, 6, 0.8);
}
#crosshair.is-hot .cross-range { color: #ff6a4a; }

/* The reticle IS the pointer in the isometric view, so the real one goes.
   Only on the canvas: over the chat box and the panels the ordinary cursor is
   what anybody wants. */
#game.is-reticle { cursor: none; }

/* ======================================================================
   TOUCH LAYOUT

   Keyed off `html[data-touch]`, which `touch.js` writes. The media query
   lives there and only there: a second copy here would agree until somebody
   tightened one of them, and it could not see the `?touch=1` override at all.

   ONE LAYOUT FOR BOTH ORIENTATIONS, pinned to the corners and the bottom, so
   turning the phone changes nothing about where anything is — and there is
   no second set of rules to keep in step.
   ====================================================================== */

/* Present only on the other kind of device. Stated once, both ways, so a new
   control declares which world it belongs to instead of being positioned out
   of the way and hoped about. */
.touch-only { display: none !important; }
/* `:not([hidden])` because both rules are !important and this one is the more
   specific, so without it a touch-only control could never be hidden by JS. */
html[data-touch] .touch-only:not([hidden]) { display: flex !important; }
html[data-touch] .no-touch { display: none !important; }

/* ---------------------------------------------------- the left-hand column */

/* Transparent on a desktop: the two children keep their own fixed corners. */
#hud-left { display: contents; }

html[data-touch] #hud-left {
  display: flex;
  position: fixed; top: 10px; left: 10px;
  flex-direction: column; align-items: flex-start; gap: 10px;
}
html[data-touch] #loadout,
html[data-touch] #health { position: static; }
html[data-touch] #loadout { flex-direction: column; gap: 6px; }
html[data-touch] #health { align-items: center; width: 46px; }

/* Smaller, because the column is now a third of the way down a short screen —
   and WITHOUT the 1 2 3 captions, which name keys this device does not have.
   A caption that names a key nobody can press is a lie in the interface. */
html[data-touch] .slot-key { display: none; }
html[data-touch] .slot-tile { width: 46px; height: 46px; border-radius: 10px; }
html[data-touch] .slot-art { width: 38px; height: 38px; }
html[data-touch] .hp-dial { width: 46px; height: 46px; }
html[data-touch] .hp-heart { width: 25px; height: 25px; }

/* ------------------------------------------------- the top-right two buttons */

#touch-bar {
  position: fixed; top: 10px; right: 10px;
  gap: 8px;
  /* Above the full-screen chat, so the same icon closes what it opened, and
     above the reticle at 15 — which is not a dialog, so nothing else hides
     it while the menu is up. Below every sheet at 25. */
  z-index: 18;
}
#touch-bar button {
  display: grid; place-items: center;
  width: 46px; height: 46px;
  background: var(--panel);
  border: 1px solid var(--panel-line);
  border-radius: 12px;
  color: var(--text-dim);
  cursor: pointer;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
#touch-bar button[data-open="1"] {
  background: rgba(182, 232, 106, 0.26);
  border-color: var(--accent);
  color: var(--text);
}
#touch-bar .ico { width: 23px; height: 23px; }
/* Open, the chat button is the way out of what it opened. */
.ico-chatbtn .chatbtn-close { display: none; }
#chat-btn[data-open="1"] .chatbtn-bubble { display: none; }
#chat-btn[data-open="1"] .chatbtn-close { display: block; }

/* -------------------------------------------------------------- the burger */

/* The SAME bar, stood on end. Its children are already in the order the owner
   asked for once fullscreen, camera tracking and the editor are taken out —
   so there is no second menu to build, nothing to move between two parents,
   and every listener, permission gate and state attribute keeps working
   without knowing this view exists. */
html[data-touch] #hud-bar {
  top: 64px; right: 10px; bottom: auto; left: auto;
  flex-direction: column; align-items: stretch;
  min-width: 216px; max-width: min(76vw, 320px);
  max-height: calc(100dvh - 78px);
  overflow-y: auto; overflow-x: hidden;
  padding: 6px;
  border-radius: 14px;
  z-index: 17;
}
html[data-touch] #hud:not([data-menu="1"]) #hud-bar { display: none; }

/* No hairlines. They earn their place in a dense strip of unlabelled icons;
   in a short list of labelled rows they are just lines. */
html[data-touch] #hud-bar .hud-sep { display: none; }

html[data-touch] #hud-bar .hud-group {
  height: 46px;              /* the smallest target a thumb hits reliably */
  padding: 0 12px; gap: 12px;
  justify-content: flex-start;
  border-radius: 9px;
  font-size: 14px; color: var(--text);
}
html[data-touch] #hud-bar .hud-btn { padding: 0 12px; }
html[data-touch] #hud-bar .hud-btn:active { background: rgba(255, 255, 255, 0.15); }

/* THE LABEL IS THE TITLE, minus the name of the key it mentions. Nothing is
   authored twice: a row says "Мікрофон вимкнено" or "увімкнено" exactly as the
   tooltip does, because it IS the tooltip, already translated by whoever set
   it. `touch-hud.js` copies it into `data-label` and drops the "(Q)" — see the
   comment there. Read from an attribute rather than a child element, because
   the language button rewrites its own textContent. */
html[data-touch] #hud-bar .hud-btn::after {
  content: attr(data-label);
  font-size: 14px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
html[data-touch] #hud-bar .ico { width: 21px; height: 21px; flex: none; }
html[data-touch] #hud-bar #hud-lang { font-size: 13px; }
html[data-touch] #hud-bar #logout-btn { color: #ffabc0; }
html[data-touch] #hud-bar #crouch-btn[data-on="1"],
html[data-touch] #hud-bar #sit-btn[data-on="1"] {
  background: rgba(182, 232, 106, 0.22); color: var(--text);
}

/* Catches the tap that means "not this". Under the panel, over the world. */
#touch-scrim { position: fixed; inset: 0; z-index: 16; background: none; }
html[data-touch] #hud:not([data-menu="1"]) #touch-scrim { display: none !important; }

/* -------------------------------------------------- chat, over everything */

/* The bottom is given up entirely to the stick and the buttons, so the chat
   box cannot stay where it is. It becomes a full screen instead of a second
   widget: same log, same form, same two toggles, same listeners — restyled,
   not rebuilt. */
html[data-touch] #chat {
  position: fixed; inset: 0; left: 0; bottom: 0;
  width: auto;
  padding: 66px 12px calc(12px + var(--kb, 0px));
  gap: 8px;
  justify-content: flex-end;
  background: rgba(12, 22, 10, 0.90);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  z-index: 17;
}
html[data-touch] #hud:not([data-chat="1"]) #chat { display: none; }
html[data-touch] #chat-log {
  max-height: none; flex: 1 1 auto;
  background: none; border: none; padding: 0 2px;
  justify-content: flex-end;
}
html[data-touch] #chat-log:empty { display: flex; }
html[data-touch] .chat-line { font-size: 15px; line-height: 1.5; }
/* 16px is not a taste: Safari zooms the whole page in when a focused input is
   smaller than that, and a page zoomed by the keyboard does not zoom back. */
html[data-touch] #chat-input { font-size: 16px; padding: 12px 78px 12px 14px; }

/* A PHONE ON ITS SIDE IS 390 px TALL, and the menu does not fit: ten rows at
   46 px come to 468 against the 312 left under the button. Measured, so the
   compromise is chosen rather than discovered — rows come down to 38, which
   fits a plain player's eight rows exactly and leaves an admin with a
   microphone scrolling for the last two. Below the 44 px rule of thumb, and
   knowingly: that rule is about square targets, and these are full-width
   strips a thumb cannot miss sideways. */
@media (max-height: 500px) {
  html[data-touch] #hud-bar { top: 58px; padding: 4px; max-height: calc(100dvh - 66px); }
  html[data-touch] #hud-bar .hud-group { height: 38px; }
}

/* ------------------------------------------------- stick and action buttons */

/* Under the scrim (16) and both panels (17): a burger over the stick must
   swallow the tap, not sit on top of a live control. */
#touch-stick, #touch-actions { z-index: 13; }

/* THE ZONE, not the ring. A quarter of the screen, and every one of its taps
   belongs to walking — the ring inside merely shows where the stick lives
   until a thumb tells it where it actually is. `touch-action: none` because
   otherwise the first drag scrolls the page instead of moving anybody. */
#touch-stick {
  position: fixed; left: 0; bottom: 0;
  width: min(46%, 260px); height: min(46%, 300px);
  touch-action: none;
}
.stick-base {
  position: absolute;
  /* The resting place, stated here as well as in the module: the ring should
     already look right in the frame before any script has run, and a preview
     that renders no JS should show the stick where it lives. Same two numbers
     the module walks it home to. */
  left: 26px; top: calc(100% - 158px);
  width: 128px; height: 128px;
  border-radius: 50%;
  background: rgba(24, 38, 22, 0.30);
  border: 2px solid rgba(255, 255, 255, 0.20);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  display: grid; place-items: center;
  /* Fades back rather than jumping: released, the ring walks home. Held, JS
     turns the transition off so the knob does not lag the thumb. */
  transition: left 0.18s ease, top 0.18s ease, opacity 0.18s ease;
  opacity: 0.62;
}
#touch-stick[data-live="1"] .stick-base { opacity: 1; transition: none; }
.stick-knob {
  width: 56px; height: 56px;
  border-radius: 50%;
  background: rgba(182, 232, 106, 0.42);
  border: 2px solid rgba(255, 255, 255, 0.34);
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.28);
  will-change: transform;
}
#touch-stick[data-run="1"] .stick-knob {
  background: rgba(182, 232, 106, 0.78);
  border-color: var(--accent);
}

/* Holes, not a panel: only the buttons catch anything, so the gaps between
   them still belong to whatever the free screen does. */
#touch-actions {
  position: fixed; right: 0; bottom: 0;
  width: 190px; height: 120px;
  pointer-events: none;
}
#touch-actions button {
  position: absolute;
  pointer-events: auto;
  touch-action: none;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(24, 38, 22, 0.44);
  border: 2px solid rgba(255, 255, 255, 0.22);
  color: var(--text);
  cursor: pointer;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
#touch-actions button[data-down="1"] {
  background: rgba(182, 232, 106, 0.42);
  border-color: var(--accent);
  transform: scale(0.94);
}
/* An arc under the thumb rather than a row: the thumb swings, it does not
   slide. The same size for both, because neither dominates the other — 68 px
   is 17% of a 390 px screen. The corner, which is the easiest reach, goes to
   interaction: on a football pitch that is the button that gets used. */
#act-use  { right: 22px; bottom: 26px;  width: 68px; height: 68px; }
#act-jump { right: 104px; bottom: 40px; width: 68px; height: 68px; }
/* 48 of the button's 68 px, and the number is DERIVED rather than copied from
   the weapon slots: those give their art 46 of a 54 px tile, 85%, but a tile
   is square and this is a circle. The biggest square inside a 68 px circle is
   68/√2 = 48, so this fills the button exactly as fully as a slot fills its
   tile. Copying the 85% across would have run the corners past the rim. */
.act-art {
  width: 48px; height: 48px; display: block;
  -webkit-user-drag: none; user-select: none;
}

/* Driving: the two action buttons become the pedals and a third leaves the
   kart. Labelled with a glyph rather than art, because these are transient —
   they exist only while somebody is at a wheel. The `data-driving` flag on the
   bar lets the pictured jump/interact art hide under the pedal glyphs. */
#touch-actions[data-driving="1"] .act-art { display: none; }
#touch-actions[data-driving="1"] #act-jump::after { content: '▲'; font-size: 26px; }
#touch-actions[data-driving="1"] #act-use::after  { content: '▼'; font-size: 26px; }
#act-exit {
  right: 30px; bottom: 104px; width: 46px; height: 46px; font-size: 22px;
}
#act-exit[hidden] { display: none; }

/* The canvas must stop offering the browser's own gestures once fingers are
   driving the game: without this a drag scrolls the page and a double tap
   zooms it, both in the middle of a fight. */
html[data-touch] #game { touch-action: none; }

/* Race controls stay above the driving pedals, on desktop and touch. */
.race-hud { position: fixed; left: calc(16px + env(safe-area-inset-left, 0px)); top: calc(14px + env(safe-area-inset-top, 0px)); width: min(350px, calc(100vw - 32px)); padding: 12px 16px; border: 1px solid #d4e7ec66; border-radius: 12px; background: #172b39ed; color: #ffedbd; font-size: 13px; line-height: 1.45; z-index: 20; pointer-events: auto; }
.race-hud strong { display: block; margin-bottom: 4px; }
.race-hud button { padding: 9px 12px; margin: 8px 6px 0 0; border-radius: 8px; border: 1px solid #a7c7c8; background: #315769; color: #fff1ca; cursor: pointer; }
.race-hud button:disabled { opacity: .5; cursor: default; }

@media(max-height:500px) { html[data-touch] .race-hud { width: 300px; font-size: 11px; padding: 6px 10px; } }

.notice-editor{position:fixed;inset:0;z-index:1600;background:#101b19bb;display:grid;place-items:center;padding:20px}
.notice-editor[hidden]{display:none}
.notice-editor form{box-sizing:border-box;width:min(650px,100%);max-height:calc(100dvh - 40px);overflow:auto;padding:24px;background:#30472a;color:#f4eedc;border:1px solid #819768;border-radius:12px}
.notice-editor textarea{box-sizing:border-box;width:100%;resize:vertical;min-height:160px;padding:12px;background:#172b24;color:#fff4d9;border:1px solid #819768;font:inherit}
.notice-editor form>div{display:flex;justify-content:flex-end;gap:12px;margin-top:16px}
.notice-editor button{padding:10px 20px}

.notice-editor label{display:flex;align-items:center;gap:10px;margin-top:14px;cursor:pointer}
.notice-editor label[hidden]{display:none}
.notice-editor input[type="checkbox"]{width:20px;height:20px;flex-shrink:0;accent-color:var(--accent)}
.notice-editor select{min-width:0;flex:1;padding:10px;background:#172b24;color:#fff4d9;border:1px solid #819768;border-radius:6px;font:inherit}

/* Robot settings and proximity disclosure. */
.bot-settings { display:flex; flex-direction:column; gap:14px; max-width:980px; }
.bot-settings input[type="password"], .bot-settings textarea { width:100%; box-sizing:border-box; padding:14px; font:inherit; color:var(--text,#f1efd9); background:#192e26; border:1px solid #829963; border-radius:8px; }
.bot-settings textarea { resize:vertical; min-height:220px; }
.bot-settings button { align-self:flex-start; padding:12px 28px; font:inherit; }

.bot-settings + .bot-settings { margin-top:24px; }
