:root {
  --a: #3b82f6;
  --b: #ef4444;
  --bg: #0b0e14;
  --fg: #e8ecf3;
  --dim: #93a1b5;
  --panel: rgba(14, 18, 27, 0.94);
  --line: rgba(255, 255, 255, 0.1);
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--fg);
  font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  /* Chrome's pull-to-refresh fires on a downward drag at the top of the page,
     which is also how you look up. Without this the game reloads mid-match. */
  overscroll-behavior: none;
  /* A long press anywhere would otherwise start a text selection or pop the
     callout menu on iOS, both of which cancel the pointer stream underneath. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* Inputs are the one thing that must still be selectable. */
input { user-select: text; -webkit-user-select: text; }

#view { display: block; width: 100%; height: 100%; touch-action: none; }

button {
  font: inherit;
  color: var(--fg);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px 16px;
  cursor: pointer;
  transition: background .15s, border-color .15s, transform .05s;
}
button:hover { background: rgba(255, 255, 255, 0.13); }
button:active { transform: translateY(1px); }
button.primary { background: var(--a); border-color: transparent; font-weight: 600; }
button.primary:hover { background: #4f8ef7; }
button.ghost { background: none; color: var(--dim); }

input {
  font: inherit;
  color: var(--fg);
  background: rgba(0, 0, 0, .35);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px 12px;
  width: 100%;
  outline: none;
}
input:focus { border-color: var(--a); }

kbd {
  font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
  background: rgba(255, 255, 255, .1);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 4px;
  padding: 1px 5px;
  margin-right: 2px;
}

/* ---------------- server browser ---------------- */
/* ONE SCREEN, NEVER A DOCUMENT.
   A game menu does not scroll — you do not scroll a main menu in any desktop game,
   and a page that scrolls behind a fixed HUD is how you end up looking at the bottom
   half of a lobby. So this layer is exactly the viewport and clips, and the only
   thing inside it that scrolls is the room list.

   `100dvh` rather than `vh` because mobile browsers count `vh` against a viewport
   that includes the address bar they are about to hide — at fifteen rooms that is the
   difference between the last row being reachable and being under the chrome. */
#browser {
  position: fixed;
  inset: 0;
  height: 100dvh;
  display: grid;
  place-items: center;
  padding: 16px;
  overflow: hidden;
  background: radial-gradient(1200px 700px at 50% -10%, #16213a 0%, #0b0e14 60%);
  z-index: 40;
}
#browser[hidden] { display: none; }
/* A column, so the list can be told to take the leftover height and no more.
   `min-height: 0` is the load-bearing line: a flex child's default minimum is its
   content, so without it the list refuses to shrink, the panel grows past the
   viewport, and the overflow above clips the bottom rows into being unreachable. */
#browser .panel {
  width: min(520px, calc(100vw - 32px));
  max-height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.panel {
  width: min(440px, calc(100vw - 32px));
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 28px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, .6);
}
.panel h1 { margin: 0 0 4px; font-size: 30px; letter-spacing: -.02em; }
.panel .tag { margin: 0 0 22px; color: var(--dim); font-size: 14px; }
.panel label { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--dim); margin: 0 0 6px; }
.row { display: flex; gap: 8px; margin-top: 12px; }
.row button { white-space: nowrap; }
.row .primary { flex: 1; }
.or { text-align: center; color: var(--dim); font-size: 12px; margin: 16px 0 4px; }
.err { color: #fca5a5; font-size: 13px; min-height: 18px; margin: 10px 0 0; }

/* The list. A five-column grid shared by the header and every row, so the
   columns line up without either knowing the other's widths.

   The frame takes whatever height is left over and the ROWS scroll inside it — the
   column header stays put, because a header that scrolls away is a list you have to
   remember the shape of. `min-height: 0` again, and for the same reason. */
.servers {
  margin-top: 18px;
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.server-head { flex: 0 0 auto; }
#server-list {
  overflow-y: auto;
  min-height: 0;
  /* Do not hand the scroll on to whatever is behind: reaching the end of the list
     must not start moving the page, which is the artefact the fixed layer exists to
     prevent in the first place. */
  overscroll-behavior: contain;
  /* A hairline, so a list that continues past the frame looks like it does. */
  scrollbar-width: thin;
}
.server-head,
.server-row {
  display: grid;
  grid-template-columns: 1fr auto 96px 76px 84px;
  gap: 12px;
  align-items: center;
  padding: 10px 14px;
}
.server-head {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--dim);
  background: rgba(255, 255, 255, .03);
  border-bottom: 1px solid var(--line);
}
.server-row + .server-row { border-top: 1px solid var(--line); }
.server-row:hover { background: rgba(255, 255, 255, .04); }
.server-row .s-name { font-weight: 600; }
.server-row .s-players,
.server-row .s-status { color: var(--dim); font-size: 13px; }
.server-row .s-status.live { color: #86efac; }
.server-row button { padding: 7px 14px; width: 100%; }
.server-row button[disabled] { opacity: .4; cursor: not-allowed; }
.servers .empty { padding: 18px 14px; color: var(--dim); font-size: 13px; text-align: center; }

/* --- ping, as three bars ---------------------------------------------------
   The row says nothing about which region a room is in — deliberately. A player
   does not want to reason about geography, they want to know whether this room
   will play well, and that is exactly one number. Three bars is the phone-signal
   convention, so it needs no key: 3 green under 50 ms, 2 yellow to 100, 1 red past
   it. The figure sits beside them for anyone who wants the actual number.

   Thresholds are not arbitrary. `maxRewindMs` is 200, so past ~100 ms of rtt lag
   compensation is doing most of the work of making a hit register and a player
   starts having to lead their throws — which is the honest meaning of one red bar.

   The bars are `align-items: end` in a fixed-height row so they grow upward from a
   common baseline, rather than being three centred blocks of different heights. */
.s-ping { display: flex; align-items: center; gap: 7px; justify-content: flex-end; }
.s-ping .ms { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; }
.s-ping .bars { display: flex; align-items: flex-end; gap: 2px; height: 12px; }
.s-ping .bars i { width: 3px; border-radius: 1px; background: rgba(255, 255, 255, .18); }
.s-ping .bars i:nth-child(1) { height: 5px; }
.s-ping .bars i:nth-child(2) { height: 8px; }
.s-ping .bars i:nth-child(3) { height: 12px; }
/* Only the lit bars take the colour; the rest stay as the empty track above. */
.s-ping.q3 .bars i { background: #4ade80; }
.s-ping.q2 .bars i:nth-child(-n + 2) { background: #fbbf24; }
.s-ping.q1 .bars i:nth-child(1) { background: #f87171; }
.s-ping.q3 .ms { color: #86efac; }
.s-ping.q2 .ms { color: #fcd34d; }
.s-ping.q1 .ms { color: #fca5a5; }
/* Not measured yet, or the region did not answer. An empty track and no number is
   honestly "we do not know" — a zero or a dash in the number column would read as
   a measurement. */
.s-ping.q0 .ms { color: var(--dim); }

/* ---------------- spectator overlay ---------------- */
/* Sits over a live camera, so the layer passes clicks through and only the
   things you can actually press take them back. */
#spectate {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: 18px;
  gap: 14px;
  pointer-events: none;
  background: linear-gradient(rgba(5, 8, 13, .55), rgba(5, 8, 13, 0) 28%, rgba(5, 8, 13, 0) 62%, rgba(5, 8, 13, .6));
}
#spectate[hidden] { display: none; }
#spectate button,
#spectate .team-panel { pointer-events: auto; }

.spec-bar { display: flex; align-items: center; gap: 16px; }
.spec-room { display: flex; align-items: baseline; gap: 10px; }
.spec-room strong { font-size: 19px; letter-spacing: -.01em; }
.spec-room span { color: var(--dim); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
.spec-score { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 15px; }
.spec-score b { font: 600 20px ui-monospace, SFMono-Regular, Menlo, monospace; }
.spec-score .spec-dash { color: var(--dim); }
.spec-bar #btn-quit { padding: 7px 14px; font-size: 13px; }

/* The two panels, centred over the arena view. */
.spec-teams {
  align-self: center;
  justify-self: center;
  display: grid;
  grid-template-columns: repeat(2, minmax(210px, 260px));
  gap: 18px;
}
.team-panel {
  position: relative;
  display: grid;
  gap: 8px;
  padding: 14px;
  padding-top: 42px; /* room for the Join button that appears above the list */
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
}
.team-panel.team-a { border-color: color-mix(in srgb, var(--a) 45%, transparent); }
.team-panel.team-b { border-color: color-mix(in srgb, var(--b) 45%, transparent); }

/* Hover (or focus) anywhere on a panel reveals its Join. It is always in the
   layout — fading opacity rather than appearing — so nothing shifts under the
   pointer at the moment you go to click it. */
.team-join {
  position: absolute;
  top: 8px;
  left: 14px;
  right: 14px;
  padding: 6px 10px;
  font-weight: 600;
  opacity: 0;
  transition: opacity .12s;
}
.team-panel:hover .team-join,
.team-panel:focus-within .team-join { opacity: 1; }
.team-panel.team-a .team-join { background: var(--a); border-color: transparent; }
.team-panel.team-b .team-join { background: var(--b); border-color: transparent; }
.team-join[disabled] { opacity: 0; }
.team-panel:hover .team-join[disabled],
.team-panel:focus-within .team-join[disabled] {
  opacity: 1;
  background: rgba(255, 255, 255, .07);
  color: var(--dim);
  cursor: not-allowed;
}

.slots { display: grid; gap: 3px; }
.slot {
  display: grid;
  grid-template-columns: 1fr 26px 26px;
  gap: 6px;
  align-items: center;
  padding: 5px 8px;
  border-radius: 7px;
  font-size: 13px;
  background: rgba(255, 255, 255, .03);
  min-height: 30px;
}
.slot.empty { background: none; border: 1px dashed var(--line); color: var(--dim); }
.slot.filled.me { background: rgba(255, 255, 255, .12); font-weight: 600; }
.slot.filled.off { opacity: .45; }
.slot .s-k, .slot .s-d { text-align: right; color: var(--dim); font-variant-numeric: tabular-nums; }
.slot .s-n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.team-foot { display: flex; align-items: baseline; gap: 8px; padding-top: 2px; border-top: 1px solid var(--line); }
.team-foot .tname { font-weight: 600; font-size: 14px; }
.team-panel.team-a .team-foot .tname { color: var(--a); }
.team-panel.team-b .team-foot .tname { color: var(--b); }
.team-foot .tcount { margin-left: auto; color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }

/* A landscape phone is ~390 px tall and has to fit the bar, five slots a side
   and the footer. Everything shrinks rather than scrolling, because a scrolling
   overlay over a live camera is a way to lose the Join button off-screen. */
@media (max-height: 460px) {
  #spectate { padding: 10px; gap: 8px; }
  .spec-room strong { font-size: 16px; }
  .spec-score b { font-size: 17px; }
  .spec-teams { grid-template-columns: repeat(2, minmax(180px, 230px)); gap: 12px; }
  .team-panel { padding: 10px; padding-top: 38px; gap: 6px; }
  .slot { min-height: 24px; padding: 3px 7px; font-size: 12px; }
  .team-foot .tname { font-size: 13px; }
  #roster { font-size: 11px; }
  /* The panel gets tighter, and the ROOM LIST is what absorbs the rest — same as
     desktop. This used to make `#browser` itself scroll, which was wrong for the same
     reason it is wrong on desktop: it turns a menu into a document.
     `place-items: center` is inherited from the base rule and still correct. */
  #browser { padding: 10px; }
  #browser .panel { padding: 14px 16px; }
  #browser .panel h1 { font-size: 20px; margin-bottom: 2px; }
  #browser .panel .tag { margin-bottom: 8px; font-size: 13px; }
  #browser .panel label { margin-bottom: 4px; }
  #browser .panel input { padding: 8px 10px; }
  #browser .err { min-height: 0; margin-top: 6px; }
  .servers { margin-top: 10px; }
  .server-head { padding: 6px 12px; }
  /* THE BUTTON IS WHAT SETS ROW HEIGHT, so it is what has to give. At the desktop
     size a row is ~50 px and only two and a bit of them fit a 390 px-tall phone —
     shrinking the padding and the label buys a third and a half, which is the
     difference between a list you can see and a list you have to discover. */
  .server-row { padding: 5px 12px; gap: 8px; }
  .server-row button { padding: 5px 10px; font-size: 13px; }

  /* THE PING NUMBER STAYS. This query is a LANDSCAPE PHONE — the rotate gate means a
     phone plays at about 844 x 390 — so what is scarce here is height, not width. An
     earlier version hid the figure on the assumption that "mobile" meant "narrow",
     which is the one thing this viewport is not. Only the rows get shorter. */
  .server-row { min-height: 0; }
}

/* Genuinely narrow, which a landscape phone is not: a portrait tablet, or a desktop
   window dragged thin. Here width really is the constraint, so the columns tighten and
   the status wording is what gives way — the player count already implies most of it,
   and the ping is the column the list exists for. */
@media (max-width: 560px) {
  .server-head,
  .server-row { grid-template-columns: 1fr auto 58px 62px 68px; gap: 6px; }
  .server-row .s-players,
  .server-row .s-status { font-size: 11px; }
  .server-head { font-size: 10px; }
  .s-ping { gap: 5px; }
  .s-ping .ms { font-size: 11px; }
  .server-row button { padding: 7px 8px; font-size: 13px; }
}

/* ---- touch ----
   Last on purpose: a phone matches the short-viewport query above as well, and
   these have to win.

   There is no hover on a phone, so a Join that only appears on one is a Join
   that does not exist. It becomes an ordinary block above the list instead of an
   overlay revealed on hover — which also means the panel stops needing padding
   reserved for it, and a thumb-sized button can no longer end up sitting on top
   of the first player's row. `hover: none` rather than a width query on purpose:
   what matters is the input device, not the viewport, and a small window on a
   desktop still has a mouse. */
@media (hover: none) {
  .team-panel { padding-top: 10px; }
  .team-join {
    position: static;
    opacity: 1;
    padding: 11px;
  }
  .team-join[disabled] {
    opacity: 1;
    background: rgba(255, 255, 255, .07);
    color: var(--dim);
  }
  .server-row button { padding: 12px 14px; }
  .spec-bar #btn-quit { padding: 10px 14px; }
}

.spec-foot { display: grid; gap: 4px; justify-items: center; text-align: center; }
#roster { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; font-size: 12px; }
#roster .r { display: flex; gap: 6px; align-items: center; color: var(--dim); }
#roster .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: #6b7280; }
#roster .r.off { opacity: .45; }

/* ---------------- pause ---------------- */
#pause {
  position: fixed;
  inset: 0;
  z-index: 35;
  display: grid;
  place-items: center;
  background: rgba(5, 8, 13, .55);
  backdrop-filter: blur(3px);
  cursor: pointer;
}
#pause[hidden] { display: none; }
.pause-card { display: grid; gap: 14px; justify-items: center; }
.pause-card p { margin: 0; font-size: 20px; letter-spacing: .01em; }
.pause-card .row { margin: 0; }

/* ---------------- HUD ---------------- */
#hud { position: fixed; inset: 0; pointer-events: none; z-index: 10; }
#hud[hidden] { display: none; }

#crosshair { position: absolute; left: 50%; top: 50%; width: 18px; height: 18px; transform: translate(-50%, -50%); opacity: .85; }
#crosshair span { position: absolute; background: #fff; box-shadow: 0 0 2px rgba(0, 0, 0, .9); }
#crosshair span:first-child { left: 50%; top: 0; width: 2px; height: 100%; margin-left: -1px; }
#crosshair span:last-child { top: 50%; left: 0; height: 2px; width: 100%; margin-top: -1px; }

/* --- hitmarker -------------------------------------------------------------
   Four short ticks pointing out of the middle, the Modern Warfare cue. Yellow,
   because the thing that landed is a rubber duck.

   The ticks are one element each, rotated 45° and pushed outward along their own
   axis, so the group is a true X that a single scale on the parent can pop. It is
   `display: none` when idle (not merely transparent) so it costs nothing on the
   frames — the overwhelming majority — where nobody is being hit, and the
   animation is restarted by removing and re-adding the class in hud.js.

   Every number below is twice what it was, stroke width and glow included, so the
   marker is uniformly 2x rather than merely longer. */
#hitmarker { position: absolute; left: 50%; top: 50%; width: 68px; height: 68px; transform: translate(-50%, -50%); display: none; }
#hitmarker.on { display: block; animation: hm .28s cubic-bezier(.16, .84, .3, 1) forwards; }
#hitmarker i {
  position: absolute; left: 50%; top: 50%;
  width: 4px; height: 18px; margin: -9px -2px;
  background: #ffd634;
  /* Dark rim, or a yellow tick over a bright wall is invisible. */
  box-shadow: 0 0 6px rgba(0, 0, 0, .95), 0 0 14px rgba(255, 190, 40, .55);
}
#hitmarker i:nth-child(1) { transform: rotate(45deg) translateY(-16px); }
#hitmarker i:nth-child(2) { transform: rotate(135deg) translateY(-16px); }
#hitmarker i:nth-child(3) { transform: rotate(225deg) translateY(-16px); }
#hitmarker i:nth-child(4) { transform: rotate(315deg) translateY(-16px); }

/* A HIT REPLACES THE CROSSHAIR. Both drawn at once is a cluttered middle of the
   screen at exactly the moment the player is being told something, and at 2x the
   marker's ticks now reach across where the crosshair's arms are. Cut rather than
   faded, in both directions: the marker is an impact, and an impact does not
   dissolve into place.

   This is why the marker sits BEFORE the crosshair in the markup — CSS has no
   previous-sibling combinator, so the ordering is what makes the rule expressible.
   `hud.hitMarker` drops the class on `animationend`, which is what brings the
   crosshair back; without that the class would persist and the crosshair would
   never return. */
#hitmarker.on ~ #crosshair { visibility: hidden; }

/* Snaps in oversized and settles, which is what makes it read as an impact
   rather than as a label fading in. */
@keyframes hm {
  0% { opacity: 1; transform: translate(-50%, -50%) scale(1.5); }
  22% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.15); }
}

#scorebar {
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 14px;
  background: rgba(10, 14, 22, .72); border: 1px solid var(--line);
  border-radius: 10px; padding: 7px 14px; backdrop-filter: blur(8px);
}
#scorebar .team { display: flex; align-items: center; gap: 9px; }
#scorebar .tname { font-size: 12px; letter-spacing: .07em; text-transform: uppercase; color: var(--dim); }
#scorebar .tscore { font: 700 22px ui-monospace, SFMono-Regular, Menlo, monospace; min-width: 34px; text-align: center; }
#scorebar .team-a .tscore { color: var(--a); }
#scorebar .team-b .tscore { color: var(--b); }
#scoretarget { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: .06em; }

#killfeed { position: absolute; top: 14px; right: 14px; display: grid; gap: 5px; justify-items: end; }
#killfeed .k {
  background: rgba(10, 14, 22, .72); border: 1px solid var(--line);
  border-radius: 7px; padding: 5px 10px; font-size: 13px;
  animation: kf .25s ease-out;
}
#killfeed .k .n-a { color: var(--a); font-weight: 600; }
#killfeed .k .n-b { color: var(--b); font-weight: 600; }
#killfeed .k .v { color: var(--dim); margin: 0 5px; }
@keyframes kf { from { opacity: 0; transform: translateX(14px); } }

#cooldown { position: absolute; left: 50%; bottom: 54px; transform: translateX(-50%); width: 120px; height: 3px; background: rgba(255, 255, 255, .16); border-radius: 2px; overflow: hidden; }
#cooldown-fill { height: 100%; width: 100%; background: #fff; transform-origin: left; }

#respawn { position: absolute; left: 50%; top: 58%; transform: translateX(-50%); font-size: 20px; letter-spacing: .04em; text-shadow: 0 2px 12px #000; }
#respawn[hidden] { display: none; }

/* Reconnect banner. Top-centre and under the scorebar rather than in the middle
   of the screen: the game is still drawn and still playable-looking underneath,
   and a modal-looking panel over the top of it would read as "the game has
   stopped" when what is actually true is "the last second of it is stale". */
#netstatus {
  position: fixed; left: 50%; top: 64px; transform: translateX(-50%);
  padding: 7px 16px; border-radius: 999px;
  background: rgba(10, 14, 22, .86); border: 1px solid var(--line);
  font-size: 13px; letter-spacing: .04em; color: var(--fg);
  backdrop-filter: blur(8px);
  /* Informational only — never in the way of the thing underneath it. */
  pointer-events: none;
  animation: nsin .18s ease-out;
}
#netstatus[hidden] { display: none; }
@keyframes nsin { from { opacity: 0; transform: translate(-50%, -6px); } }

/* Centred under the cooldown bar (bottom 54px), where the eye already is. */
#netstat { position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%); font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--dim); opacity: .75; }

/* ---------------- scoreboard ---------------- */
#scoreboard {
  position: fixed; inset: 0; z-index: 30;
  display: grid; place-content: center; gap: 18px;
  background: rgba(5, 8, 13, .82); backdrop-filter: blur(6px);
}
#scoreboard[hidden] { display: none; }
#sb-title { margin: 0; text-align: center; font-size: 24px; font-weight: 600; }
#sb-teams { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.sb-team { border-collapse: collapse; min-width: 270px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.sb-team th { text-align: left; padding: 9px 14px; font-size: 12px; text-transform: uppercase; letter-spacing: .07em; border-bottom: 1px solid var(--line); }
.sb-team th:nth-child(n+2) { text-align: right; width: 46px; color: var(--dim); }
.sb-a thead th:first-child { color: var(--a); }
.sb-b thead th:first-child { color: var(--b); }
.sb-team td { padding: 7px 14px; font-size: 14px; }
.sb-team td:nth-child(n+2) { text-align: right; font-variant-numeric: tabular-nums; color: var(--dim); }
.sb-team tr.me td { background: rgba(255, 255, 255, .06); font-weight: 600; }
.sb-team tr.off td { opacity: .45; }
#sb-footer { display: flex; justify-content: center; align-items: center; gap: 14px; }
#sb-hint { color: var(--dim); font-size: 12px; }

/* ---------------- touch controls ----------------
   Above the HUD, below every overlay that wants a tap of its own. `touch-action`
   is what stops the browser claiming a drag for scroll or zoom before the
   pointer events ever reach the game. */
#touch {
  position: fixed; inset: 0; z-index: 15;
  touch-action: none;
  overscroll-behavior: none;
}
#touch[hidden] { display: none; }

/* Visual only. Hit-testing is numeric against the split, so a button placed
   over a zone is never swallowed by it. */
.tzone {
  position: absolute; top: 0; bottom: 0;
  pointer-events: none;
  display: grid; place-items: center;
  transition: background .15s;
}
#tz-move { left: 0; }
#tz-look { right: 0; }
.tzone span {
  font-size: 13px; letter-spacing: .22em; color: var(--dim);
  opacity: 0; transition: opacity .15s;
}

#tsplit { position: absolute; top: 0; bottom: 0; width: 44px; margin-left: -22px; opacity: 0; pointer-events: none; }
#tsplit i {
  position: absolute; left: 21px; top: 0; bottom: 0; width: 2px;
  background: linear-gradient(transparent, var(--fg) 18%, var(--fg) 82%, transparent);
}
#tsplit::after {
  content: "↔"; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--panel); border: 1px solid var(--fg); font-size: 15px;
}

/* --- edit mode --- */
#touch.editing #tz-move { background: color-mix(in srgb, var(--a) 12%, transparent); }
#touch.editing #tz-look { background: color-mix(in srgb, var(--b) 12%, transparent); }
#touch.editing .tzone span { opacity: .8; }
#touch.editing #tsplit { opacity: 1; pointer-events: auto; }
#touch.editing .tbtn { border-style: dashed; animation: tnudge 1.6s ease-in-out infinite; }
@keyframes tnudge { 50% { transform: translate(-50%, -50%) scale(1.06); } }

#teditbar {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: calc(14px + env(safe-area-inset-bottom));
  display: flex; align-items: center; gap: 10px;
  /* The size slider joined the bar, so on a narrow phone it wraps rather
     than pushing Done off the edge of the screen. */
  flex-wrap: wrap; justify-content: center; max-width: calc(100vw - 24px);
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 8px 10px 8px 16px; font-size: 13px; color: var(--dim);
  box-shadow: 0 12px 40px rgba(0, 0, 0, .6);
}
#teditbar[hidden] { display: none; }
#teditbar button { padding: 7px 14px; font-size: 13px; }
#teditbar label { margin: 0 0 0 6px; }
#teditbar input[type="range"] { width: 110px; }

/* --- floating stick --- */
#tstick {
  position: absolute; width: 112px; height: 112px;
  margin: -56px 0 0 -56px;
  border: 2px solid rgba(255, 255, 255, .22); border-radius: 50%;
  pointer-events: none;
}
#tstick[hidden] { display: none; }
#tstick-knob {
  position: absolute; left: 50%; top: 50%;
  width: 52px; height: 52px; border-radius: 50%;
  background: rgba(255, 255, 255, .16);
  border: 2px solid rgba(255, 255, 255, .5);
}

/* --- action buttons --- */
.tbtn {
  position: absolute;
  transform: translate(-50%, -50%);
  padding: 0; border-radius: 50%;
  background: rgba(10, 14, 22, .34);
  border: 2px solid rgba(255, 255, 255, .34);
  font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;
  letter-spacing: .1em; color: rgba(255, 255, 255, .82);
  display: grid; place-items: center;
  backdrop-filter: blur(2px);
  transition: background .08s, border-color .08s;
}
.tbtn:hover { background: rgba(10, 14, 22, .34); }
.tbtn.down { background: rgba(255, 255, 255, .3); border-color: #fff; }
#tbtn-throw.ready { border-color: rgba(255, 255, 255, .7); }
#tbtn-throw b { position: relative; z-index: 1; font-weight: 600; }

/* Cooldown ring, driven by --cd from touch.js. Masked to a ring so the button
   face stays readable underneath it. */
.tcool {
  position: absolute; inset: -5px; border-radius: 50%; pointer-events: none;
  background: conic-gradient(from -90deg,
    rgba(255, 255, 255, .9) var(--cd, 360deg), rgba(255, 255, 255, .12) 0);
  -webkit-mask: radial-gradient(circle, transparent calc(50% - 3px), #000 calc(50% - 3px));
  mask: radial-gradient(circle, transparent calc(50% - 3px), #000 calc(50% - 3px));
}

/* --- corner buttons --- */
.tcorner {
  position: absolute;
  width: 40px; height: 40px; padding: 0; border-radius: 10px;
  background: rgba(10, 14, 22, .55); border: 1px solid var(--line);
  font-size: 17px; line-height: 1; color: var(--dim);
  display: grid; place-items: center;
}
#tbtn-back { top: calc(10px + env(safe-area-inset-top)); left: calc(10px + env(safe-area-inset-left)); }
#tbtn-settings { top: calc(10px + env(safe-area-inset-top)); right: calc(10px + env(safe-area-inset-right)); }

/* ---------------- controls settings ---------------- */
#tsettings {
  position: fixed; inset: 0; z-index: 45;
  display: grid; place-items: center;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  background: rgba(5, 8, 13, .72); backdrop-filter: blur(4px);
}
#tsettings[hidden] { display: none; }
#tsettings .panel { max-height: calc(100vh - 24px); overflow-y: auto; padding: 20px; }
#tsettings h2 { margin: 0 0 4px; font-size: 21px; }
#tsettings label { margin-top: 16px; }
#tsettings #tset-edit { width: 100%; margin-bottom: 4px; }
#tsettings .row { margin-top: 18px; }
#tsettings .row button { flex: 1; }

input[type="range"] {
  accent-color: var(--a);
  padding: 0; height: 30px; background: none; border: none;
}

/* ---------------- landscape gate ----------------
   Portrait on a coarse pointer, and nothing else. A desktop window narrower
   than it is tall is still a desktop and must never see this. */
#rotate { display: none; }
@media (orientation: portrait) and (pointer: coarse) {
  #rotate {
    position: fixed; inset: 0; z-index: 100;
    display: grid; place-items: center;
    background: var(--bg); text-align: center;
  }
}
.rotate-card { display: grid; gap: 6px; justify-items: center; padding: 24px; }
.rotate-card p { margin: 14px 0 0; font-size: 19px; }
.rotate-card span { color: var(--dim); font-size: 14px; }
.rotate-card .phone {
  width: 54px; height: 92px; border: 2px solid var(--fg); border-radius: 10px;
  animation: rot 2.2s ease-in-out infinite;
}
@keyframes rot {
  0%, 30% { transform: rotate(0deg); }
  60%, 100% { transform: rotate(-90deg); }
}

/* A phone in landscape is short. Give the overlays their space back. */
@media (max-height: 460px) {
  .panel { padding: 18px; border-radius: 12px; }
  .panel h1 { font-size: 24px; }
  .panel .tag { margin-bottom: 14px; }
  #roster { max-height: 84px; }
  #scoreboard { padding: 10px; }
  .sb-team { min-width: 200px; }
  #sb-title { font-size: 19px; }
}
