/* Spawnpoint — a small token set plus component classes. Extend this rather than inventing a
   per-page styling system; it is deliberately not a framework. */

:root {
    --accent: #4c3fb1;
    --accent-contrast: #ffffff;
    /* The accent as *text* on the app's surfaces. The same violet that works as a filled button
       fails contrast as text on a dark surface, so this one lightens in dark mode. */
    --accent-text: #4c3fb1;

    --bg: #f4f6f7;
    --surface: #ffffff;
    --surface-2: #eef1f3;
    --border: #d9e0e3;

    --text: #16202a;
    --text-muted: #5c6b76;
    --danger: #b3261e;
    --danger-bg: #fdecea;
    --warn: #b7791f;
    --warn-bg: #fdf3dc;
    --ok: #1f8a4c;
    --busy: #c98a12;

    --radius: 10px;
    --radius-sm: 8px;
    --gap: 1rem;
    /* Vertical rhythm between the blocks of a page — header, banners, filters, list. Tuned by eye:
       tighter than --gap, which is the padding *inside* a card, so the blocks of a page read as
       closer together than the contents of any one of them. */
    --section-gap: 0.75rem;
    --topbar-height: 3.25rem;
    --shadow: 0 1px 2px rgb(16 32 42 / 8%), 0 2px 8px rgb(16 32 42 / 6%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #10161b;
        --surface: #182027;
        --surface-2: #212b33;
        --border: #2d3944;

        --text: #e7edf1;
        --text-muted: #9aabb7;
        --danger: #f2b8b5;
        --danger-bg: #3a1c1a;
        --warn: #e0b45c;
        --warn-bg: #3a2e12;
        --ok: #4fc27a;
        --busy: #e2a93b;
        --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 2px 8px rgb(0 0 0 / 30%);
        --accent-text: #b4a8ff;
    }
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 16px;
    line-height: 1.5;
}

h1, h2, h3 { line-height: 1.25; margin: 0 0 0.5rem; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.1rem; }

a { color: var(--accent-text); }

.muted { color: var(--text-muted); }
.small { font-size: 0.85rem; }
.empty { padding: 2rem 0; text-align: center; }

/* ----------------------------------------------------------------- layout */

.app-shell {
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

.app-main {
    flex: 1;
    width: 100%;
    max-width: 52rem;
    margin: 0 auto;
    /* No allowance for the topbar: it is sticky, so it occupies its own space in flow rather than
       floating over the content and needing a padding kept in sync with its height by hand. */
    padding: var(--gap) var(--gap) calc(var(--gap) + env(safe-area-inset-bottom));
}

.blank-layout { min-height: 100dvh; display: grid; place-items: center; padding: var(--gap); }

/*
  A flex column with a gap, not child margins.

  This was `.page > * + * { margin-top: var(--gap) }`, which loses every time a component resets its
  own margin: `.card-list { margin: 0 }` has identical specificity and comes later in this file, so
  any list following a control row or a banner sat flush against it — 0px, not merely tight. A gap on
  the container cannot be overridden by a child, so the rhythm holds whatever a component does to its
  own box. Do not reintroduce the sibling-margin version.
*/
.page {
    display: flex;
    flex-direction: column;
    gap: var(--section-gap);
}

.page > * {
    /* The gap owns all vertical spacing here, so children must not bring their own. Flex items do
       not collapse margins the way block siblings do, so a paragraph's default 1em would *add* to
       the gap instead of merging with it. */
    margin-block: 0;
    /* Long titles and pasted text must not be able to stretch the column wider than its parent. */
    min-width: 0;
}

.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap);
    flex-wrap: wrap;
}

.page-header h1 { margin: 0; }

/* ----------------------------------------------------------------- topbar */

/*
  Sticky rather than fixed, and three regions that divide the width explicitly.

  A `position: fixed` bar plus a plain `display: flex` nav is how this clips on a phone: a flex item
  defaults to `min-width: auto`, so the nav refuses to shrink below its content and pushes the
  account strip past the right edge, with no scrollbar and no way to reach it. Brand and account
  never shrink, the nav absorbs whatever width is left, and if the links still do not fit they
  scroll instead of vanishing. Do not reintroduce the fixed version.
*/
.topbar {
    position: sticky;
    top: 0;
    z-index: 30;
    height: calc(var(--topbar-height) + env(safe-area-inset-top));
    padding: env(safe-area-inset-top) var(--gap) 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-weight: 650;
    color: var(--text);
    text-decoration: none;
    white-space: nowrap;
    flex: 0 0 auto;
}

/* The app's own icon, not a glyph on a tile. No background and no radius: icon.svg already carries
   its rounded ground, and rounding it again here would clip the corners twice. */
.brand-mark {
    display: block;
    flex: 0 0 auto;
    width: 1.65rem;
    height: 1.65rem;
}

.brand-text { font-size: 0.95rem; }

.topnav {
    display: flex;
    align-items: center;
    gap: 0.15rem;
    /* Takes the leftover width, and — the part that actually prevents the clipping — is allowed to
       be narrower than its own content. */
    flex: 1 1 auto;
    min-width: 0;
    justify-content: center;
    overflow-x: auto;
    scrollbar-width: none;
}

.topnav::-webkit-scrollbar { display: none; }

.topnav-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.7rem;
    /* Pill, not rounded rect: it reads as a control at small sizes where a 10px radius looks square. */
    border-radius: 999px;
    color: var(--text-muted);
    text-decoration: none;
    white-space: nowrap;
}

.topnav-item:hover { background: var(--surface-2); color: var(--text); }
.topnav-item.active { background: var(--surface-2); color: var(--accent-text); font-weight: 600; }

.account { display: flex; align-items: center; gap: 0.5rem; flex: 0 0 auto; }

/*
  The signed-in account is the avatar and nothing else; the name and sign-out sit in a popover
  behind it. That is what keeps the strip fitting on a phone — and unlike simply hiding the button
  on narrow screens, sign-out stays reachable. If the app grows a settings page that offers sign-out
  as well, hiding the button on narrow screens becomes a reasonable alternative.

  A native <details> rather than a scripted menu: focusable, keyboard-operable and Escape-closable
  for free. The one thing given up is closing on an outside click.
*/
.account-menu {
    position: relative;
    flex: 0 0 auto;
}

.account-menu > summary {
    display: grid;
    place-items: center;
    cursor: pointer;
    border-radius: 50%;
    /* Both this and the two pseudo-element rules below are needed: WebKit uses its own
       pseudo-element for the disclosure triangle. */
    list-style: none;
}

.account-menu > summary::-webkit-details-marker { display: none; }
.account-menu > summary::marker { content: ""; }
.account-menu > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.account-panel {
    position: absolute;
    top: calc(100% + 0.45rem);
    /* Anchored to the right edge so it opens inward and cannot itself overflow the viewport. */
    right: 0;
    z-index: 40;
    min-width: 11rem;
    display: grid;
    gap: 0.5rem;
    padding: 0.75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.account-who {
    font-size: 0.85rem;
    color: var(--text-muted);
    overflow-wrap: anywhere;
}

/* ----------------------------------------------------------------- avatar */

/* The initials sit underneath the image, which is how the Gravatar fallback works with no error
   handling: `d=blank` returns a transparent PNG, so the initials show through. See Avatar.razor. */
.avatar {
    position: relative;
    flex: 0 0 auto;
    width: var(--avatar-size, 2rem);
    height: var(--avatar-size, 2rem);
    border-radius: 50%;
    overflow: hidden;
    background: var(--surface-2);
    /* A ring rather than a border: a border would shrink the content box and misalign the initials. */
    box-shadow: inset 0 0 0 1px var(--border);
    display: grid;
    place-items: center;
    user-select: none;
}

.avatar-initials {
    /* Scales with the circle so one component works in a topbar and on a profile page. */
    font-size: calc(var(--avatar-size, 2rem) * 0.42);
    font-weight: 650;
    line-height: 1;
    color: var(--text-muted);
    letter-spacing: 0.02em;
}

.avatar-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* No background: a transparent Gravatar response must let the initials through. */
    background: transparent;
}

/* Shed in order of least value, and never the avatar or the links themselves: first the wordmark,
   then — only on a genuinely tiny screen — the nav labels, leaving the icons to carry navigation. */
@media (max-width: 35rem) {
    .topbar { gap: 0.35rem; padding-inline: 0.6rem; }
    .brand-text { display: none; }
    .topnav-item { padding: 0.35rem 0.55rem; font-size: 0.85rem; }
}

@media (max-width: 22rem) {
    .topnav-label { display: none; }
    .topnav-item { padding: 0.35rem 0.6rem; }
    .topnav-icon { font-size: 1.15rem; }
}

/* ----------------------------------------------------------------- buttons */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.5rem 0.9rem;
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--text);
    font: inherit;
    font-weight: 550;
    text-decoration: none;
    cursor: pointer;
    /* Below this, iOS Safari zooms the page on tap. */
    min-height: 2.5rem;
}

.btn:hover:not(:disabled) { filter: brightness(0.97); }
.btn:disabled { opacity: 0.55; cursor: default; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.btn.primary { background: var(--accent); color: var(--accent-contrast); }
.btn.ghost { background: transparent; border-color: var(--border); }
.btn.danger { color: var(--danger); border-color: var(--danger); }
.btn.small { padding: 0.3rem 0.6rem; min-height: 2rem; font-size: 0.875rem; }

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

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: var(--gap);
}

.card-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.75rem; }

.card-list .card { display: flex; align-items: flex-start; gap: var(--gap); }

.card-main { flex: 1; min-width: 0; }
.card-title { margin: 0 0 0.25rem; }
.card-body { margin: 0 0 0.25rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.card-actions { display: flex; gap: 0.35rem; flex-shrink: 0; }

/* ----------------------------------------------------------------- forms */

.form { display: grid; gap: var(--gap); }

.field { display: grid; gap: 0.3rem; }
.field > span { font-weight: 550; font-size: 0.9rem; }

.input {
    width: 100%;
    padding: 0.55rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
    /* 16px minimum, or iOS Safari zooms on focus. */
    font: inherit;
}

.input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }

textarea.input { resize: vertical; }

.form-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }

.validation-message { color: var(--danger); font-size: 0.85rem; }
.invalid { border-color: var(--danger); }

.alert {
    padding: 0.7rem var(--gap);
    border-radius: var(--radius);
    border: 1px solid transparent;
}

.alert.error { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
.alert.warn { background: var(--warn-bg); border-color: var(--warn); color: var(--text); display: grid; gap: 0.15rem; }
.alert.info { background: var(--surface-2); border-color: var(--border); color: var(--text-muted); font-size: 0.9rem; }

/* ----------------------------------------------------------------- status */

.centered-status {
    display: grid;
    place-items: center;
    gap: 0.75rem;
    padding: 3rem 1rem;
    text-align: center;
    color: var(--text-muted);
}

.spinner {
    width: 2rem;
    height: 2rem;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 3s; }
    *, *::before, *::after { transition-duration: 0.01ms !important; }
}

/* --------------------------------------------- boot + error UI (framework) */

/* Shown by index.html before the WebAssembly runtime starts; the framework drives the variables. */
.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto 1rem;
}

.loading-progress circle {
    fill: none;
    stroke: var(--surface-2);
    stroke-width: 0.6rem;
    transform-origin: 50% 50%;
    transform: rotate(-90deg);
}

.loading-progress circle:last-child {
    stroke: var(--accent);
    stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
    transition: stroke-dasharray 0.05s ease-in-out;
}

.loading-progress-text {
    text-align: center;
    color: var(--text-muted);
}

.loading-progress-text:after {
    content: var(--blazor-load-percentage-text, "Loading");
}

#blazor-error-ui {
    display: none;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 1000;
    padding: 0.7rem var(--gap);
    background: #ffe9a8;
    color: #16202a;
    box-shadow: 0 -1px 4px rgb(0 0 0 / 20%);
}

#blazor-error-ui .dismiss { float: right; cursor: pointer; }

/* ----------------------------------------------------------------- spawnpoint */

/* A state dot: one glyph that means the same thing on every list. `busy` pulses so "Starting" is
   visibly in motion rather than a differently coloured "stopped". */
.dot {
    display: inline-block;
    width: 0.65rem;
    height: 0.65rem;
    border-radius: 50%;
    background: var(--text-muted);
    flex: 0 0 auto;
    vertical-align: middle;
}

.dot.on { background: var(--ok); }
.dot.off { background: var(--border); box-shadow: inset 0 0 0 1px var(--text-muted); }
.dot.bad { background: var(--danger); }
.dot.unknown { background: transparent; box-shadow: inset 0 0 0 2px var(--text-muted); }
.dot.busy { background: var(--busy); animation: pulse 1.2s ease-in-out infinite; }

@keyframes pulse { 50% { opacity: 0.35; } }

@media (prefers-reduced-motion: reduce) { .dot.busy { animation: none; } }

.badge {
    display: inline-block;
    padding: 0.05rem 0.5rem;
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: 0.78rem;
    font-weight: 550;
    white-space: nowrap;
}

.badge.on { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
.badge.bad { background: var(--danger-bg); color: var(--danger); }

.danger-text { color: var(--danger); }

code, .code { font-family: ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace; }
code { font-size: 0.9em; background: var(--surface-2); padding: 0 0.3em; border-radius: 4px; overflow-wrap: anywhere; }
textarea.code { font-size: 0.85rem; line-height: 1.4; tab-size: 2; }

/* Game cards: the whole card is the link. */
.game-card { padding: 0; }
.game-link {
    display: flex;
    align-items: center;
    gap: var(--gap);
    padding: var(--gap);
    color: inherit;
    text-decoration: none;
    width: 100%;
    border-radius: var(--radius);
}
.game-link:hover { background: var(--surface-2); }
.game-icon { font-size: 2rem; line-height: 1; flex: 0 0 auto; }
.game-icon-small { font-size: 1.1rem; line-height: 1; }
.game-main { display: grid; gap: 0.2rem; min-width: 0; flex: 1; }
.game-desc { overflow-wrap: anywhere; }
.game-counts { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.chevron { color: var(--text-muted); font-size: 1.5rem; flex: 0 0 auto; }

/* Slot rows: port on the left, server in the middle, actions on the right. On a phone the
   actions drop under the summary rather than squeezing it. */
.card-list .card.slot { align-items: center; flex-wrap: wrap; }
.slot-port {
    display: grid;
    gap: 0.1rem;
    min-width: 5.5rem;
    flex: 0 0 auto;
}
.slot-port-number { font-weight: 650; font-variant-numeric: tabular-nums; }
.slot .card-main { flex: 1 1 12rem; }
.slot .card-actions { flex: 0 0 auto; }

.server-summary { display: flex; align-items: flex-start; gap: 0.5rem; min-width: 0; }
.server-summary .dot { margin-top: 0.45rem; }
.server-summary-main { display: grid; gap: 0.1rem; min-width: 0; }
.server-name { font-weight: 600; color: var(--text); text-decoration: none; overflow-wrap: anywhere; }
.server-name:hover { color: var(--accent-text); }

.server-actions { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; }

/* Slot editor */
.slot-rows { display: grid; gap: 0.5rem; }
.slot-row {
    display: grid;
    grid-template-columns: 8rem 1fr auto;
    gap: 0.5rem;
    align-items: end;
}
@media (max-width: 35rem) {
    .slot-row { grid-template-columns: 1fr 1fr; }
    .slot-row .btn { grid-column: 1 / -1; }
}
.fill-range { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.small-input { width: 6rem; padding: 0.3rem 0.5rem; min-height: 2rem; }

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); }
@media (max-width: 35rem) { .field-row { grid-template-columns: 1fr; } }

.check { display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; }
.check input { margin-top: 0.3rem; }

.config-help summary { cursor: pointer; color: var(--accent-text); font-size: 0.9rem; }
.kv { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.75rem; margin: 0.5rem 0; font-size: 0.9rem; }
.kv dt { font-weight: 500; }
.kv dd { margin: 0; color: var(--text-muted); }
@media (max-width: 35rem) {
    .kv { grid-template-columns: 1fr; gap: 0; }
    .kv dd { margin-bottom: 0.4rem; }
}

/* Server detail */
.server-heading { display: grid; gap: 0.15rem; min-width: 0; }
.server-heading h1 { display: flex; align-items: center; gap: 0.5rem; overflow-wrap: anywhere; }
.crumb { text-decoration: none; }
.crumb:hover { text-decoration: underline; }

.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
    gap: 0.75rem;
    margin: 0;
}
.stat-grid dt { font-size: 0.8rem; color: var(--text-muted); }
.stat-grid dd { margin: 0; overflow-wrap: anywhere; }
.stat-big { font-size: 1.4rem; font-weight: 650; font-variant-numeric: tabular-nums; }

.player-list { list-style: none; margin: 0.75rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.35rem 1rem; }
.player-list li { display: flex; align-items: center; gap: 0.35rem; }

/* The console. Newest line first in the DOM, column-reverse on screen, so the browser keeps the
   view anchored at the bottom as lines arrive. Dark in both themes: terminal output reads best on
   a dark ground and the images print ANSI-free text. */
.console {
    display: flex;
    flex-direction: column-reverse;
    height: 24rem;
    max-height: 60vh;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 0.6rem 0.75rem;
    border-radius: var(--radius-sm);
    background: #0f1419;
    color: #d7dee6;
    font-family: ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
    font-size: 0.8rem;
    line-height: 1.45;
    scrollbar-width: thin;
}
.console-empty { margin: auto 0 0; }
.console-line { display: flex; gap: 0.6rem; overflow-wrap: anywhere; white-space: pre-wrap; }
.console-time { color: #6b7d8f; flex: 0 0 auto; user-select: none; }
.console-text { min-width: 0; }
.console-line.stderr .console-text { color: #f2b8b5; }
.console-line.meta .console-text { color: #a9b8ff; }

.console-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.console-toolbar > span:first-child { display: inline-flex; align-items: center; gap: 0.5rem; }
.console-buttons { display: flex; gap: 0.35rem; }

/* Whether the hub is connected: green when live, hollow when not. */
.live-dot { display: inline-block; width: 0.55rem; height: 0.55rem; border-radius: 50%; background: var(--ok); vertical-align: middle; }
.live-dot.off { background: transparent; box-shadow: inset 0 0 0 2px var(--text-muted); }
.alert.info .live-dot { margin-right: 0.4rem; }

.command-row { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
.command-row .input { flex: 1; min-width: 0; }

details.settings > summary { cursor: pointer; list-style: none; }
details.settings > summary::-webkit-details-marker { display: none; }
details.settings > summary h2 { display: inline; }
details.settings > summary::after { content: " \25B8"; color: var(--text-muted); }
details.settings[open] > summary::after { content: " \25BE"; }
details.settings[open] > summary { margin-bottom: var(--gap); }

.danger-zone { margin-top: var(--gap); padding-top: var(--gap); border-top: 1px solid var(--border); display: grid; gap: 0.5rem; }
.danger-zone h3 { margin: 0; font-size: 1rem; color: var(--danger); }
.btn.danger-solid { background: var(--danger); color: #fff; }
