/* ---------- Meridian design tokens ---------- */
:root {
    --bg: #0a0e13;
    --surface: #121a22;
    --surface-2: #1a2530;
    --surface-3: #212f3c;
    --border: #47617a;
    --text: #dce6ec;
    --text-muted: #7891a3;
    --accent: #3fd0e0;
    --accent-strong: #8fe9f2;
    --on-accent: #05171a;
    --warning: #f5a623;
    --critical: #ff5468;
    --success: #5fd897;
    /* Stripe's own brand indigo/purple (#635BFF) — used only for the
       Admin -> Users "Payments" pill, deliberately not folded into the
       app's own semantic warning/critical/success palette, so it reads as
       "this is about Stripe" specifically rather than a generic status color. */
    --stripe-purple: #635bff;
    --radius: 6px;
    --font-display: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
    --font-mono: 'IBM Plex Mono', 'Consolas', monospace;
}

* {
    box-sizing: border-box;
}

html, body {
    height: 100%;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-display);
    font-size: 15px;
    line-height: 1.5;
    font-variant-numeric: tabular-nums;
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
    font-weight: 700;
    letter-spacing: 0.01em;
    margin: 0 0 0.5em;
}

h1:focus {
    outline: none;
}

.page-subtitle {
    color: var(--text-muted);
    font-size: 13.5px;
    margin: -0.75em 0 1.25em;
    max-width: 65ch;
}

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

/* ---------- form controls ---------- */
label {
    display: block;
    font-size: 12.5px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 5px;
}

input, select, textarea {
    font-family: var(--font-mono);
    font-size: 14px;
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: calc(var(--radius) - 2px);
    padding: 8px 10px;
    width: 100%;
}

input:focus, select:focus, textarea:focus {
    outline: 2px solid var(--accent);
    outline-offset: -1px;
    border-color: var(--accent);
}

input[type=checkbox] {
    width: auto;
}

/* Every on/off settings checkbox in the app opts into this — a real slider
   look instead of a plain browser checkbox, for a more deliberate/
   professional feel on preference toggles specifically (row-selection
   checkboxes, if this app ever grows any, should stay plain checkboxes —
   a slider reads as "setting," not "selected"). Still a genuine
   <input type="checkbox"> underneath (appearance: none only removes the
   native visual, not the semantics/keyboard behavior), so screen readers,
   :checked/:disabled/:focus-visible, and @bind/@onchange all keep working
   exactly as before — only the class name is new at every call site. */
input[type=checkbox].toggle-switch {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 38px;
    min-width: 38px;
    height: 21px;
    padding: 0;
    border-radius: 999px;
    background: var(--surface-3);
    border: 1px solid var(--border);
    cursor: pointer;
    vertical-align: middle;
    transition: background-color .15s ease, border-color .15s ease;
}

input[type=checkbox].toggle-switch::after {
    content: "";
    position: absolute;
    top: 1px;
    left: 1px;
    width: 17px;
    height: 17px;
    border-radius: 50%;
    background: var(--text-muted);
    transition: transform .15s ease, background-color .15s ease;
}

input[type=checkbox].toggle-switch:checked {
    background: var(--accent);
    border-color: var(--accent);
}

input[type=checkbox].toggle-switch:checked::after {
    transform: translateX(17px);
    background: var(--on-accent);
}

input[type=checkbox].toggle-switch:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

input[type=checkbox].toggle-switch:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

input[type=range] {
    border: none;
    background: transparent;
    padding: 6px 0;
    accent-color: var(--accent);
}

.mono-input {
    font-family: var(--font-mono);
    width: auto;
    flex: 1 1 auto;
}

.join-session-row {
    display: flex;
    gap: 8px;
    align-items: center;
    max-width: 260px;
    margin-bottom: 24px;
}

.field {
    margin-bottom: 14px;
}

.field-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 12px;
}

.invalid {
    outline: 1px solid var(--critical);
}

.validation-message {
    color: var(--critical);
    font-size: 12.5px;
    margin-top: 4px;
}

.field-hint {
    font-size: 11px;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--text-muted);
}

.field-hint-error {
    display: block;
    color: var(--critical);
    font-size: 12px;
    margin-top: 4px;
}

/* ---------- buttons ---------- */
button, .btn {
    /* A real <button> is inline-block by default, so this only matters for
       <a class="btn">-as-button links — without it they stay display:inline,
       where e.g. .auth-submit's width:100% has no effect at all. */
    display: inline-block;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 14px;
    letter-spacing: 0.02em;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text);
    border-radius: calc(var(--radius) - 2px);
    padding: 8px 16px;
    cursor: pointer;
    text-decoration: none;
    transition: border-color 0.12s, color 0.12s, background 0.12s;
}

button:hover, .btn:hover {
    border-color: var(--accent);
    color: var(--accent-strong);
}

button:disabled, .btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.btn-primary {
    background: var(--accent);
    color: var(--on-accent);
    border-color: var(--accent);
}

.btn-primary:hover {
    color: var(--on-accent);
    background: var(--accent-strong);
    border-color: var(--accent-strong);
}

.btn-danger {
    color: var(--critical);
}

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

.btn-sm {
    padding: 5px 10px;
    font-size: 12.5px;
}

/* ---------- surfaces ---------- */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 16px;
}

.assignment-card + .assignment-card {
    margin-top: 16px;
}

.panel-title {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 0 0 12px;
}

/* ---------- data table ---------- */
table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-mono);
    font-size: 13.5px;
}

th {
    text-align: left;
    font-family: var(--font-display);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-muted);
    padding: 8px 10px;
    border-bottom: 1px solid var(--border);
}

td {
    padding: 9px 10px;
    border-bottom: 1px solid var(--border);
}

tr:hover td {
    background: var(--surface-2);
}

td.actions {
    text-align: right;
    white-space: nowrap;
}

td.actions button {
    margin-left: 6px;
}

/* Consolidated row-action sizing (World Editor + Admin list pages) — every
   Edit/Delete/Publish-style button gets the same footprint regardless of its
   label length, so a row never looks lopsided next to another page's. Order
   in markup should always be extra-action(s), then Edit, then Delete — with
   td.actions already right-aligned, that puts Delete outermost/rightmost,
   matching a real "most destructive = furthest from accidental misclicks
   near other content" convention, not just visual habit. rem-based so it
   still respects the user's own text-zoom/font-size, not just viewport width. */
.btn-row-action {
    min-width: 4.5rem;
    text-align: center;
}

/* ---------- world editor CRUD forms ---------- */
.toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
    flex-wrap: wrap;
}

/* Groups the filter input with the filter-popout toggle as one visual unit
   on the toolbar's start side, so the lone "+ New X" button is always the
   only thing on the end side (space-between keeps it pinned to the far
   right regardless of how many filter controls sit on the other side). */
.toolbar-filters {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1 1 auto;
    min-width: 0;
}

.filter-input {
    flex: 1 1 240px;
    max-width: 320px;
    font-family: var(--font-mono);
    font-size: 13px;
    padding: 7px 10px;
}

/* Confirmed 2026-09-02: .filter-input's own font-metrics-derived height
   (13px font, 7px padding) came out ~3px shorter than .filter-toggle-btn's
   (a plain .btn: 14px font, 8px padding) — same row, visibly uneven.
   Pinning both to one explicit height (box-sizing:border-box is a global
   reset, so this is authoritative regardless of padding/border) settles it
   without touching every .btn/input elsewhere in the app. */
.toolbar-filters .filter-input,
.toolbar-filters .filter-toggle-btn {
    height: 34px;
}

/* QuotaPill.razor — sits last in .toolbar-filters, after the filter input/popout
   and, on pages that have one, the "Browse Library" button. flex-shrink:0 so it
   never gets squeezed as the filter input's own flex-basis competes for space. */
.quota-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

.quota-pill-count {
    font-family: var(--font-mono);
    font-size: 12.5px;
    color: var(--text-muted);
}

/* QuotaLockBanner.razor — sits between .toolbar and the model's own table,
   spanning the same full width as the table (a plain block-level div, not
   confined to .toolbar-filters' width). */
.quota-lock-banner {
    margin-bottom: 16px;
}

/* ---------- filter popout (enum-like multiselect filters) ---------- */
.filter-popout-wrap {
    position: relative;
    flex: 0 0 auto;
}

/* <summary> renders as a real .btn (see FilterPopout.razor) — same
   list-marker-hiding + custom chevron trick MainLayout's account menu
   already uses for its own <details>/<summary> popout. */
.filter-toggle-btn {
    list-style: none;
}

.filter-toggle-btn::-webkit-details-marker {
    display: none;
}

.filter-toggle-btn::after {
    content: ' \25BE';
}

.filter-toggle-badge {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--on-accent);
    font-size: 11px;
    font-weight: 700;
}

.filter-popout {
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 4px;
    min-width: 260px;
    max-width: 320px;
    /* Tall enough that an open combobox dropdown (up to ~10 rows, see
       .ms-dropdown below) doesn't create a second nested scroll region
       inside an already-scrolling popout — viewport-relative so it still
       behaves on a short window instead of a fixed px cap fighting with
       legitimately-tall content. */
    max-height: 80vh;
    overflow-y: auto;
    padding: 10px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
    z-index: 20;
}

.filter-popout-group-title {
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 6px;
}

.filter-popout-clear {
    align-self: flex-start;
}

/* Per-group filterable multiselect — st.multiselect's own shape: selected
   values render as removable pills inside the input itself, typing filters
   the dropdown of remaining options. Keeps a 20-option field to one compact
   control instead of a wall of checkboxes. */
.ms-combobox {
    position: relative;
}

.ms-input-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    min-height: 34px;
    padding: 4px 6px;
    /* Matches every plain input's own background (input,select,textarea
       above) — previously var(--surface), a shade darker than a plain
       input ever gets against the same card, which stood out once
       MultiSelect started sitting directly next to real <input>s in a
       form's own .field-row instead of only inside the filter popout's
       own already-dark panel. */
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: calc(var(--radius) - 2px);
    cursor: text;
}

.ms-input-row:focus-within {
    border-color: var(--accent);
}

.ms-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 4px 2px 8px;
    background: var(--surface-3);
    border-radius: 999px;
    font-size: 12px;
    color: var(--text);
    white-space: nowrap;
}

.ms-pill-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--text-muted);
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    border-radius: 50%;
}

.ms-pill-remove:hover {
    background: var(--border);
    color: var(--text);
}

.ms-search-input {
    flex: 1 1 60px;
    min-width: 60px;
    border: none;
    background: transparent;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 13px;
    padding: 2px 4px;
}

.ms-search-input:focus {
    outline: none;
}

.ms-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 2px;
    /* ~10 rows before scrolling (.ms-dropdown-option's own padding/font-size
       puts each row at ~30px) — previously 180px (~6 rows), cramped enough
       that opening a dropdown inside the already-scrolling .filter-popout
       felt like scrolling within scrolling. */
    max-height: 320px;
    overflow-y: auto;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: calc(var(--radius) - 2px);
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
    z-index: 21;
}

.ms-dropdown-option {
    padding: 6px 10px;
    font-size: 13px;
    cursor: pointer;
}

.ms-dropdown-option:hover {
    background: var(--surface-3);
}

.ms-dropdown-empty {
    padding: 6px 10px;
    font-size: 13px;
    color: var(--text-muted);
}

.form-card {
    margin-bottom: 20px;
}

/* Instructor per-scenario-slot SimConfig overrides (Organization/Assignments.razor) */
.sim-overrides-card {
    margin: 4px 0 14px;
    background: var(--surface-2);
}

.sim-overrides-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 10px 16px;
}

.sim-overrides-grid .field {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 0;
    font-size: 12.5px;
}

.sim-overrides-grid .field input[type="number"] {
    width: 80px;
    text-align: right;
}

/* The usual shape is a wrapping <div class="field checkbox-field"><label>...
   — but a checkbox living inside an existing grid of <label class="field">
   cells (e.g. Organization/Assignments.razor's per-scenario Sim Settings
   grid) can't add a wrapper without breaking the grid's cell count, so
   label.checkbox-field (the class directly on the label) is supported too. */
.checkbox-field label, label.checkbox-field {
    display: flex;
    align-items: center;
    gap: 8px;
    text-transform: none;
    letter-spacing: normal;
    font-size: 13.5px;
    color: var(--text);
}

.runways-title {
    margin-top: 20px;
}

.runway-row {
    align-items: end;
    padding: 10px;
    margin-bottom: 8px;
    background: var(--surface-2);
    border-radius: calc(var(--radius) - 2px);
}

/* A row's "Remove" button is a bare child (no .field label above it), so
   align-items:end lands it against the ROW's track bottom — which sits
   14px below the sibling .field's own content, since that 14px is the
   .field's own margin-bottom counted into the track height. Matching that
   margin on the button keeps its content bottom flush with the inputs. */
.runway-row > button {
    margin-bottom: 14px;
}

/* One physical runway, both ends — three stacked .field-row grids (End 1,
   End 2, Width/surface/lighting) inside one card, replacing the old
   single-row-per-direction layout. */
.runway-row-group {
    padding: 10px;
    margin-bottom: 8px;
    background: var(--surface-2);
    border-radius: calc(var(--radius) - 2px);
}

.runway-row-group .field-row {
    margin-bottom: 10px;
}

.runway-end-label {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    margin-bottom: 6px;
}

.form-actions {
    display: flex;
    gap: 8px;
    margin-top: 18px;
}

.form-alert {
    margin-top: 14px;
}

/* ---------- chips / badges ---------- */
.chip {
    display: inline-block;
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    padding: 2px 8px;
    border-radius: 20px;
    background: var(--surface-3);
    color: var(--text-muted);
}

.chip-error {
    background: rgba(255, 84, 104, 0.12);
    color: var(--critical);
}

.chip-success {
    background: rgba(95, 216, 151, 0.12);
    color: var(--success);
}

.chip-accent {
    background: rgba(63, 208, 224, 0.15);
    color: var(--accent-strong);
}

/* Generic pill badges, same shape as .badge-published — success/warning/danger
   variants for anywhere a status needs a color beyond "published". */
.badge-success {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(95, 216, 151, 0.15);
    color: var(--success);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.badge-warning {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(245, 166, 35, 0.15);
    color: var(--warning);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.badge-danger {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(255, 84, 104, 0.15);
    color: var(--critical);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Admin -> Users "Payments" column — an active/trialing subscriber's pill,
   same shape as .badge-success/.badge-danger but in Stripe's own brand
   color so it reads as "this is about Stripe" at a glance. */
.badge-stripe {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(99, 91, 255, 0.15);
    color: var(--stripe-purple);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* ---------- alerts ---------- */
.alert {
    border-radius: var(--radius);
    padding: 10px 14px;
    font-size: 13.5px;
    border: 1px solid transparent;
}

.alert-error, .alert-danger {
    background: rgba(255, 84, 104, 0.12);
    border-color: rgba(255, 84, 104, 0.4);
    color: var(--critical);
}

.alert-success {
    background: rgba(95, 216, 151, 0.12);
    border-color: rgba(95, 216, 151, 0.4);
    color: var(--success);
}

/* Full-sentence status pill (Home page's "no known bugs"/"no features
   requested" empty states) — a wide pill-shaped banner, not the tiny
   tag-sized .badge-success above, since this wraps a whole sentence
   including an inline link straight to the feedback form. */
.status-pill {
    display: block;
    padding: 12px 20px;
    border-radius: 999px;
    font-size: 13.5px;
    line-height: 1.5;
}

.status-pill-success {
    background: rgba(95, 216, 151, 0.12);
    color: var(--success);
}

.status-pill a {
    color: inherit;
    font-weight: 700;
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Ownership pill (OwnershipBadge.razor) — three tints of blue/purple, one
   per OwnershipType, deliberately a distinct hue family from the semantic
   success/warning/danger badges above (this isn't a status, it's a
   category). Built-in reuses the app's own accent (cyan-blue) since it's
   the "most first-party" tier; Organization and Owned step progressively
   toward violet. */
.badge-owner {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.badge-owner-builtin {
    background: rgba(63, 208, 224, 0.15);
    color: var(--accent-strong);
}

.badge-owner-organization {
    background: rgba(129, 140, 248, 0.16);
    color: #b0b8fc;
}

.badge-owner-personal {
    background: rgba(192, 132, 252, 0.16);
    color: #d4b2fb;
}

.badge-published {
    display: inline-block;
    margin-left: 6px;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(95, 216, 151, 0.15);
    color: var(--success);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.alert-warning {
    background: rgba(245, 166, 35, 0.12);
    border-color: rgba(245, 166, 35, 0.4);
    color: var(--warning);
}

.alert a, .alert button.link-button {
    color: inherit;
    font-weight: 600;
    text-decoration: underline;
}

.link-button {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    cursor: pointer;
}

/* CSS-only hide, not @if removal — see Login.razor's resend-confirmation
   form comment for why: a static-SSR named form has to stay structurally
   present in every render or its own postback gets rejected. */
.auth-hidden {
    display: none;
}

.text-danger {
    display: block;
    color: var(--critical);
    font-size: 12px;
    margin-top: 4px;
}

/* ---------- auth (login/register) card contents ---------- */
.auth-heading {
    font-size: 20px;
    font-weight: 700;
    margin: 0 0 4px;
}

.auth-subheading {
    color: var(--text-muted);
    font-size: 13px;
    margin: 0 0 22px;
}

.auth-form .field {
    margin-bottom: 16px;
}

.auth-checkbox-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: -4px 0 18px;
    font-size: 13px;
    color: var(--text-muted);
}

/* Added 2026-09-06 — the "by continuing you agree..." disclaimer under the
   Google/Microsoft buttons on Register.razor. No checkbox moment exists in
   that one-click flow, so this is the passive-agreement notice instead. */
.auth-legal-notice {
    margin: 8px 0 0;
    font-size: 12px;
    text-align: center;
    color: var(--text-muted);
}

/* PasskeySubmit.razor renders a real <button> plus this sibling custom
   element (form-associated, purely behavioral — see PasskeySubmit.razor.js,
   it never renders any visual content of its own). Left in normal flow, it
   still counts as a second flex item wherever the button sits in a flex row
   (e.g. .auth-checkbox-row's own `gap`), silently shrinking the button by
   the gap width even though there's nothing to actually see here — display:
   none removes it from layout without affecting its form-association/JS. */
passkey-submit {
    display: none;
}

.auth-checkbox-row input {
    width: auto;
}

.auth-submit {
    width: 100%;
    padding: 11px;
    font-size: 14px;
}

.auth-submit:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.auth-submit-hint {
    display: none;
    margin-top: 8px;
    text-align: center;
    font-size: 12px;
    color: var(--text-muted);
}

.auth-divider {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 22px 0;
    color: var(--text-muted);
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

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

.auth-oauth-row {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.auth-oauth-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    padding: 10px;
    font-size: 13.5px;
    font-weight: 600;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: calc(var(--radius) - 2px);
    color: var(--text);
    cursor: pointer;
}

.auth-oauth-btn:hover {
    background: var(--surface-3);
    border-color: var(--text-muted);
}

.auth-oauth-btn svg {
    flex: 0 0 auto;
}

.auth-footer {
    margin-top: 22px;
    padding-top: 18px;
    border-top: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: 8px;
    text-align: center;
    font-size: 12.5px;
    color: var(--text-muted);
}

.auth-footer a {
    color: var(--accent);
    text-decoration: none;
}

.auth-footer a:hover {
    text-decoration: underline;
}

.auth-turnstile {
    margin: 4px 0 18px;
    display: flex;
    justify-content: center;
}

.empty-state {
    color: var(--text-muted);
    text-align: center;
    padding: 40px 20px;
    font-size: 14px;
}

/* LoadingSpinner.razor — a section that's still fetching its own data reads
   as "working," not frozen. Same padding/color rhythm as .empty-state so
   swapping between the two (loading -> real content, or loading -> "nothing
   here") doesn't visibly jump. */
.loading-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    color: var(--text-muted);
    padding: 40px 20px;
    font-size: 14px;
}

.spinner {
    display: inline-block;
    width: 18px;
    height: 18px;
    border: 2.5px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spinner-rotate 0.7s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
    .spinner {
        animation-duration: 1.8s;
    }
}

::-webkit-scrollbar {
    width: 8px;
    height: 8px;
}

::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 4px;
}

/* ---------- radar scope shell (Simulator + Replay pages) ---------- */
.sim-shell {
    display: grid;
    grid-template-columns: 1fr 320px;
    gap: 16px;
    height: calc(100vh - 54px - 48px);
    min-height: 600px;
}

.radar-panel {
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    min-width: 0;
}

.radar-toolbar {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 8px 12px;
    border-bottom: 1px solid var(--border);
    flex: 0 0 auto;
}

.sim-clock {
    font-family: var(--font-mono);
    font-size: 15px;
    font-weight: 700;
    color: var(--accent-strong);
    margin-left: auto;
}

.conn-status {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 3px 8px;
    border-radius: 20px;
}

.conn-status.connected {
    color: var(--success);
    background: rgba(95, 216, 151, 0.14);
}

.conn-status.disconnected {
    color: var(--warning);
    background: rgba(245, 166, 35, 0.14);
}

#radar-canvas {
    flex: 1 1 auto;
    width: 100%;
    height: 100%;
    min-height: 0;
    display: block;
    cursor: crosshair;
}

.console-panel {
    display: flex;
    flex-direction: column;
    min-width: 0;
    overflow-y: auto;
}

.strip-bay {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 20px;
}

.strip {
    display: grid;
    grid-template-columns: 1.4fr 0.8fr 0.6fr 0.6fr 0.8fr;
    gap: 6px;
    align-items: center;
    font-family: var(--font-mono);
    font-size: 12.5px;
    text-align: left;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: calc(var(--radius) - 2px);
    padding: 7px 10px;
    cursor: pointer;
}

.strip:hover {
    border-color: var(--accent);
    background: var(--surface-2);
}

.strip.selected {
    border-left-color: var(--accent-strong);
    background: var(--surface-2);
    outline: 1px solid var(--accent-strong);
}

.strip.conflict {
    border-left-color: var(--critical);
}

.strip.conflict .strip-callsign {
    color: var(--critical);
}

.strip-callsign {
    font-weight: 700;
    color: var(--text);
}

.strip-type, .strip-alt, .strip-speed {
    color: var(--text-muted);
}

.strip-ctl {
    font-size: 10.5px;
    color: var(--text-muted);
    text-transform: uppercase;
    text-align: right;
}

/* ---------- notices ---------- */
/* Severity ramp: blue (information) -> gray (maintenance) -> amber (warning) -> red (disruption). */
.notice-card {
    border: 1px solid var(--border);
    border-left: 4px solid var(--accent);
    border-radius: var(--radius);
    padding: 12px 16px;
    background: var(--surface);
}

.notice-card-information { border-left-color: #0074d9; background: rgba(0, 116, 217, 0.06); }
.notice-card-maintenance { border-left-color: #546e7a; background: rgba(84, 110, 122, 0.06); }
.notice-card-warning { border-left-color: var(--warning); background: rgba(245, 166, 35, 0.08); }
.notice-card-disruption { border-left-color: var(--critical); background: rgba(255, 84, 104, 0.08); }

.notice-card-header {
    display: flex;
    align-items: center;
    gap: 8px;
}

.notice-card-title {
    font-weight: 700;
}

.notice-card-badge {
    margin-left: auto;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    padding: 2px 8px;
    border-radius: 20px;
}

.notice-card-badge-published {
    color: var(--success);
    background: rgba(95, 216, 151, 0.14);
}

.notice-card-badge-hidden {
    color: var(--text-muted);
    background: var(--surface-2);
}

.notice-card-body {
    margin: 8px 0 6px;
    font-size: 13.5px;
    white-space: pre-wrap;
}

.notice-card-meta {
    font-size: 11px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* ---------- admin page section divider (e.g. Admin/Feedback.razor's Features/Bugs/Reports split) ---------- */
.admin-section-divider {
    border: none;
    border-top: 1px solid var(--border);
    margin: 32px 0;
}

/* ---------- roadmap (RoadmapItemCard.razor) ---------- */
.roadmap-item + .roadmap-item {
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--border);
}

.roadmap-item-title {
    font-weight: 600;
    font-size: 13.5px;
    display: flex;
    align-items: center;
    gap: 8px;
}

.roadmap-item-description {
    margin: 4px 0 0;
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.5;
    white-space: pre-wrap;
}

/* ---------- Blazor framework hooks (kept, restyled) ---------- */
.blazor-error-boundary {
    background: var(--critical);
    padding: 1rem 1rem 1rem 1.25rem;
    color: #fff;
    border-radius: var(--radius);
    margin: 1rem;
}

.blazor-error-boundary::after {
    content: "An error has occurred.";
}

#blazor-error-ui {
    background: var(--critical);
    color: #fff;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 0.8rem 1.25rem;
    display: none;
}

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

/* ---------- Guided tour highlight ---------- */
/* Created via raw DOM (tutorial.js), outside Blazor's render tree — scoped
   component CSS can't reach it, so this lives here instead.

   One element does double duty: box-shadow's "0 0 0 9999px" spread dims the
   whole viewport except the element's own box (sized/positioned exactly
   over the target — see tutorial.js), which stays background:transparent
   and gets a real border, so the same box is both "the see-through hole in
   the dim" and "the floating, unfilled, pulsing outline rectangle" the tour
   asks for. pointer-events stays none always, including over that box — a
   click meant for the highlighted element must reach it, not an invisible
   blocker sitting exactly on top of it (the M29-31 live walkthrough
   requires the trainee to actually click what's highlighted, not just look
   at it). Centering falls straight out of using the target's own
   getBoundingClientRect(), so it already tracks any element at any screen
   size; a resize/scroll listener (tutorial.js) keeps it glued to the target
   if the layout shifts under it while the step is still showing. */
.atc-tutorial-ring {
    position: fixed;
    z-index: 10000;
    border-radius: 10px;
    border: 3px solid var(--accent);
    background: transparent;
    pointer-events: none;
    transition: top 0.15s ease, left 0.15s ease, width 0.15s ease, height 0.15s ease;
    box-shadow: 0 0 0 9999px rgba(4, 10, 16, 0.55);
    animation: atc-tutorial-pulse 1.6s ease-in-out infinite;
}

/* Target-less "intro"-style steps — nothing to draw a hole around, so this
   just flat-dims the whole viewport instead. */
.atc-tutorial-ring.atc-tutorial-ring-full {
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: rgba(4, 10, 16, 0.55);
    animation: none;
}

@keyframes atc-tutorial-pulse {
    0%, 100% {
        box-shadow: 0 0 0 9999px rgba(4, 10, 16, 0.55), 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
        border-color: var(--accent);
    }
    50% {
        box-shadow: 0 0 0 9999px rgba(4, 10, 16, 0.55), 0 0 0 8px color-mix(in srgb, var(--accent) 0%, transparent);
        border-color: color-mix(in srgb, var(--accent) 60%, white);
    }
}

@media (prefers-reduced-motion: reduce) {
    .atc-tutorial-ring {
        animation: none;
        box-shadow: 0 0 0 9999px rgba(4, 10, 16, 0.55), 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
    }
}

/* ---------- idle-timeout warning (idle-timeout.js) ---------- */
/* Same dim-backdrop language as the tutorial ring above, plain overlay div
   instead of a spotlight cutout — there's no target element to frame here,
   the whole page is what's "paused." Built via raw DOM, outside Blazor's
   render tree, for the same reason the tutorial overlay is. */
.idle-timeout-overlay {
    position: fixed;
    inset: 0;
    z-index: 10001;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(4, 10, 16, 0.7);
}

.idle-timeout-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 24px;
    max-width: 380px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
}

.idle-timeout-card p {
    margin: 10px 0;
    font-size: 13.5px;
    line-height: 1.5;
}

/* ---------- Sign-off timer pill (sign-off-timer.js) ---------- */
/* Deliberately NOT a blocking overlay like idle-timeout's — the human here
   is presumed actively working and just needs a heads-up, not a dim-screen
   interruption. Normally a small pill sitting in the topbar's own flex row
   (#signoff-pill-slot, MainLayout.razor) — visible without covering any
   page content. The one exception is fullscreen: the browser's Fullscreen
   API hides everything outside the fullscreened element (Simulator.razor's
   own fullscreen only ever covers .sim-shell), so a topbar-anchored pill
   would be entirely invisible there — sign-off-timer.js re-homes the same
   pill directly onto the fullscreened element as a floating overlay in that
   one case instead, deliberately overlapping the radar since that's the
   only way it can be seen at all while fullscreened. */
.signoff-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 12px;
    border-radius: 999px;
    background: var(--warning);
    color: #1a1200;
    font-size: 12px;
    font-weight: 700;
    white-space: nowrap;
}

/* z-index sits below the Report dialog's .modal-overlay (9000) so the two never fight if somehow both are open at once. */
.signoff-pill-overlay {
    position: fixed;
    top: 14px;
    right: 14px;
    z-index: 8000;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
}

/* ---------- Report dialog (ReportDialog.razor) ---------- */
/* Same dim-backdrop language as the tutorial ring/idle-timeout overlay above,
   but plain Blazor markup rather than raw DOM — this one opens/closes
   entirely within a page's normal render lifecycle (see ReportDialog.razor's
   own doc comment for why it doesn't need JS the way those two do). */
.modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 9000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(4, 10, 16, 0.7);
    padding: 20px;
}

.modal-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
    width: 100%;
    max-width: 720px;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
}

.modal-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 16px 20px;
    border-bottom: 1px solid var(--border);
}

.modal-card-header .panel-title {
    margin: 0;
}

.modal-close-btn {
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
}

.modal-close-btn:hover {
    color: var(--text);
}

.modal-card-body {
    padding: 16px 20px;
    overflow-y: auto;
}

.modal-card-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    padding: 14px 20px;
    border-top: 1px solid var(--border);
}

/* The ruleset-override picker at the top of a report dialog (Simulation.razor/
   Organization/Assignments.razor) — sits above ScoreBreakdownView's own content. */
.report-ruleset-picker {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 12px;
    margin-bottom: 16px;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--border);
}

.report-ruleset-picker label {
    margin-bottom: 0;
}

.report-ruleset-picker select {
    width: auto;
}

/* ScoreBreakdownView.razor — the category/findings/metrics content rendered inside a report dialog */
.report-category-table {
    width: 100%;
}

.report-findings-list {
    margin: 8px 0 0;
    padding-left: 18px;
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--text-muted);
}

.report-findings-list strong {
    color: var(--text);
}

.report-metrics-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 20px;
    margin-top: 14px;
    padding-top: 10px;
    border-top: 1px solid var(--border);
    font-size: 12px;
    color: var(--text-muted);
}

.report-student-section {
    margin-top: 22px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}

/* ---------- 2FA authenticator QR code ---------- */
/* The PNG itself already renders on a white background (QRCoder's default),
   but a bit of padding + a border keeps it from looking like a stray image
   pasted onto the dark theme, and guarantees scan-reliable white margin
   ("quiet zone") around the modules regardless of surrounding page background. */
.authenticator-qr {
    display: block;
    background: #fff;
    padding: 12px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    margin: 8px 0;
}
