:root {
  color-scheme: dark;
  --bg: #0f1115;
  --panel: #161922;
  --border: #262b38;
  --text: #d8dce6;
  --muted: #6b7280;
  --accent: #5b9dff;
  --ok: #3ecf8e;
  --warn: #f2b84b;
  --bad: #ef5a5a;
  /* Raised surfaces: the panel is the sidebar/sheet body, the rail is the
     header strip that sits on top of it. */
  --rail: #1b1f2a;
  --hover: rgba(255, 255, 255, 0.045);
  --sidebar-width: 320px;
  /* The notch, the home indicator, and the height the bottom tab bar needs to
     clear the latter. Named once so every mobile rule reads the same. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --tabbar-row: 48px;
  --tabbar-height: calc(var(--tabbar-row) + var(--safe-bottom));
  /* How tall the app makes itself — see the standalone correction below. */
  --app-height: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Compatibility shim for installed apps that draw underneath the status bar.
   Those are handed a viewport with the status bar's height subtracted while
   still being painted from the top of the screen, so the page lands exactly
   one status bar short and leaves a dead strip along the bottom — and nothing
   reports the real height, `100%`, `100dvh` and `window.innerHeight` all carry
   the same subtraction. Measured on a 390x844pt device: viewport 797pt, inset
   47pt, screen 844pt. Adding the inset back is exact rather than approximate.

   index.html no longer asks for that layout (see the status-bar-style note
   there), which zeroes the inset and makes this inert — but a home screen icon
   added before that change can keep the old style, so this stays for them. */
@media (display-mode: standalone) {
  :root {
    --app-height: calc(100% + env(safe-area-inset-top, 0px));
  }
}

* {
  box-sizing: border-box;
}

/* Only the root takes the corrected height; body follows it at a plain 100%.
   Giving both `calc(100% + inset)` would add the inset twice, since body's
   percentage resolves against the root's already-corrected height. */
html {
  margin: 0;
  height: var(--app-height);
  /* The corrected height is taller than the viewport the platform reported, so
     the document would otherwise be draggable by exactly that much. Every
     scrolling region in the app is an inner one. */
  overflow: hidden;
}

body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
}

.hidden {
  display: none !important;
}

input {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 6px 8px;
  border-radius: 6px;
  font: inherit;
}

button {
  background: #1e2330;
  border: 1px solid var(--border);
  color: var(--text);
  padding: 6px 10px;
  border-radius: 6px;
  cursor: pointer;
  font: inherit;
}

button:hover {
  border-color: var(--accent);
}

/* --- login screen --- */

/* Centred by the card's own auto margins rather than by align/justify, which
   clip the top of anything taller than the screen instead of letting it
   scroll — with the document itself no longer scrollable, that would put a
   long character list out of reach. */
.login {
  height: 100%;
  display: flex;
  overflow-y: auto;
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.login-card {
  display: flex;
  flex-direction: column;
  margin: auto;
  gap: 10px;
  width: 260px;
  padding: 24px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
}

.login-card h1 {
  font-size: 16px;
  margin: 0 0 6px;
  color: var(--accent);
  text-align: center;
  letter-spacing: 0.02em;
}

.login-card input {
  width: 100%;
}

/* Secondary action on the auth card (register beside log in): same shape,
   quieter, so the primary action still reads as the one to press. */
button.secondary {
  background: none;
  color: var(--muted);
}

button.secondary:hover {
  color: var(--text);
}

/* Splits the card between the two ways in â with an account, or without one at
   all â so "Play as guest" doesn't read as a third variation on logging in. */
.login-divider {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--muted);
  font-size: 11px;
}

.login-divider::before,
.login-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* --- password rules --- */

/* The requirements sit next to the fields and tick over as you type, so the
   rule you're failing is visible before you submit rather than after. */
.input-rules {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 12px;
}

.input-rules li {
  display: flex;
  align-items: baseline;
  gap: 7px;
  color: var(--muted);
}

/* A dot that becomes a tick — same width either way, so the text doesn't
   shift sideways as rules are met. */
.input-rules li::before {
  content: "·";
  width: 9px;
  text-align: center;
}

.input-rules li.met {
  color: var(--ok);
}

.input-rules li.met::before {
  content: "✓";
}

/* --- character select --- */

/* Whose account this is — the session outlives the tab, so the character list
   is often the first thing you see and needs to say which login it belongs
   to. Sits under the heading, quiet enough not to compete with the names. */
.account-email {
  margin: -6px 0 0;
  color: var(--muted);
  font-size: 12px;
  text-align: center;
  overflow-wrap: anywhere;
}

/* The one thing a guest account has to say for itself. Sits under the address
   line in the warning colour rather than the muted one: it isn't a label, it's
   the consequence of there being no email to recover the account with. */
.account-note {
  margin: -4px 0 0;
  color: var(--warn);
  font-size: 11px;
  line-height: 1.4;
  text-align: center;
}

.player-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* One row per character, tall enough to be a comfortable tap target on the
   phone this screen is mostly used from. */
.player-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 9px 11px;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}

/* The row is the button that enters the game, so delete can't nest inside it
   — it sits alongside as its own button and the pair share the row's width. */
.player-row-wrap {
  display: flex;
  gap: 6px;
}

.player-row-wrap .player-row {
  flex: 1;
  min-width: 0;
}

.player-delete {
  flex: none;
  padding: 0 11px;
  color: var(--muted);
  line-height: 1;
}

.player-delete:hover {
  color: var(--bad);
  border-color: var(--bad);
}

/* The confirm step replaces the row in place rather than opening a dialog, so
   the character you're about to lose stays on screen while you decide. */
.player-row-confirm {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 9px 11px;
  background: #1e2330;
  border: 1px solid var(--bad);
  border-radius: 6px;
}

.player-row-actions {
  display: flex;
  gap: 6px;
  padding-top: 6px;
}

button.danger {
  border-color: var(--bad);
  color: var(--bad);
}

button.danger:hover {
  border-color: var(--bad);
  background: var(--bad);
  color: var(--bg);
}

.player-row-name {
  font-weight: 600;
}

.player-row-meta {
  color: var(--muted);
  font-size: 12px;
}

.player-empty {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  text-align: center;
}

/* Name field and Create button share a line — the form is one field, so a
   stacked layout would waste the card's width. */
.new-player {
  display: flex;
  gap: 8px;
  padding-top: 4px;
  border-top: 1px solid var(--border);
}

.new-player input {
  flex: 1;
  min-width: 0;
}

.login-status {
  min-height: 16px;
  margin: 0;
  font-size: 12px;
  color: var(--warn);
  text-align: center;
}

/* Which build is being served. Quiet enough to ignore, but it's the answer to
   "did my change actually reach the phone" — an installed app holding a stale
   shell otherwise looks exactly like a fix that didn't work. */
.build-tag {
  margin: 2px 0 0;
  color: var(--muted);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-align: center;
  opacity: 0.65;
}

/* --- game screen --- */

.game {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

/* Desktop only — see the @media rule at the bottom, where it goes away
   entirely. All it holds now is the app's name and the way out; panels are
   reached from the sidebar's own headers. */
header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 16px;
  padding-top: calc(9px + var(--safe-top));
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}

header h1 {
  font-size: 15px;
  margin: 0;
  color: var(--accent);
  letter-spacing: 0.02em;
}

/* Log on the left, panel sidebar on the right. The sidebar is a real grid
   column rather than an overlay, so panels never cover the log. */
main {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--sidebar-width);
}

/* --- player HUD strip --- */

/* Name, level and the two meters worth watching mid-fight, pinned to the top
   of the stage on every screen size. It wraps to a second line rather than
   squeezing the meters when the name and level leave too little room. */
.player-bar {
  display: flex;
  flex: none;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 7px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}

.hud-id {
  display: flex;
  flex: none;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

.hud-name {
  color: var(--ok);
  font-size: 13px;
  font-weight: 600;
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hud-level {
  color: var(--muted);
  font-size: 12px;
  white-space: nowrap;
}

/* Below ~240px of free space the meters take a line of their own. */
.hud-meters {
  display: flex;
  flex: 1 1 240px;
  gap: 14px;
  min-width: 0;
}

.hud-meter {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.hud-meter b {
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
}

/* Same look as the panel's .stat-bar, without its stacked-row margins. */
.hud-track {
  flex: 1;
  min-width: 36px;
  height: 8px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.04);
  overflow: hidden;
}

.hud-text {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Mobile only: with no header there has to be a way off the character, and
   this is the only chrome left at the top of the screen to hang it on. */
.hud-exit {
  display: none;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  background: none;
  border-color: transparent;
  color: var(--muted);
}

.hud-exit svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
}

.hud-meter-hp {
  position: relative;
}

.hud-float-host {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 0;
  pointer-events: none;
}

.hud-float-pop {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  color: var(--ok);
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
  animation: hud-float-up 900ms ease-out forwards;
}

@keyframes hud-float-up {
  0% { opacity: 0; transform: translate(-50%, 4px); }
  15% { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -16px); }
}

/* The left column: the two action bars stacked over the log, which is the
   only part that scrolls. Identical on desktop and mobile — the bars are the
   primary way to fight either way, so they don't move around between them. */
.stage {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.log {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: 10px 16px;
}

/* --- action bars (spells, enemies) --- */

/* One horizontal strip of cards each. They scroll sideways rather than
   wrapping, so however many spells you know or enemies you're facing, the
   bars keep exactly the same height and never push the log off screen.
   `touch-action: pan-x` keeps a sideways drag from being read as a page
   scroll on mobile. */
.action-bar {
  display: flex;
  flex: none;
  gap: 8px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  overflow-x: auto;
  overflow-y: hidden;
  touch-action: pan-x;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}

/* Cards are fixed-width so a long name truncates instead of stretching the
   card and making the strip's scroll length unpredictable. */
.card {
  position: relative;
  display: flex;
  flex: none;
  flex-direction: column;
  gap: 4px;
  width: 128px;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #1b1f2a;
  color: var(--text);
  text-align: left;
  overflow: hidden;
  /* Cards are tapped constantly mid-fight; the tap highlight and the
     text-selection that a double tap causes both get in the way. */
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

.card-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px;
}

.card-name {
  overflow: hidden;
  font-size: 12px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-sub {
  flex: none;
  color: var(--muted);
  font-size: 11px;
  line-height: 1.2;
}

.card-bar {
  display: block;
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.07);
  overflow: hidden;
}

.card-bar-fill {
  display: block;
  height: 100%;
  width: 100%;
  transition: width 200ms ease-out;
}

.card-bar-fill.hp {
  background: var(--bad);
}

/* --- buff cards --- */

/* The buff row is the only bar that isn't always there — it's added and
   removed as things run out, so it carries no empty state. Cards aren't
   interactive: a buff is a readout, not something you can act on. */
.buff-card {
  cursor: default;
}

.buff-card .card-name {
  color: #9d7bff;
}

.card-bar-fill.buff {
  background: #9d7bff;
  /* Redrawn every frame as the buff drains, so the 200ms ease the health bars
     use would be permanently chasing its own tail here. */
  transition: none;
}

.buff-card .buff-time {
  font-variant-numeric: tabular-nums;
}

/* --- enemy cards --- */

.enemy-card:hover:not(:disabled) {
  border-color: var(--accent);
}

/* A kill greys the card out in place rather than taking it off the strip, so
   the row you're aiming at never moves — the enemy respawns back into the same
   slot. The `button:hover` accent has to be held off too, or a corpse would
   still look tappable. */
.enemy-card.dead,
.enemy-card.dead:hover {
  border-color: var(--border);
  background: #14171f;
  cursor: default;
  opacity: 0.55;
}

.enemy-card.dead .card-name {
  color: var(--muted);
  text-decoration: line-through;
}

/* The one you're fighting. The ring is drawn with a box-shadow rather than a
   thicker border so the card doesn't shift by a pixel as targets change. */
.enemy-card.targeted {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  background: rgba(91, 157, 255, 0.12);
}

.enemy-card.targeted .card-name {
  color: var(--accent);
}

/* --- spell cards --- */

/* The cooldown sweep: a scrim over the whole card that drains left to right
   as the spell comes back. Scaled rather than resized so the per-frame tick
   never triggers layout. */
.spell-drain {
  position: absolute;
  inset: 0;
  transform: scaleX(0);
  transform-origin: left center;
  background: rgba(15, 17, 21, 0.72);
  pointer-events: none;
}

.spell-card {
  justify-content: space-between;
}

.spell-card:hover:not(:disabled) {
  border-color: var(--accent);
}

/* What the spell is, as a colour: damage reads as the combat red, buffs and
   heals as the spell purple and the heal green used in the log. */
.spell-card.damage .card-name { color: var(--bad); }
.spell-card.buff .card-name { color: #9d7bff; }
.spell-card.heal .card-name { color: var(--ok); }

/* The global `button:hover` accent still lands on a disabled card, which
   reads as "tap me" for a spell that isn't ready — hold the resting border. */
.spell-card:disabled,
.spell-card:disabled:hover {
  border-color: var(--border);
  cursor: not-allowed;
}

.spell-card:disabled .card-name {
  color: var(--muted);
}

/* The countdown out-ranks the drain, so the number stays readable while the
   scrim is over it. */
.spell-card .card-sub {
  position: relative;
  z-index: 1;
}

.spell-card .cooldown-text.counting {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/* --- loot bar --- */

.loot-bar {
  gap: 6px;
  padding: 6px 16px;
}

.loot-chip {
  display: inline-flex;
  flex: none;
  align-items: baseline;
  gap: 6px;
  max-width: 240px;
  padding: 4px 10px;
  border-radius: 999px;
  background: #1b1f2a;
  font-size: 12px;
  line-height: 1.4;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

.loot-chip.pending,
.loot-chip.pending:hover {
  border-color: var(--border);
  opacity: 0.45;
  cursor: progress;
}

.loot-chip .loot-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

.loot-chip .loot-qty,
.loot-chip .loot-meta {
  color: var(--muted);
  font-size: 11px;
}

.loot-chip .loot-timer {
  color: var(--muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.loot-chip .loot-timer.reserved { color: var(--ok); }
.loot-chip .loot-timer.urgent { color: var(--warn); }

.loot-chip .price { color: var(--warn); }
.loot-chip .item-name.common { color: var(--text); }
.loot-chip .item-name.rare { color: var(--accent); }
.loot-chip .item-name.legendary { color: var(--warn); font-weight: 600; }

.loot-chip.take-all {
  color: var(--accent);
  font-weight: 600;
}

.line {
  padding: 3px 0;
  line-height: 1.5;
  border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}

.line .ts {
  color: var(--muted);
  margin-right: 8px;
  font-size: 12px;
}

.line .block-title { font-weight: bold; }
.line .block-row { padding-left: 14px; }
.line .block-row b { color: var(--muted); font-weight: 600; margin-right: 6px; }
.line .block-row.item-row { padding-left: 28px; }

/* --- item tables (inventory / equipment) --- */

/* Cells never wrap, so a long inventory scrolls sideways inside the block
   instead of stretching the log or folding item names onto two lines. */
.line .table-wrap {
  margin-left: 14px;
  overflow-x: auto;
}

/* Tables under a sub-heading row (the per-NPC tables in `list`) line up with
   .block-row.item-row one level deeper. */
.line .table-wrap.nested {
  margin-left: 28px;
}

.line .item-table {
  border-collapse: collapse;
  margin: 4px 0 2px;
  font-size: 13px;
}

.line .item-table th,
.line .item-table td {
  padding: 2px 14px 2px 0;
  text-align: left;
  white-space: nowrap;
}

.line .item-table th {
  color: var(--muted);
  font-weight: 600;
  border-bottom: 1px solid var(--border);
}

/* The block title keeps the log-kind color; the rows themselves are plain
   text so a wide table doesn't turn into a wall of amber. */
.line .item-table td {
  color: var(--text);
  border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}

.line .item-table tbody tr:last-child td { border-bottom: none; }
.line .item-table .num { text-align: right; }
.line .item-table td.price { color: var(--warn); }
.line .item-table .dim,
.line .block-row .dim { color: var(--muted); }

/* Also used by the friends/party panels, hence no `.line` scope. */
.presence-dot {
  display: inline-block;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  vertical-align: middle;
}

.presence-dot.online { background: var(--ok); }
.presence-dot.offline { background: var(--bad); }

/* Room names that travel when clicked (the `todo` listing). Underlined rather
   than recoloured so they still read as part of the table row, and given the
   same focus ring treatment as the map's room nodes for keyboard use. */
.room-link {
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
}

.room-link:hover,
.room-link:focus-visible {
  text-decoration-style: solid;
}

/* Help topics work like room links: typed out as the command they run, so
   they read the same whether you click them or type them yourself. */
.line .help-topic {
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  white-space: nowrap;
}

.line .help-topic:hover,
.line .help-topic:focus-visible {
  text-decoration-style: solid;
}

/* Item names carry their rarity as colour, the usual way: plain for common,
   blue for rare, gold for legendary. Names show up both in tables and inside
   log sentences, so these have to out-specify the per-line `.line.drop span`
   colours below — hence the `span` in the selector. */
.line span.item-name.common { color: var(--text); }
.line span.item-name.rare { color: var(--accent); }
.line span.item-name.legendary { color: var(--warn); font-weight: 600; }

.line.system span { color: var(--muted); }
.line.chat span { color: var(--text); }
.line.whisper span { color: #c68ae0; font-style: italic; }
.line.move span { color: var(--accent); }
.line.look { color: #9d7bff; }
.line.npc { color: #9d7bff; }
.line.damage span { color: var(--bad); }
/* Crits are read at a glance in a scrolling combat log, so they get their own
   color rather than the line's — bright enough to catch the eye against both
   the red of a melee hit and the purple of a spell. */
.line .crit {
  color: #ffb020;
  font-weight: bold;
  text-shadow: 0 0 6px rgba(255, 176, 32, 0.35);
}
.line.spell span { color: #9d7bff; }
.line.respawn span { color: var(--ok); }
.line.heal span { color: var(--ok); }
.line.drop span { color: var(--warn); }
.line.inventory { color: var(--warn); }
.line.equipment { color: var(--warn); }
.line.stats { color: var(--accent); }
.line.todo { color: var(--accent); }
.line.ack span { color: var(--muted); font-style: italic; }
.line.error span { color: var(--bad); font-weight: bold; }
.line.raw span { color: var(--muted); }
/* Help flips the usual block coloring: the usage (in <b>) is the part worth
   scanning, so it gets the accent and the descriptions stay muted. */
.line.help { color: var(--muted); }
.line.help .block-row b { color: var(--accent); }

footer {
  display: flex;
  flex: none;
  gap: 8px;
  padding: 10px 16px;
  padding-bottom: max(10px, var(--safe-bottom));
  border-top: 1px solid var(--border);
  background: var(--panel);
}

footer input {
  flex: 1;
}

/* --- bottom tab bar --- */

/* Mobile navigation, and the thing that replaced the row of buttons wedged
   into the header. Hidden on desktop, where the sidebar shows the same panels
   without anything to switch between. The @media rule at the bottom turns it
   on and gives it its safe-area padding. */
.panel-tabs {
  display: none;
}

.panel-tab {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-width: 0;
  height: var(--tabbar-row);
  padding: 0 2px;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--muted);
  -webkit-tap-highlight-color: transparent;
}

.panel-tab:hover {
  border-color: transparent;
}

.panel-tab svg {
  width: 21px;
  height: 21px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.panel-tab-label {
  max-width: 100%;
  overflow: hidden;
  font-size: 10px;
  letter-spacing: 0.01em;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The sheet this tab opens is the one on screen. */
.panel-tab.active {
  color: var(--accent);
}

/* Something behind the tab is waiting on an answer — an incoming friend
   request, or a party invite. */
.panel-tab.alert {
  color: var(--warn);
}

.panel-tab.alert::after {
  content: "";
  position: absolute;
  top: 6px;
  left: 50%;
  margin-left: 6px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--warn);
}

/* --- panel windows --- */

/* The desktop sidebar: one scroller holding every panel as a collapsible
   card, so the two or three you care about can be open at once and the rest
   stay out of the way as a single header line each. On mobile the same
   elements become full-screen sheets — see the @media rule at the bottom. */
#panels {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  border-left: 1px solid var(--border);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.panel {
  display: flex;
  /* Natural height, and the sidebar scrolls — a card that shrank to fit would
     take its own scroller's height away. Long lists cap themselves instead
     (see .gear-list / .social-list). */
  flex: none;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}

.panel-header {
  display: flex;
  flex: none;
  align-items: stretch;
  background: var(--rail);
}

.panel.open .panel-header {
  border-bottom: 1px solid var(--border);
}

/* The whole header line is the collapse control on desktop; on mobile it's
   just the sheet's title and the click handler stands down (see panels.ts). */
.panel-heading {
  display: flex;
  flex: 1;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 9px 11px;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}

.panel-heading:hover {
  background: var(--hover);
  border-color: transparent;
}

.panel.open .panel-heading {
  color: var(--accent);
}

.panel-icon,
.panel-chevron,
.panel-close-icon {
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.panel-icon {
  width: 16px;
  height: 16px;
}

.panel-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Something behind a collapsed card is waiting on an answer. */
.panel.alert .panel-heading {
  color: var(--warn);
}

.panel-dot {
  display: none;
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--warn);
}

.panel.alert .panel-dot {
  display: block;
}

.panel-chevron {
  width: 14px;
  height: 14px;
  color: var(--muted);
  transition: transform 140ms ease;
}

.panel:not(.open) .panel-chevron {
  transform: rotate(-90deg);
}

.panel:not(.open) .panel-body {
  display: none;
}

/* Mobile only: the sheets are the only panels there is anything to close. */
.panel-close {
  display: none;
  align-items: center;
  justify-content: center;
  width: 46px;
  padding: 0;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--muted);
}

.panel-close:hover {
  color: var(--text);
  border-color: transparent;
}

.panel-close-icon {
  width: 18px;
  height: 18px;
}

.panel-body {
  padding: 10px 12px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* --- character panel --- */

/* Who you are, above the numbers describing you. */
.char-head {
  padding-bottom: 2px;
}

.char-name {
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

.char-sub {
  margin-top: 2px;
  color: var(--muted);
  font-size: 12px;
}

/* Splits a panel that holds more than one view — the character card's stat
   block above its equipment slots. */
.panel-subhead {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.panel-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 3px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.03);
  font-size: 13px;
}

.panel-row b {
  color: var(--muted);
  font-weight: 600;
}

/* The derived half of a stat — the damage a defense rating actually turns
   away, the note that XP has nowhere left to go. */
.panel-row .dim {
  color: var(--muted);
}

/* --- stat bars --- */

.stat-bar {
  height: 8px;
  margin: 4px 0 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.04);
  overflow: hidden;
}

.stat-bar-fill {
  height: 100%;
  width: 100%;
  transition: width 250ms ease-out;
}

.stat-bar-fill.hp {
  background: var(--bad);
}

.stat-bar-fill.xp {
  background: var(--accent);
}

/* --- map panel --- */

.map {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* The window the map is panned around inside. `touch-action: none` hands
   every gesture to the pointer handlers in map.ts, so a drag pans the map
   instead of scrolling the panel behind it. */
.map-viewport {
  position: relative; /* anchors .map-tooltip and .map-focus */
  height: 190px;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #12151d;
  touch-action: none;
  cursor: grab;
  overscroll-behavior: contain;
  /* A drag pans the map, so don't let it also select the room labels (or pop
     the iOS long-press callout over them). The caption below stays selectable. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.map-viewport:active {
  cursor: grabbing;
}

/* Moved by a transform set from JS; the svg itself is always drawn at its
   natural size. */
.map-canvas {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

.map-canvas svg {
  display: block;
}

/* Recentre-on-player button, floating over the bottom-right of the map. */
.map-focus {
  position: absolute;
  right: 6px;
  bottom: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 6px;
  background: rgba(22, 25, 34, 0.9);
  color: var(--muted);
  z-index: 2;
}

.map-focus:hover {
  color: var(--accent);
}

.map-focus svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
}

/* Corridors. Drawn as polylines (a single straight segment between
   neighbouring rooms, an elbow for a stair between floors), so they need
   `fill: none` or the bend would be filled in as a shape. */
.map-link {
  fill: none;
  stroke: var(--border);
  stroke-width: 2;
}

/* Stairs between one floor of the map and the next */
.map-link.stair {
  stroke-dasharray: 3 3;
}

/* Which floor of the area each block of rooms is */
.map-floor {
  fill: var(--muted);
  font-family: inherit;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.map-node {
  cursor: pointer;
  outline: none;
}

.map-node rect {
  fill: #1b1f2a;
  stroke: var(--border);
  stroke-width: 1;
}

.map-node:hover rect,
.map-node:focus-visible rect {
  fill: #232936;
  stroke: var(--accent);
}

.map-node:hover text,
.map-node:focus-visible text {
  fill: var(--text);
}

.map-node text {
  fill: var(--muted);
  font-family: inherit;
  font-size: 9px;
  text-anchor: middle;
  dominant-baseline: central;
}

.map-node.current rect {
  fill: rgba(91, 157, 255, 0.22);
  stroke: var(--accent);
}

.map-node.current text {
  fill: var(--accent);
}

/* Hover label for a room, positioned over the svg from JS (see map.ts).
   No `display` here so the `hidden` attribute keeps working. */
.map-tooltip {
  position: absolute;
  transform: translate(-50%, calc(-100% - 6px));
  padding: 3px 7px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #11151d;
  color: var(--text);
  font-size: 12px;
  white-space: nowrap;
  pointer-events: none;
  z-index: 1;
}

.map-caption {
  color: var(--muted);
  font-size: 12px;
  text-align: center;
}

.map-caption b {
  color: var(--accent);
  font-weight: 600;
}

.map-empty {
  color: var(--muted);
  font-size: 13px;
}

/* Mobile: no room for a sidebar, so panels go back to being full-screen
   sheets opened one at a time from the header toggles. */
/* --- friends & party panels --- */

/* In the desktop sidebar every panel is stacked in one scroller, so a long
   friends list would push the ones below it out of reach — it gets its own
   scroll instead, which also keeps the name field pinned in view. The mobile
   sheet has the whole screen and resets this. */
.social-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 300px;
  overflow-y: auto;
}

.social-section + .social-section {
  margin-top: 10px;
}

.social-section-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 6px;
}

.social-empty {
  font-size: 12px;
  color: var(--muted);
  padding: 2px 0 4px;
}

.social-entry {
  padding: 7px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}

.social-entry:last-child {
  border-bottom: none;
}

.social-entry-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 13px;
}

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

.social-level {
  color: var(--muted);
  font-size: 12px;
}

.social-tag {
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0 4px;
}

.social-entry-sub {
  margin-top: 3px;
  font-size: 12px;
  color: var(--text);
}

.social-entry-sub .dim {
  color: var(--muted);
}

/* The bar sits inline with its reading rather than above it, so a member
   frame stays three short lines however many are in the party. */
.social-hp {
  display: flex;
  align-items: center;
  gap: 6px;
}

.social-hp .stat-bar {
  flex: 1;
  margin: 3px 0;
}

.social-hp-text {
  flex: none;
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.social-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 6px;
}

.social-btn {
  padding: 3px 8px;
  font-size: 11px;
  border-radius: 5px;
}

.social-btn.primary {
  border-color: var(--ok);
  color: var(--ok);
}

.social-btn.danger:hover {
  border-color: var(--bad);
  color: var(--bad);
}

/* Armed: one more click sends it. */
.social-btn.armed {
  border-color: var(--bad);
  color: var(--bad);
  font-weight: 600;
}

.social-add {
  display: flex;
  gap: 6px;
  margin-top: 12px;
}

.social-add input {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  padding: 4px 7px;
}

.social-add button {
  flex: none;
  padding: 4px 10px;
  font-size: 12px;
}

.social-footer-btn {
  width: 100%;
  margin-top: 8px;
  font-size: 12px;
  padding: 5px 10px;
}

.social-footer-btn.armed {
  border-color: var(--bad);
  color: var(--bad);
  font-weight: 600;
}

/* --- inventory & equipment panels --- */

/* Both panels are lists of one item per row: what it is, what it does for
   you, and the things you can do to it. A table would be the log's shape, and
   a table's cells aren't tappable — in a 320px sidebar or on a phone, the
   buttons are the point. */
.gear-summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 7px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}

.gear-summary b {
  color: var(--muted);
  font-weight: 600;
}

.gear-summary .gold {
  color: var(--warn);
  font-variant-numeric: tabular-nums;
}

/* Like .social-list: its own scroller, so a full bag can't push the panels
   under it out of the sidebar. The mobile sheet resets this. */
.gear-list {
  display: flex;
  flex-direction: column;
  max-height: 320px;
  overflow-y: auto;
}

.gear-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 7px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}

.gear-row:last-child {
  border-bottom: none;
}

/* The slot label leads an equipment row — which slot this is matters even
   when nothing is in it, so it reads before the item name. */
.gear-slot {
  flex: none;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.gear-head {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
  font-size: 13px;
}

.gear-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gear-qty {
  flex: none;
  color: var(--muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* Type, slot, rolled stats and what a merchant here would pay — a row of
   short readings rather than four columns that would never line up. */
.gear-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  font-size: 12px;
  color: var(--muted);
}

.gear-meta .stat {
  color: var(--text);
}

.gear-meta .gold {
  color: var(--warn);
}

.gear-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 2px;
}

.gear-empty {
  color: var(--muted);
  font-style: italic;
}

/* Mobile — and the layout every decision here leans towards. The desktop
   sidebar can't fit, so the panels become one full-screen sheet at a time
   raised from the bottom tab bar, and the header disappears into the HUD
   strip. The breakpoint is deliberately high: a narrow desktop window is
   better served by the sheets than by a sidebar squeezing the log. */
@media (max-width: 860px) {
  /* Nothing left in it worth a row of its own: the title is the app icon's
     job and Disconnect moves onto the HUD strip. */
  header {
    display: none;
  }

  main {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The HUD is the top of the screen now, so it owns the notch. Laid out as
     two explicit rows rather than left to wrap: who you are and the way out on
     the first, both meters across the full width on the second, at every phone
     size instead of only the ones where the wrap happens to land well. */
  .player-bar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 5px 10px;
    padding: 7px 12px;
    padding-top: calc(7px + var(--safe-top));
  }

  .hud-id {
    grid-area: 1 / 1;
  }

  .hud-exit {
    display: flex;
    grid-area: 1 / 2;
  }

  .hud-meters {
    grid-area: 2 / 1 / 3 / -1;
  }

  /* Tighter gutters, and cards sized for a thumb rather than a cursor. */
  .action-bar {
    gap: 6px;
    padding: 8px 10px;
  }

  .loot-bar {
    padding: 6px 10px;
  }

  .card {
    width: 122px;
    padding: 8px 10px;
  }

  .log {
    padding: 10px 12px;
  }

  /* Tighter than desktop's: the tab bar below it already costs a row, and the
     two of them together are the bottom of a phone screen. */
  footer {
    padding: 6px 12px;
  }

  /* iOS Safari zooms the page on focus of any input with font-size under
     16px; 16px here keeps the command bar from triggering it. */
  footer input {
    font-size: 16px;
  }

  /* --- bottom tab bar --- */

  .panel-tabs {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    flex: none;
    border-top: 1px solid var(--border);
    background: var(--panel);
    padding-bottom: var(--safe-bottom);
    /* Above the sheets, so switching panels is always one tap — and
       positioned, or a fixed sheet would paint over it whatever the z-index
       said. */
    position: relative;
    z-index: 60;
  }

  /* --- panel sheets --- */

  /* Stops above the tab bar rather than covering it: the bar is how you get
     from one panel to the next, and tapping the active tab closes the sheet. */
  #panels {
    position: fixed;
    top: 0;
    /* Fixed positioning steps outside .game, so the side insets a notched
       phone needs in landscape have to be repeated here. */
    left: env(safe-area-inset-left, 0px);
    right: env(safe-area-inset-right, 0px);
    bottom: var(--tabbar-height);
    display: block;
    gap: 0;
    padding: 0;
    border-left: none;
    overflow: visible;
    pointer-events: none;
    z-index: 50;
  }

  .panel {
    position: absolute;
    inset: 0;
    border: none;
    border-radius: 0;
    pointer-events: auto;
  }

  /* Only the open sheet is on screen; the rest aren't stacked behind it. */
  .panel:not(.open) {
    display: none;
  }

  /* The notch again — without this the close button lands under the status
     bar's clock and battery in a standalone PWA. */
  .panel-header {
    padding-top: var(--safe-top);
  }

  .panel-heading {
    padding: 13px 14px;
    font-size: 15px;
  }

  .panel-icon {
    width: 18px;
    height: 18px;
  }

  /* Nothing to collapse here — a sheet is open or it isn't. */
  .panel-chevron {
    display: none;
  }

  .panel-close {
    display: flex;
  }

  .panel-body {
    flex: 1;
    padding: 14px 16px 24px;
  }

  /* The sheet has the whole screen, so the sidebar's inner scrollers (which
     exist only to stop one panel crowding out the others) come off. */
  .social-list,
  .gear-list {
    max-height: none;
    overflow-y: visible;
  }

  /* Sized for a thumb, and 16px on the name field so iOS Safari doesn't zoom
     the sheet in when it gains focus. */
  .social-btn {
    padding: 6px 12px;
    font-size: 12px;
  }

  .social-add input {
    font-size: 16px;
    padding: 7px 9px;
  }

  .social-add button,
  .social-footer-btn {
    padding: 7px 12px;
    font-size: 13px;
  }

  .gear-row {
    padding: 9px 0;
  }

  /* Full-screen sheet: the map is the whole sheet. The body stops scrolling
     and becomes the flex column, so the caption pins to the top and the
     viewport takes every pixel left between it and the tab bar. */
  .panel[data-panel="map"] .panel-body {
    display: flex;
    min-height: 0;
    padding-bottom: 14px;
    overflow: hidden;
  }

  .map {
    flex: 1;
    min-height: 0;
  }

  .map-caption {
    order: -1;
  }

  .map-viewport {
    flex: 1;
    height: auto;
    min-height: 0;
  }

  .map-focus {
    width: 38px;
    height: 38px;
  }
}
