/* ---------------------------------------------------------------------------
   Additions on top of the shared Nord sheet, which came from the AC EVO page.

   nord.css was written for a grid of clickable server tiles, so .card there is a
   button: pointer cursor, hover lift, focus ring. Here most cards are containers
   holding a form, and a container that lifts as the mouse crosses it reads as
   broken. .card is neutralised and the tile behaviour handed to .card.selectable,
   which only the server grid opts into.
   --------------------------------------------------------------------------- */
.card { cursor: default; padding: 20px; }
.card:hover { border-color: var(--line); background: var(--surface); transform: none; }
.card:focus-visible { outline: none; }
.card + .card { margin-top: var(--gap); }

.card.selectable { cursor: pointer; transition: border-color 120ms ease, transform 120ms ease, background 120ms ease; }
.card.selectable:hover { border-color: var(--accent); background: var(--raised); transform: translateY(-1px); }
.card.selectable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.card-top h2 { margin: 0; }
.pill-row { display: flex; gap: 8px; align-items: center; margin-bottom: 14px; flex-wrap: wrap; }

/* --- header ------------------------------------------------------------- */
.top-left, .top-right { display: flex; align-items: center; gap: 12px; }
.top-right { margin-left: auto; }
.brand {
  font-size: 15px; font-weight: 600; letter-spacing: 0.02em;
  color: var(--heading); text-decoration: none;
}
.nav { display: flex; gap: 4px; }
.nav a {
  font-size: 13px; padding: 5px 11px; border-radius: 6px;
  color: var(--text-dim); text-decoration: none;
}
.nav a:hover { color: var(--heading); background: var(--surface); }
.nav a.active { color: var(--nord8); background: color-mix(in srgb, var(--accent) 18%, transparent); }

/* --- server page -------------------------------------------------------- */
.srv-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--gap); margin-bottom: 16px; }
.srv-head h1 { margin: 4px 0 2px; font-size: 21px; color: var(--heading); }
.srv-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.back { font-size: 12px; color: var(--nord3); text-decoration: none; }
.back:hover { color: var(--accent); }

.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: var(--gap); flex-wrap: wrap; }
.tab {
  padding: 9px 16px; font-size: 13px; text-decoration: none; color: var(--text-dim);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--heading); }
.tab.active { color: var(--nord8); border-bottom-color: var(--accent); }

/* --- sections ----------------------------------------------------------- */
section.group { margin-bottom: 28px; }
section.group.two { display: grid; grid-template-columns: repeat(auto-fit, minmax(430px, 1fr)); gap: var(--gap); align-items: start; }
section.group > h3 { margin: 0 0 2px; }
section.group > .hint { margin: 0 0 14px; }

.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--gap); margin-bottom: 12px; }
.card-head h2 {
  margin: 0; font-size: 14px; font-weight: 600; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--nord7);
}
/* Sub-headings inside a card need real air, or "Scheduled" collides with the
   controls belonging to the section above it. */
.card h3 {
  margin: 22px 0 10px; font-size: 11.5px; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--nord3);
}
.card h3 .hint { text-transform: none; letter-spacing: 0; font-weight: 400; }

/* --- buttons ------------------------------------------------------------ */
/* Roomier than the shared sheet: these sit in dense rows inside cards rather
   than alone in a toolbar, and at 7px they read as cramped. */
.btn { padding: 9px 16px; }
.btn.small { padding: 5px 11px; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; margin: 0 0 12px; }
.btn-row:last-child { margin-bottom: 0; }
/* A field and its button belong on one line; stacked, the button looks orphaned
   under a full-width input. */
.input-row { display: flex; gap: 10px; align-items: stretch; margin-bottom: 12px; }
.input-row .btn { flex: 0 0 auto; }
.input-row select, .input-row input { flex: 1 1 auto; min-width: 0; }

/* --- text --------------------------------------------------------------- */
.hint { color: var(--nord3); font-size: 12.5px; line-height: 1.55; }
.hint.inline { margin: 0; font-size: 12px; }
.mono { font-family: var(--mono); font-size: 12.5px; }
.nowrap { white-space: nowrap; }
.trunc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
code { font-family: var(--mono); font-size: 11.5px; padding: 1px 4px; border-radius: 4px; background: var(--raised); color: var(--nord8); }
/* nord.css centres .empty with 60px of padding, sized for a whole empty page.
   Inside a card that is a hole. */
.empty { padding: 20px 0; font-size: 13px; }
a { color: var(--accent); }

/* --- inputs ------------------------------------------------------------- */
textarea, .when-input {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--line); border-radius: 6px; padding: 10px 12px;
  font-family: var(--mono); font-size: 12.5px; line-height: 1.6; resize: vertical;
}
textarea::placeholder, .when-input::placeholder { color: var(--nord3); }
textarea:focus, .when-input:focus { outline: none; border-color: var(--accent); }
.when-input { flex: 1 1 auto; min-width: 0; resize: none; font-size: 13px; }
.sub-label { display: block; margin: 16px 0 6px; font-size: 11.5px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--nord3); }

.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.seg-btn {
  border: 0; background: transparent; color: var(--text-dim); font: inherit; font-size: 12px;
  padding: 6px 15px; cursor: pointer; transition: background 120ms ease, color 120ms ease;
}
.seg-btn:hover { color: var(--heading); }
.seg-btn.active { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--nord8); }

/* --- drop zone ---------------------------------------------------------- */
.drop {
  display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center;
  padding: 22px 18px; margin: 4px 0 4px; border: 1px dashed var(--line);
  border-radius: var(--radius); background: color-mix(in srgb, var(--bg) 55%, transparent);
  cursor: pointer; transition: border-color 120ms ease, background 120ms ease;
}
.drop:hover, .drop:focus-visible { border-color: var(--accent); outline: none; }
.drop.over { border-color: var(--accent); border-style: solid; background: color-mix(in srgb, var(--accent) 12%, transparent); }
.drop strong { color: var(--heading); font-size: 13.5px; }

/* --- mod tiles ---------------------------------------------------------- */
.mod-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.mod {
  display: flex; flex-direction: column; gap: 10px; padding: 12px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
}
/* Placeholder art until preview extraction lands. Deliberately a flat initial
   rather than a broken image icon. */
.mod-art {
  height: 96px; display: flex; align-items: center; justify-content: center;
  border-radius: 6px; background: linear-gradient(135deg, var(--raised), var(--surface));
  color: var(--nord3); font-family: var(--mono); font-size: 26px; letter-spacing: 0.06em;
}
.mod-art img { width: 100%; height: 100%; object-fit: cover; border-radius: 6px; }
.mod-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.mod-origin { font-size: 11.5px; font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mod-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* --- map tiles ---------------------------------------------------------- */
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-top: 12px; }
.tile {
  position: relative; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg); overflow: hidden;
  transition: border-color 120ms ease, transform 120ms ease;
}
/* Keyed on data-map rather than on .tile itself: an account without map.change
   gets the same tiles as a reference list, and a pointer cursor plus a lift on
   hover on something that cannot be clicked is a promise the page cannot keep. */
.tile[data-map] { cursor: pointer; }
.tile[data-map]:hover { border-color: var(--accent); transform: translateY(-1px); }
.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tile.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.tile-art {
  height: 92px; display: flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, var(--raised), var(--surface));
  color: var(--nord3); font-family: var(--mono); font-size: 24px; letter-spacing: 0.08em;
}
.tile-art img { width: 100%; height: 100%; object-fit: cover; }
.tile-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 2px; }
.tile-flag { position: absolute; top: 8px; right: 8px; }

/* --- configuration form ------------------------------------------------- */
.cfg-group { margin-top: 18px; }
.cfg-group:first-of-type { margin-top: 6px; }
.cfg-group h4 { margin: 0 0 2px; font-size: 12px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase; color: var(--nord7); }
.cfg-group > .hint { margin: 0 0 8px; }
.field .ctl { min-width: 0; }
.field .desc .rule { font-family: var(--mono); font-size: 11.5px; color: var(--nord10); margin-right: 8px; }
.with-toggle { display: flex; gap: 8px; align-items: center; }
.with-toggle input { flex: 1 1 auto; min-width: 0; }

/* --- list rows ---------------------------------------------------------- */
.row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent); }
.row:last-child { border-bottom: 0; }
.row-main { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto; }
.row.job .row-main { align-items: baseline; }
.row-main .hint.own-line { flex: 1 0 100%; margin: 2px 0 0; }
/* The action column takes its natural width and .row-main absorbs the rest.
   Without this the flex row shares the squeeze between them, and a row with
   three buttons wrapped "Issue #12" onto two lines reading "Issue" and "#12"
   while there was empty space beside the title. .row-main has min-width: 0, so
   it is the one that gives way, which is the right way round: a long subject can
   ellipsis, a button cannot. Overridden below 560px, where the buttons take
   their own row instead. */
.row > .btn-row { flex: 0 0 auto; }
.job-notes { flex-basis: 100%; margin: 4px 0 0; padding-left: 18px; font-size: 12px; line-height: 1.55; }
.job-notes li.bad { color: var(--bad); }
.job-notes li.warn { color: var(--warn); }

/* --- badge and pill variants -------------------------------------------- */
.badge.ok { color: var(--ok); background: color-mix(in srgb, var(--ok) 16%, transparent); }
.badge.warn { color: var(--warn); background: color-mix(in srgb, var(--warn) 16%, transparent); }
.badge.bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 16%, transparent); }
.badge.info { color: var(--nord9); background: color-mix(in srgb, var(--nord9) 16%, transparent); }
.badge.muted {
  /* Was nord3, which is about 1.5:1 on the card surface: the same unreadable
     grey the "secondary text contrast" block below was written to get rid of.
     A badge is text, so it gets the same mid tone. */
  color: var(--muted);
  background: color-mix(in srgb, var(--nord3) 30%, transparent);
  border-color: color-mix(in srgb, var(--muted) 26%, transparent);
}
.badge.game { margin-left: 0; }
.badge.mono { font-family: var(--mono); text-transform: none; letter-spacing: 0; }
/* nord.css covers running, starting, offline and error. Wings also reports
   stopping and the Panel reports installing. */
.pill.stopping { color: var(--warn); background: color-mix(in srgb, var(--warn) 16%, transparent); }
.pill.installing { color: var(--nord9); background: color-mix(in srgb, var(--nord9) 16%, transparent); }

@media (max-width: 940px) {
  section.group.two { grid-template-columns: 1fr; }
  .card-head, .srv-head { flex-wrap: wrap; }
  .field { grid-template-columns: 1fr; }
  .field .desc { grid-column: 1; }
}

/* --- modal --------------------------------------------------------------- */
/* Replaces confirm() and prompt() entirely. A native dialog cannot be styled,
   announces the origin in a header that reads like a security warning, and
   blocks the event loop while it is open. */
.modal-back {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: color-mix(in srgb, var(--nord0) 72%, transparent);
  backdrop-filter: blur(3px);
  animation: modal-in 120ms ease-out;
}
.modal {
  width: 100%;
  max-width: 440px;
  padding: 22px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5);
}
.modal h3 {
  margin: 0 0 8px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--heading);
}
.modal-body {
  margin: 0 0 4px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-dim);
}
.modal-body code {
  word-break: break-all;
}
.modal-field {
  display: block;
  margin-top: 14px;
}
.modal-field > span {
  display: block;
  margin-bottom: 5px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--nord3);
}
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 20px;
}

@keyframes modal-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .modal-back { animation: none; }
}

/* --- numbered steps ------------------------------------------------------ */
.steps {
  margin: 0 0 4px;
  padding-left: 20px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--text-dim);
}
.steps li { margin-bottom: 2px; }
.steps em { color: var(--nord8); font-style: normal; }
.steps strong { color: var(--heading); }

/* --- uniform server cards ------------------------------------------------ */
/* Every card in a row identical in width and height. The grid already gives
   equal columns; the work is making the CONTENT line up, because a name that
   wraps to two lines would otherwise push that card's facts and bars down
   relative to its neighbours. */
.grid { align-items: stretch; }

.card.selectable {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.card-top { flex: 0 0 auto; }
.card-top h2 {
  /* Always two lines tall, so a short name and a wrapped one occupy the same
     space and every card starts its facts at the same height. Longer names are
     clamped rather than allowed to grow the card. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.3;
  min-height: 2.6em;
}
.card .sub {
  flex: 0 0 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pill-row { flex: 0 0 auto; }
.card .facts { flex: 0 0 auto; }
/* Pushes the bars to the bottom edge, so they align across the row even if
   something above them differs in height. */
.card .bars { margin-top: auto; }

/* The badge was wrapping "AC EVO" onto two lines inside the pill, because the
   flex row squeezed it to fit the heading beside it. */
.badge { white-space: nowrap; }
.badge.game {
  margin-left: 0;
  flex: 0 0 auto;
  align-self: flex-start;
}

/* --- loading skeletons --------------------------------------------------- */
/* Shown while the first request is in flight. Without these the page renders
   the empty state for a moment and reads as "you have no servers", which is a
   lie that arrives before the truth. */
.sk {
  border-radius: 5px;
  background: linear-gradient(
    90deg,
    var(--raised) 0%,
    color-mix(in srgb, var(--raised) 55%, var(--surface)) 50%,
    var(--raised) 100%
  );
  background-size: 200% 100%;
  animation: sk 1.3s ease-in-out infinite;
}
@keyframes sk {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .sk { animation: none; }
}
.sk-title { height: 17px; width: 68%; margin-bottom: 8px; }
.sk-sub { height: 11px; width: 46%; margin-bottom: 14px; }
.sk-pill { height: 22px; width: 84px; border-radius: 999px; margin-bottom: 16px; }
.sk-line { height: 12px; width: 100%; margin-bottom: 8px; }
.sk-line.short { width: 55%; }
.sk-bar { height: 6px; width: 100%; margin-bottom: 10px; border-radius: 999px; }
.sk-art { height: 96px; width: 100%; margin-bottom: 10px; }
.sk-tile { height: 92px; width: 100%; }
.card.skel { pointer-events: none; }

/* --- empty states -------------------------------------------------------- */
/* Says what is missing and what to do about it, rather than one grey word. */
.blank {
  padding: 30px 22px;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--bg) 45%, transparent);
}
.blank strong {
  display: block;
  margin-bottom: 6px;
  font-size: 14px;
  color: var(--heading);
}
.blank p {
  margin: 0 auto;
  max-width: 52ch;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--nord3);
}
.blank .btn-row { justify-content: center; margin: 14px 0 0; }

/* Stacked cards inside a tab need separating, but grid children must NOT: the
   grid already provides the gap, and a top margin on every card after the first
   shifts it down inside its own row. That is what made the server cards look
   like different heights when they were in fact identically sized and merely
   offset from each other. */
.grid > .card + .card {
  margin-top: 0;
}

/* --- secondary text contrast --------------------------------------------- */
/* nord3 (#4c566a) on the card surface (#3b4252) is about 1.5:1, which is not
   readable text, it is a hint that something is there. Every secondary label
   went through it: the address line, the fact labels, the bar labels, .hint.
   This mid tone lands near 4.3:1 while still clearly sitting behind the
   primary text. Defined here rather than edited into nord.css so the AC EVO
   page keeps whatever it had. */
:root {
  --muted: color-mix(in srgb, var(--nord4) 58%, var(--nord3));
}
.card .sub,
.sub-label,
.card h3,
.card .facts dt,
.bar-row .lbl,
.hint,
/* nord.css sets this one to nord3 directly, and section.group > .hint beats a
   bare .hint on specificity, so the section sub-headings were still landing at
   1.69:1 on the page background long after the rest was fixed. Same specificity
   as that rule and later in the cascade, which is what makes it win. */
section.group > .hint,
.field .env,
.mod-origin .hint,
/* Missed by the original sweep because nothing on the pages rendered at the time
   used them: the field labels inside a modal (which the feedback dialog made
   suddenly prominent, three of them stacked) and the two on the sign-in gate,
   which is the first thing anybody sees. All three were nord3 on a surface. */
.modal-field > span,
.login-field > span,
.login-sub {
  color: var(--muted);
}
.blank p { color: var(--muted); }

/* The repository verdict gets its own row inside the tile. Wrapping rather than
   clipping, because the badge plus a date is wider than a tile on one line, and
   nord.css gives .badge a leading margin meant for trailing a heading. */
.mod-compat {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 11.5px;
  min-width: 0;
}
.mod-compat .badge { margin-left: 0; }
.mod-compat .hint { min-width: 0; }

/* --- title height -------------------------------------------------------- */
/* Reserving two lines in every card put visible dead space above the state pill
   whenever the names happened to be short, which is most of the time. The clamp
   stays, so one very long name cannot stretch the row, but the height is natural
   again and the BOTTOM block is what gets aligned instead: the grid already
   makes the boxes equal, so pushing facts and bars down together lines up the
   part with numbers in it. */
.card-top h2 {
  min-height: 0;
}
.card .facts {
  margin-top: auto;
}
.card .bars {
  margin-top: 0;
}

/* Ten category chips do not fit one pill-shaped row, so this variant wraps and
   squares off rather than overflowing the card. */
.seg.wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
  border-radius: var(--radius);
  margin-bottom: 10px;
}
.seg.wrap .seg-btn {
  border-radius: 5px;
}

/* --- overview filter ----------------------------------------------------- */
.filter-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
  flex-wrap: wrap;
}
.filter-row .seg {
  flex: 0 0 auto;
}
.filter-row .when-input {
  flex: 1 1 220px;
  min-width: 180px;
  max-width: 340px;
}

/* --- console ------------------------------------------------------------- */
.console {
  margin: 0 0 12px;
  padding: 12px 14px;
  height: 46vh;
  min-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}
.console:focus-visible {
  outline: 1px solid var(--accent);
}

/* --- link out to Pelican ------------------------------------------------- */
/* The grid card is one big button, so a second target inside it has to look
   like one: a bordered chip that lights up on its own hover, not a bare glyph
   that reads as decoration. Sized to the badge beside it rather than to the
   text, so the top row of every card is the same height whatever it holds. */
.card-top-right {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}
/* nord.css gives .badge a left margin for when it trails a heading. Here the
   flex gap does that job and the margin only pushes the pair off-centre. */
.card-top-right .badge { margin-left: 0; }

.icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex: 0 0 auto;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--nord9);
  text-decoration: none;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.icon-link:hover {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.icon-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.icon-link svg { display: block; }

/* ===========================================================================
   Minecraft, plus the per-game accent the whole grid gained with it.

   Four games of identical boxes read as one undifferentiated wall, so each card
   now carries its game's hue on a hairline and on its game badge. The hue comes
   from games.py as a Nord token name and arrives as the --game custom property
   set on the card itself, so nothing here hardcodes which game is which.
   =========================================================================== */
.card.accented { border-left: 3px solid color-mix(in srgb, var(--game, var(--accent)) 55%, var(--line)); }
.card.accented:hover { border-left-color: var(--game, var(--accent)); }
.card.selectable.accented:hover { border-color: var(--game, var(--accent)); }

/* Falls back to the old fixed hue so a page without a card around it, such as
   the server header, still renders. */
.badge.game {
  color: var(--game, var(--nord15));
  background: color-mix(in srgb, var(--game, var(--nord15)) 18%, transparent);
}

.card-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.card-title h2 { margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* server-icon.png is 64x64 pixel art. Smoothing it looks like a decoding fault,
   so it is scaled with nearest neighbour and framed instead. */
.mc-art { flex: 0 0 auto; line-height: 0; }
.mc-art img {
  display: block;
  image-rendering: pixelated;
  border-radius: 6px;
  background: var(--raised);
  border: 1px solid var(--line);
}
/* Double dash, not a bare .tile or .big modifier: .tile is already the BeamMP
   map tile in this sheet, complete with a hover lift and its own border, and a
   shared modifier name silently gave the MOTD a box and a hover animation. */
.mc-art--tile img { width: 40px; height: 40px; }
.mc-art--big img { width: 72px; height: 72px; }

/* The live player count. The dot carries the state, so the text stays short
   enough to sit in the pill row next to the power state. */
.mc-live {
  /* nowrap and no shrink: inside .pill-row this is a flex item, and without both
     "3 / 20 online" breaks across two lines and makes the whole row 41px tall. */
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap; flex: 0 0 auto;
  font-size: 11.5px; font-family: var(--mono);
  padding: 3px 9px; border-radius: 999px;
  color: var(--muted); background: color-mix(in srgb, var(--nord3) 22%, transparent);
}
.mc-live i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
.mc-live.on { color: var(--ok); background: color-mix(in srgb, var(--ok) 15%, transparent); }
.mc-live.idle { color: var(--text-dim); }
.mc-live.off { color: var(--nord3); }

/* The MOTD is the server owner's own words, so it keeps its colours. Monospace
   because Minecraft's own server list is, and clamped on a tile so a three line
   MOTD cannot push the resource bars out of alignment across the grid. */
.mc-motd {
  font-family: var(--mono); font-size: 11.5px; line-height: 1.45;
  margin: 6px 0 2px; color: var(--text-dim); word-break: break-word;
}
.mc-motd--tile {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; min-height: 2.9em;
}

/* Two stretch bugs with the same root cause: a child of a flex column or a grid
   cell fills its cross axis unless told not to. The true/false pair became a
   full width empty bar, and the placement badge became a full width strip that
   read as a progress bar. .badge.game already had this fix for the same reason. */
.field .seg { justify-self: start; }
.mod-body > .badge { align-self: flex-start; }

/* Player names, whitelist entries and ops. A chip rather than a table row: these
   are short, unordered and mostly read at a glance. */
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-family: var(--mono);
  padding: 5px 10px; border-radius: 6px;
  color: var(--text); background: var(--raised); border: 1px solid var(--line);
}
.chip-x {
  border: 0; background: none; cursor: pointer; padding: 0 0 0 2px;
  color: var(--nord3); font-size: 15px; line-height: 1;
}
.chip-x:hover { color: var(--bad); }
.chip-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

/* server.properties has fifty-odd keys, so the uncurated remainder is a two
   column list rather than the two-per-row layout .facts uses elsewhere. */
.facts.wide { grid-template-columns: minmax(140px, auto) 1fr; }

/* Reserved for statements about consequences, never for decoration: the
   whitelist being off, or a save that a running server will overwrite. */
.warn-text { color: var(--warn); }

/* --- laptimes ------------------------------------------------------------- */
/* A leaderboard is the one thing here that genuinely wants a table: six columns
   that must line up down the page, where the flex .row used elsewhere would let
   every name push the times out of alignment. Nord throughout, no new colours:
   the only accents are the existing --ok for the fastest lap and --muted for
   what a driver did not supply. */

.tbl-wrap { overflow-x: auto; margin: 0 -4px; }

.tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.tbl th {
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 10px 8px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.tbl td {
  padding: 9px 10px;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 45%, transparent);
  vertical-align: baseline;
}

.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl tbody tr:hover td { background: color-mix(in srgb, var(--nord3) 20%, transparent); }
.tbl td.empty { text-align: center; color: var(--muted); }

/* Times are the point of the table, so they get the mono column and the bright
   foreground while everything around them stays quieter. */
.laps .lap-time { font-family: var(--mono); color: var(--heading); white-space: nowrap; }
.laps .lap-pos { width: 1%; color: var(--muted); font-variant-numeric: tabular-nums; }
.laps .lap-n { width: 1%; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.laps .lap-gap { white-space: nowrap; color: var(--muted); }
.laps .lap-when,
.laps .lap-track { color: var(--muted); white-space: nowrap; }
.laps .lap-sect { color: var(--muted); white-space: nowrap; font-size: 12px; }
.laps .lap-who { min-width: 0; }
.laps .lap-car { color: var(--text); }

/* First row is the outright best on this track, so it reads as one. */
.laps tbody tr:first-child .lap-time { color: var(--ok); }

/* --- row actions: the lap kebab ------------------------------------------ */
/* Revealed on hover, but hover alone would make this unreachable without a
   mouse, so keyboard focus and an open menu reveal it too. Kept at opacity 0
   rather than display:none so the column never changes width as the pointer
   moves down the table, which reads as the whole row twitching. */
.laps .lap-act {
  width: 1%;
  padding-left: 0;
  padding-right: 2px;
  text-align: right;
  position: relative;
  white-space: nowrap;
}
.laps .kebab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: opacity .12s ease, background .12s ease, color .12s ease;
}
.laps .kebab:hover,
.laps .kebab.open {
  background: color-mix(in srgb, var(--nord3) 55%, transparent);
  color: var(--heading);
}
.laps .kebab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.laps .kebab svg { width: 16px; height: 16px; fill: currentColor; }

.laps .kebab-menu {
  position: absolute;
  top: calc(100% - 4px);
  right: 2px;
  z-index: 20;
  min-width: 148px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 20px color-mix(in srgb, var(--nord0) 55%, transparent);
  text-align: left;
}
.laps .kebab-menu button {
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  padding: 7px 10px;
  border-radius: calc(var(--radius) - 2px);
  cursor: pointer;
  white-space: nowrap;
}
.laps .kebab-menu button:hover,
.laps .kebab-menu button:focus-visible {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--heading);
  outline: none;
}
.laps .kebab-menu button.danger { color: var(--bad); }
.laps .kebab-menu button.danger:hover,
.laps .kebab-menu button.danger:focus-visible {
  background: color-mix(in srgb, var(--bad) 22%, transparent);
  color: var(--bad);
}

/* .tbl-wrap is overflow-x: auto, and a scroll container clips on BOTH axes
   whatever the other axis says, so the dropdown would be cut off by the bottom
   of the table on any short board. Stop clipping only while a menu is actually
   open: the horizontal scroll is not needed for the second or two it is up, and
   the narrow layout hides the widest column anyway. */
.tbl-wrap:has(.kebab-menu) { overflow: visible; }

/* Last row drops upward, so it does not push the card's own edge. */
.laps tbody tr:last-child .kebab-menu { top: auto; bottom: calc(100% - 4px); }

/* Hiding is gated on the device actually HAVING hover, rather than being undone
   again for touch. Written the other way round first, and a headless Chromium
   (which reports hover: none) then matched the touch fallback and showed every
   button at once, so the reveal-on-hover behaviour could not be tested at all.
   This way the default is the reachable one and hover is the enhancement. */
@media (hover: hover) {
  .laps .kebab { opacity: 0; }
  /* :focus and not :focus-visible. Revealing a hidden control is not the same
     job as drawing a focus ring: Chrome withholds :focus-visible from focus it
     considers non-keyboard, including a programmatic .focus(), so the button
     could hold focus while still being invisible. The ring below stays on
     :focus-visible, which is what that pseudo-class is actually for. */
  .laps tbody tr:hover .kebab,
  .laps .kebab:focus,
  .laps .kebab.open { opacity: 1; }
}

.muted { color: var(--muted); }

@media (max-width: 720px) {
  /* Sectors are the first thing worth losing on a phone: the driver, the car
     and the time are what somebody came to see. */
  .laps .lap-sect { display: none; }
  .tbl th, .tbl td { padding-left: 6px; padding-right: 6px; }
}

/* ===========================================================================
   Responsive.

   Everything above this was written at 1280px and only ever checked there. On a
   390px phone every page was 825px wide: the header alone needs about 940px
   (brand, five nav links, two status chips, an identity badge and Sign out all
   on one row), so the whole document inherited that width and the viewport
   showed the left 47% of it with a dead grey field to the right. Verified with
   scripts/mobile-check.js, which renders scripts/mobile-preview.js output at a
   given width and names every element whose right edge passes the viewport.

   Three widths, each earning its place:
     940px  the header drops the identity badge, which is the widest thing on it
            that nothing depends on. Content already reflowed here.
     760px  the header collapses to one toggle. Below this the nav genuinely does
            not fit however it is trimmed.
     560px  phone proper: tighter page and card padding, 16px inputs, thumb-sized
            controls, and rows that stack instead of squeezing.

   Page and card padding are custom properties rather than repeated literals,
   because four things need to bleed content out to the edge by exactly that
   amount and a mismatch there shows up as a stripe.
   =========================================================================== */

:root {
  --page-pad: 24px;
  --card-pad: 20px;
}
/* Beats nord.css's own main padding, including its 720px override: same
   specificity, later sheet. The variable is the single place it is decided. */
main { padding: var(--page-pad); }
.card { padding: var(--card-pad); }

/* --- track minimums ------------------------------------------------------ */
/* A fixed minmax() minimum is wider than the viewport once the viewport is
   narrower than the minimum, and the column then simply sticks out: at 320px a
   minmax(320px, 1fr) track plus page padding overflows by the padding. min()
   clamps the track to the row it is in, which is what auto-fill needs anyway. */
.grid { grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); }
.mod-grid { grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); }
.tile-grid { grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); }
section.group.two { grid-template-columns: repeat(auto-fit, minmax(min(430px, 100%), 1fr)); }
.car-pop { min-width: min(340px, 100%); }

/* --- things that are one long unbreakable string -------------------------- */
/* Map paths, jar filenames, invite tokens and server.properties values. These
   sit in mono cells with no space to break at, so without this one value decides
   the width of its whole card. break-word only breaks when there is no other
   option, so ordinary prose is unaffected. */
code,
.card .facts dd,
.mc-motd,
.modal-body { overflow-wrap: anywhere; }
/* A long car name would otherwise widen the 1fr track past the row. */
.car-list .cn { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the collapsed header toggle ----------------------------------------- */
/* The nav sits beside .top-left rather than inside it, so that below 760px it
   can wrap onto its own row: a flex child cannot wrap out of its parent. The
   12px gap here reproduces the spacing .top-left used to give brand and nav;
   .top-right has margin-left:auto so the gap never applies to it. */
header.top { gap: 12px; }

.menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  margin-left: auto;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-dim);
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.menu-toggle:hover { color: var(--heading); border-color: var(--nord9); }
.menu-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.menu-toggle svg { display: block; }
/* One icon, two states. The open glyph is a close glyph, so the same control
   that opened the panel visibly closes it. */
.mt-x { display: none; }
header.top.open .menu-toggle { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
header.top.open .mt-bars { display: none; }
header.top.open .mt-x { display: block; }

/* --- 940px: shed the identity badge -------------------------------------- */
@media (max-width: 940px) {
  /* "Makl Baumann · admin" is about 155px of header and the widest thing on it
     that no action depends on. It comes back inside the panel below 760px,
     which is where knowing who you are signed in as actually belongs. */
  #whoami { display: none; }
}

/* --- 760px: the header collapses ---------------------------------------- */
@media (max-width: 760px) {
  header.top {
    flex-wrap: wrap;
    row-gap: 10px;
    padding: 11px var(--page-pad);
  }
  .menu-toggle { display: inline-flex; }
  .nav, .top-right { display: none; }
  /* The panel is the same surface as the bar it grows out of, so without this it
     reads as the page having got taller rather than as something opened over it. */
  header.top.open { box-shadow: 0 14px 28px -10px rgba(0, 0, 0, 0.55); }

  header.top.open .nav,
  header.top.open .top-right {
    display: flex;
    flex: 1 1 100%;
  }
  /* A column, not a wrapped row: five links of different widths in two ragged
     rows is harder to read than a list, and a full-width row is a bigger target
     than the text it holds. */
  header.top.open .nav {
    flex-direction: column;
    gap: 2px;
    padding-top: 4px;
    border-top: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  }
  header.top.open .nav a {
    padding: 11px 12px;
    font-size: 14px;
    border-radius: 6px;
  }
  /* Status, identity and Sign out: what the bar carried on the right, now the
     foot of the panel, in reading order rather than pushed to an edge. */
  header.top.open .top-right {
    margin-left: 0;
    flex-wrap: wrap;
    gap: 10px;
    padding-top: 10px;
    border-top: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  }
  header.top.open #whoami { display: inline-block; }
  header.top.open #sign-out { margin-left: auto; }

  /* Tabs scroll rather than wrap. Wrapping put Restarts alone on a second row
     and made the server header two rows taller on every phone; a strip keeps
     the panel content where the thumb already is. Bled to the page edges so it
     reads as scrollable and the last tab can reach the edge. */
  .tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    margin-left: calc(-1 * var(--page-pad));
    margin-right: calc(-1 * var(--page-pad));
    padding-left: var(--page-pad);
    padding-right: var(--page-pad);
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tab { flex: 0 0 auto; }
}

/* --- 720px: content padding --------------------------------------------- */
@media (max-width: 720px) {
  :root { --page-pad: 16px; }

  /* The game chips are wider than a phone at four games and get wider with each
     one added. A scroll strip rather than a wrap: the pill outline is the whole
     affordance and a pill broken across two rows has no outline left. The search
     field takes its own row, which it wanted anyway. */
  .filter-row { flex-wrap: wrap; }
  .filter-row .seg {
    flex: 1 1 100%;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .filter-row .seg::-webkit-scrollbar { display: none; }
  /* Without this the buttons shrink to fit instead of the strip scrolling, and
     "Minecraft (1)" becomes "Minecr...". Scoped off .seg.wrap, which is the
     laptimes track picker and is meant to wrap. */
  .filter-row .seg:not(.wrap) .seg-btn { flex: 0 0 auto; }
  .filter-row .when-input { flex: 1 1 100%; max-width: none; }
}

/* --- 560px: phone ------------------------------------------------------- */
@media (max-width: 560px) {
  :root {
    --page-pad: 12px;
    --card-pad: 14px;
  }

  /* iOS zooms the page in when a focused field is under 16px and does not zoom
     back out, which leaves the layout scrolled sideways with no way to recover
     short of a reload. Meeting the threshold is cheaper than fighting it. */
  input[type="text"],
  input[type="number"],
  input[type="password"],
  select,
  textarea,
  .when-input { font-size: 16px; }

  /* Thumb-sized. .btn.small was 27px tall, which is a miss more often than a
     hit, and the dashboard is nearly all small buttons. */
  .btn { padding: 11px 16px; }
  .btn.small { padding: 8px 13px; }
  .seg-btn { padding: 9px 14px; }
  .tab { padding: 11px 14px; }
  .chip-x { padding: 0 2px 0 4px; font-size: 17px; }

  .srv-head h1 { font-size: 19px; }
  .modal-back { padding: 14px; }
  .modal { padding: 18px; }
  .login-wrap { padding: 16px; }
  .login-card { padding: 24px 20px 22px; }

  /* A field and its button share a line on a desktop because a full-width input
     with a small button orphaned underneath reads as broken. Here the input has
     nothing to share, so the button takes the next row at full width, which is
     the phone idiom rather than an orphan. */
  .input-row { flex-wrap: wrap; }
  .input-row .btn { flex: 1 1 100%; }

  /* Row actions drop under the row they act on instead of squeezing the text
     that says which row it is. The 12px flex gap does the separating; a margin
     here would lose to the inline style="margin:0" several of these carry. */
  .row { flex-wrap: wrap; }
  .row > .btn-row { flex: 1 1 100%; }

  .savebar { flex-wrap: wrap; }

  /* Tables get the card's full width back. The bleed has to match --card-pad
     exactly or it shows as a stripe down one side. */
  .tbl-wrap {
    margin-left: calc(-1 * var(--card-pad));
    margin-right: calc(-1 * var(--card-pad));
    padding-left: var(--card-pad);
    padding-right: var(--card-pad);
  }

  /* Laps, on the two columns that can afford to go. The stamp is the widest
     thing in Recent laps and the only column that splits cleanly, into a date
     and a clock. The lap counter is a footnote beside the time it produced. */
  .laps .lap-when { white-space: normal; }
  .laps .lap-n { display: none; }
}

/* ===========================================================================
   Feedback: the header button, the dialog, and the attached images.
   =========================================================================== */

/* Sits between the nav and the status block, and stays on the visible row on a
   phone rather than going into the collapsed panel: it is the one control here
   that somebody who is not an admin has a reason to reach for, so hiding it
   behind a toggle would defeat the point of having it. */
.feedback-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--nord8);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.feedback-btn:hover:not(:disabled) {
  color: var(--nord6);
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  border-color: var(--accent);
}
.feedback-btn svg { display: block; flex: 0 0 auto; }

/* .top-right also carries margin-left:auto. With both of them claiming the free
   space the button ends up centred and the status block hard right, which looks
   like a mistake, so the second one gives way. */
.feedback-btn ~ .top-right { margin-left: 0; }

/* --- the dialog ---------------------------------------------------------- */
/* Wider than a confirm: this one holds a subject, six lines of description and a
   row of thumbnails, and 440px makes all three feel cramped at once. */
.fb-modal { max-width: 560px; }
/* A long report plus six thumbnails is taller than a laptop, let alone a phone,
   so the box scrolls rather than running off the screen with the Send button on
   the far side of the edge. */
.fb-modal {
  max-height: calc(100vh - 48px);
  overflow-y: auto;
}

.fb-count {
  float: right;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted);
}

/* Shorter than the mods drop zone: there it is the main event, here it is the
   third field of four and 22px of padding pushes Send below the fold. */
.fb-drop { padding: 14px 16px; margin: 0; }
.fb-drop strong { font-size: 13px; }

/* --- attached images ----------------------------------------------------- */
/* A row that wraps rather than a grid: between one and six items, and a grid
   with a fixed track would stretch a single screenshot across the whole dialog. */
.fb-shots {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}
.fb-shot {
  position: relative;
  width: 92px;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--bg);
  overflow: hidden;
}
/* A fixed box with cover, so six screenshots of six different shapes still line
   up. The full image is on the issue; this is only ever "which one is that". */
.fb-shot img {
  display: block;
  width: 100%;
  height: 64px;
  object-fit: cover;
  background: var(--raised);
}
.fb-shot figcaption {
  padding: 3px 6px;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--muted);
}
/* Always visible, not revealed on hover: half the people using this are on a
   phone, where there is no hover and a control that needs one cannot be reached
   at all. Same reasoning as the laptimes kebab. */
.fb-shot-x {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 20px;
  height: 20px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 4px;
  background: color-mix(in srgb, var(--nord0) 72%, transparent);
  color: var(--nord4);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.fb-shot-x:hover { background: var(--bad); color: var(--nord6); }
.fb-shot-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

@media (max-width: 560px) {
  /* The label goes, the icon stays. "Feedback" is 58px of a 390px header, and
     the icon plus the title attribute still say what it is. */
  .feedback-label { display: none; }
  .feedback-btn { padding: 8px 10px; }
  .fb-modal { max-height: calc(100vh - 28px); }
  /* Three across at 390px rather than two and a gap. */
  .fb-shot { width: calc((100% - 16px) / 3); }
}

/* --- BeamHQ -------------------------------------------------------------- */
/* The minted claim code. Big because it is read off the screen and typed into a
   game chat box by hand, and shown exactly once: it is never written to
   status.json, so this element is the only place it ever exists. Letter spacing
   because the alphabet already avoids 0/O and 1/I/L and the remaining job is
   simply to keep two adjacent characters from reading as one. */
.hq-code {
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--nord8);
  text-align: center;
  margin: 6px 0 14px;
  user-select: all;
}

@media (max-width: 480px) {
  .hq-code { font-size: 1.5rem; letter-spacing: 0.12em; }
}
