/* ---------- Meridian design tokens ---------- */
/* stylelint-disable color-no-hex, function-disallowed-list -- the token blocks are the one place raw colours live */
:root {
    color-scheme: dark;
    --bg: #0a0e13;
    --surface: #121a22;
    --surface-2: #1a2530;
    --surface-3: #212f3c;
    /* --border is the strong divider/outline; --border-subtle is for the
       quiet separation between cards/rows (see .row-list) — hierarchy comes
       from surface steps, not from every edge being equally loud. */
    --border: #34475a;
    --border-subtle: #243342;
    /* Boundary of an interactive control (input/select/textarea): >= 3:1 against the surfaces (WCAG 1.4.11). */
    --border-control: #5b7389;
    --text: #dce6ec;
    --text-muted: #8aa3b5;
    --accent: #3fd0e0;
    --accent-strong: #8fe9f2;
    --on-accent: #05171a;
    --warning: #f5a623;
    --critical: #ff5468;
    --success: #5fd897;
    --info: #4da3ff;
    --neutral-status: #8aa3b5;
    /* Text/fill that sits on a solid --critical / --warning / white-surface background. */
    --on-critical: #1a0508;
    --on-warning: #1a1200;
    --qr-surface: #ffffff;
    /* Soft tints of each role — chip/badge/hover backgrounds. Derived from the
       role color, so a theme only has to define the role itself. */
    --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
    --success-soft: color-mix(in srgb, var(--success) 14%, transparent);
    --warning-soft: color-mix(in srgb, var(--warning) 14%, transparent);
    --critical-soft: color-mix(in srgb, var(--critical) 14%, transparent);
    --scrim: rgba(0, 0, 0, 0.55);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 6px 16px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.35);
    /* Spacing scale — use these, not arbitrary pixel values. */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;
    --space-7: 48px;
    /* Readable measure for running text/hints and the column width of a settings form
       (docs/architecture/ui-kit.md). */
    --measure: 65ch;
    --measure-form: 720px;
    --radius-lg: 10px;
    /* ---- Scales (the one place a size/radius/timing/layer is decided) ----
       Type: pick the nearest step; don't add a new px value to a component. */
    --fs-2xs: 10px;
    --fs-xs: 11px;
    --fs-sm: 12.5px;
    --fs-md: 13.5px;
    --fs-base: 15px;
    --fs-lg: 16px;
    --fs-xl: 18px;
    --fs-2xl: 24px;
    --fs-3xl: 32px;
    /* Radius: xs for tiny chips/swatches, sm for inner elements, --radius for
       controls, lg for cards/rows, pill for badges. */
    --radius-xs: calc(var(--radius) - 3px);
    --radius-sm: calc(var(--radius) - 2px);
    --radius-pill: 999px;
    /* Motion */
    --dur-fast: 0.12s;
    --dur-base: 0.15s;
    --dur-slow: 0.3s;
    /* Stacking order — use a layer name, never a bare number. */
    --z-raised: 2;
    --z-sticky: 10;
    --z-dropdown: 20;
    --z-popover: 21;
    --z-overlay: 8000;
    --z-tour: 9000;
    --z-modal: 10000;
    --z-modal-top: 10001;
    --z-max: 10002;
    /* Radar canvas colors that have no general-purpose role (read by radar.js). */
    --radar-grid: #1a2530;
    --radar-runway: #c9d4dc;
    --radar-pavement: #2a2f36;
    --radar-pavement-stroke: #3a414a;
    --radar-taxiway: #4a525c;
    /* Ground picture (apt.dat import): fill per pavement surface, then a taxiway's edge casing, body and centreline. */
    --radar-surface-asphalt: #262c33;
    --radar-surface-concrete: #343b43;
    --radar-surface-grass: #1d2a22;
    --radar-surface-other: #2d2b27;
    --radar-taxiway-edge: #444c56;
    --radar-taxiway-surface: #2f353d;
    --radar-taxiway-centerline: #8a7424;
    /* Painted markings (apt.dat line types) and the runway strip body. */
    --radar-paint-yellow: #c9a227;
    --radar-paint-white: #cfd6dc;
    --radar-paint-border: #05080b;
    --radar-runway-surface: #444c56;
    /* Hues for the few categorical pills that aren't a status role. */
    --indigo: #b0b8fc;
    --violet: #d4b2fb;
    --gold: #f0c95c;
    --stripe-purple: #635bff;
    /* A distinct violet from --stripe-purple above (which is reserved
       specifically for "this is about Stripe") — used for Open Feature
       roadmap items (RoadmapItemCard.razor): green = Implemented, purple =
       open Feature, orange (--warning) = open Bug. */
    --feature-purple: #a78bfa;
    --radius: 6px;
    /* One focus indicator for every interactive element (WCAG 2.4.7); >= 3:1 against all surfaces in both themes. */
    --focus-ring: var(--accent);
    --font-display: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
    --font-mono: 'IBM Plex Mono', 'Consolas', monospace;
    /* One standardized height for every standard-sized standalone form
       control in the app — plain text/number/select inputs, comboboxes
       (MultiSelect/SuggestInput/WaypointPicker, already built to this same
       34px via .ms-input-row's own min-height), and regular buttons
       (".btn"/"+.btn-primary", including toolbar actions like "+ New X" and
       "Browse Library"). Confirmed 2026-09-09: those two buttons rendered
       visibly taller than everything else on the same toolbar row — this
       token is the app-wide fix, not another one-off local override. See
       the input/select and button rules below for what's deliberately
       excluded (textarea, checkboxes, range sliders, .btn-sm). */
    --control-height: 34px;
}

/* Light theme. Set by js/theme.js (loaded in <head>, before first paint) as
   data-theme on <html> — resolves the user's System/Light/Dark choice, so this
   one block is the only place light values live. Accent text on white needs a
   darker teal than the dark theme's bright one (contrast), so --accent here is
   the deeper shade and --accent-strong deeper still. */
:root[data-theme="light"] {
    color-scheme: light;
    --bg: #f2f6f8;
    --surface: #ffffff;
    --surface-2: #e9f0f4;
    --surface-3: #dce6ec;
    --border: #c3d1db;
    --border-subtle: #dbe5ec;
    --border-control: #6f8796;
    --text: #14212b;
    --text-muted: #496071;
    --accent: #076b78;
    --accent-strong: #065a65;
    --on-accent: #ffffff;
    --warning: #7d4a00;
    --critical: #a01c30;
    --success: #14653c;
    --info: #0b5fb8;
    --neutral-status: #546e7a;
    --on-warning: #ffffff;
    --on-critical: #ffffff;
    --feature-purple: #6d3fc7;
    --indigo: #3d46b0;
    --violet: #6a3399;
    --gold: #6e5300;
    --radar-grid: #cfdbe3;
    --radar-runway: #3b4a57;
    --radar-pavement: #dde3e8;
    --radar-pavement-stroke: #c3cbd3;
    --radar-taxiway: #b4bdc6;
    --radar-surface-asphalt: #cdd4da;
    --radar-surface-concrete: #e1e6ea;
    --radar-surface-grass: #cfe0d2;
    --radar-surface-other: #ddd8cc;
    --radar-taxiway-edge: #a9b3bd;
    --radar-taxiway-surface: #c4ccd3;
    --radar-taxiway-centerline: #a8841a;
    --radar-paint-yellow: #b58900;
    --radar-paint-white: #ffffff;
    --radar-paint-border: #5a6772;
    --radar-runway-surface: #9aa6b2;
    --scrim: rgba(20, 33, 43, 0.4);
    --shadow-sm: 0 1px 2px rgba(20, 33, 43, 0.08);
    --shadow-md: 0 6px 16px rgba(20, 33, 43, 0.12), 0 1px 3px rgba(20, 33, 43, 0.08);
    --shadow-lg: 0 8px 20px rgba(20, 33, 43, 0.16);
}
/* stylelint-enable color-no-hex, function-disallowed-list */

/* Touch-target sizing uplift for actual touch devices (iPad and similar) —
   pointer: coarse targets real touch input capability regardless of screen
   size, correctly firing on an iPad in landscape (a wide viewport a plain
   max-width breakpoint would miss) and correctly not firing on a merely
   narrow desktop/laptop window. .btn-sm is the class nearly every compact
   control in the app uses (the numpad, every World Editor table's row
   actions, more) — bumped here rather than per-component so the whole app
   gets the same ~44px touch-target guideline in one place. */
@media (pointer: coarse) {
    :root {
        --control-height: 44px;
    }

    .btn-sm {
        min-height: 44px;
        padding: var(--space-2) var(--space-3);
    }
}

* {
    box-sizing: border-box;
}

html, body {
    height: 100%;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-display);
    font-size: var(--fs-base);
    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 var(--space-2);
}

/* Heading sizes sit on the type scale (UI-029) - the browser's 2em/1.5em/1.17em defaults put
   30px/22.5px/17.5px text outside it. h1 = page title, h2 = section title, h3 = sub-section. */
h1 { font-size: var(--fs-2xl); line-height: 1.25; }
h2 { font-size: var(--fs-xl); line-height: 1.3; }
h3 { font-size: var(--fs-lg); line-height: 1.35; }
h4 { font-size: var(--fs-base); }

h1:focus {
    outline: none;
}

.page-subtitle {
    color: var(--text-muted);
    font-size: var(--fs-md);
    margin: calc(var(--space-3) * -1) 0 var(--space-4);
    max-width: 65ch;
}

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

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: calc(calc(var(--space-1) / 2) * -1);
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Set on <body> by js/dialog.js while a modal is open: the page behind must not scroll. */
body.modal-open {
    overflow: hidden;
}

/* Bypass block (WCAG 2.4.1): first focusable element, visible only while focused. */
.skip-link {
    position: absolute;
    left: var(--space-2);
    top: calc(-1 * var(--space-7));
    z-index: var(--z-max);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius);
    background: var(--accent);
    color: var(--on-accent);
    font-weight: 700;
    text-decoration: none;
}

.skip-link:focus {
    top: var(--space-2);
}

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

input, select, textarea {
    font-family: var(--font-mono);
    font-size: var(--fs-md);
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--border-control);
    border-radius: var(--radius-sm);
    /* Top trimmed from the original symmetric 8px — a native <select>
       ignores author line-height entirely (confirmed live: setting it had
       zero effect on computed style), so the fix has to come from the box
       model instead, which native controls do respect. Measured live: the
       selected-value text already rendered ~1-2px low within the old
       8px/8px padding, leaving visible whitespace above it while still
       clipping the descender of a lowercase letter at the bottom (e.g.
       "Belgium" losing the tail of its g in the Area dropdown) — 6px top
       both removes that excess whitespace and gives the text the room it
       was missing at the bottom, in one change. */
    padding: var(--space-1) var(--space-2) var(--space-2);
    width: 100%;
}

/* One standardized height (--control-height, above) for every standalone
   single-line field — deliberately excludes: textarea (sized by its own
   rows="N" attribute, not a fixed single-line height); [type=checkbox] and
   [type=range] (plain checkboxes and toggle-switch, sliders — both have
   their own dedicated sizing rules below, forcing 34px onto either would be
   a real visual bug, not a fix); .ms-search-input (MultiSelect/SuggestInput/
   WaypointPicker's inner text field — already correctly sized by its own
   parent .ms-input-row's min-height:34px, so also forcing a height directly
   onto the inner input would just make that wrapper grow taller instead). */
input:not([type="checkbox"]):not([type="range"]):not(.ms-search-input), select {
    height: var(--control-height);
}

/* One focus ring everywhere: buttons, links, summaries, toggles and anything focusable by tabindex
   (UI audit: buttons/links used the browser's 1px "auto" ring). */
:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

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

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: var(--radius-pill);
    background: var(--surface-3);
    border: 1px solid var(--border);
    cursor: pointer;
    vertical-align: middle;
    transition: background-color var(--dur-base) ease, border-color var(--dur-base) 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 var(--dur-base) ease, background-color var(--dur-base) 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: var(--space-1) 0;
    accent-color: var(--accent);
}

/* Text input in the UI font; mono is for identifiers/codes/JSON only (.is-code). The legacy
   .mono-input below stays for pages not yet migrated to FormField (UI-015). */
.input {
    font-family: var(--font-display);
}

.is-code {
    font-family: var(--font-mono);
}

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

.field {
    margin-bottom: var(--space-3);
}

/* One standardized grid for every editor form row across World Editor,
   Admin, and Organization pages — 3 columns wide on desktop, folding to 2
   then 1 as the viewport (or a narrower split-pane layout) shrinks. Fixed
   track counts, not auto-fit: auto-fit sized each row's columns off how
   many .field children happened to be in THAT row, so a 2-field row and a
   4-field row on the same page (or the same field-row shape on two
   different pages) rendered at different column widths — confirmed the
   root cause of the "inconsistent grid" look across editor pages. A row
   with fewer than 3 fields now simply leaves the remaining column(s)
   empty rather than stretching to fill the row, and a row with more than
   3 wraps onto additional rows of the same 3 columns — both deliberate,
   for the same reason: every row, on every page, shares one fixed column
   width instead of a per-row computed one. */
.field-row {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-3);
}

@media (max-width: 900px) {
    .field-row {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 640px) {
    .field-row {
        grid-template-columns: 1fr;
    }
}

/* .mono-input's own width:auto (deliberate everywhere it sits beside a
   label in a compact flex row, e.g. Simulator.razor's settings-menu-item
   list or the sim-overrides-grid) fights the "every field fills its own
   column" rule the instant it's used inside a .field-row grid instead
   (Curricula/Assignments/RuleSets/Notices/Users all do this for a plain
   Name/Description text field) — override it back to 100% there
   specifically, leaving every other .mono-input use untouched. */
.field-row > .field-span-2 {
    grid-column: span 2;
}

.field-row .mono-input {
    width: 100%;
}

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

.validation-message {
    color: var(--critical);
    font-size: var(--fs-sm);
    margin-top: var(--space-1);
}

.field-hint {
    font-size: var(--fs-sm);
    font-weight: 400;
    max-width: var(--measure);
    text-transform: none;
    letter-spacing: normal;
    color: var(--text-muted);
}

.field-hint-error {
    display: block;
    color: var(--critical);
    font-size: var(--fs-sm);
    margin-top: var(--space-1);
}

/* ---------- settings-table (Manage/Settings.razor) — name | control | description,
   one row per user setting, instead of the old stacked control-then-hint-below
   layout. Reuses .toggle-switch/.field-hint unchanged; only the row grid is new. ---------- */
.settings-table {
    display: flex;
    flex-direction: column;
}

.settings-table-row {
    display: grid;
    grid-template-columns: 200px auto 1fr;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}

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

.settings-table-name {
    font-size: var(--fs-md);
    font-weight: 600;
    color: var(--text);
    /* Set explicitly (not inherited from the global label rule) so a name
       rendered as a <span> — rows with no input to label — looks identical
       to one rendered as a <label>. */
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin-bottom: 0;
}

.settings-table-control {
    display: flex;
    align-items: center;
}

.settings-table-description {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    line-height: 1.5;
}

@media (max-width: 640px) {
    .settings-table-row {
        grid-template-columns: 1fr;
        gap: var(--space-1);
    }
}

/* ---------- buttons ---------- */
button, .btn {
    /* A real <button> is inline-block by default, so this matters for
       <a class="btn">-as-button links too — without it they stay
       display:inline, where e.g. .auth-submit's width:100% has no effect at
       all. inline-flex + centered content (not just the fixed height below)
       is the fix — a plain fixed height only "centers" text by
       padding/line-height coincidence, which breaks for anything at a
       different font-size/line-height than the default; .btn-icon already
       used this exact fix for icon buttons, now applied to every button. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: var(--fs-md);
    letter-spacing: 0.02em;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-4);
    height: var(--control-height);
    cursor: pointer;
    text-decoration: none;
    transition: border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast);
}

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);
}

/* ---- Button hierarchy (docs/architecture/ui-kit.md) ----
   primary   - the one main action of a view (filled accent)
   secondary - any other normal action (neutral raised; same family as a bare .btn)
   ghost     - low-emphasis / tertiary (no fill, no border until hover)
   danger    - destructive: outlined critical; .btn-danger-solid is the filled form for the
               final confirm button of a destructive dialog. */
.btn-secondary {
    background: var(--surface-3);
    border-color: var(--border);
    color: var(--text);
}

.btn-ghost {
    background: transparent;
    border-color: transparent;
    color: var(--text-muted);
}

.btn-ghost:hover {
    background: var(--surface-2);
    border-color: transparent;
    color: var(--text);
}

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

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

.btn-danger-solid {
    background: var(--critical);
    border-color: var(--critical);
    color: var(--on-critical);
}

.btn-danger-solid:hover {
    background: var(--critical);
    border-color: var(--critical);
    color: var(--on-critical);
    filter: brightness(1.1);
}

/* Icon-only trio (add above / add below / delete) on a repeatable form
   row — RowActionButtons.razor. Fixed square footprint at --control-height
   (the same token every input/select in the row already uses) instead of
   .btn-sm's auto-height text padding — the three previously rendered
   visibly shorter than the fields beside them, and slightly different
   sizes from each other (a text glyph vs the trash SVG). A 2px colored
   border — teal accent for add-above/below (.btn-accent), red critical for
   delete (.btn-danger) — makes the trio read as distinct, deliberate
   controls instead of blending into the row's neutral surface. Scoped to
   .btn-icon rather than changing .btn-danger's own default border (or
   adding a general .btn-accent fill), since .btn-danger is shared with
   Delete buttons elsewhere in the app that shouldn't change appearance. */
.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--control-height);
    height: var(--control-height);
    padding: 0;
    min-width: 0;
    border-width: 2px;
}

.btn-icon.btn-accent {
    border-color: var(--accent);
    color: var(--accent);
}

/* ---------- linked-account rows (ExternalLogins.razor, TwoFactorAuthentication.razor) —
   icon + name on the left, a connected-status pill or an action button on
   the right, sized equally regardless of provider/row via the fixed-width
   second column. ---------- */
.linked-account-row {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}

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

.linked-account-identity {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--fs-md);
    font-weight: 600;
    color: var(--text);
}

.linked-account-status {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 220px;
    justify-content: flex-end;
}

.pill-success {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--success) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--success) 40%, transparent);
    color: var(--success);
    font-size: var(--fs-sm);
    font-weight: 600;
    white-space: nowrap;
}

.pill-warning {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--warning) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent);
    color: var(--warning);
    font-size: var(--fs-sm);
    font-weight: 600;
    white-space: nowrap;
}

.pill-danger {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--critical) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--critical) 40%, transparent);
    color: var(--critical);
    font-size: var(--fs-sm);
    font-weight: 600;
    white-space: nowrap;
}

.pill-neutral {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-weight: 600;
    white-space: nowrap;
}

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

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

.btn-icon svg {
    display: block;
}

.btn-sm {
    padding: var(--space-1) var(--space-2);
    font-size: var(--fs-sm);
    /* Opts out of the standard --control-height above — row-action buttons
       in dense tables (Edit/Delete/Clone/Owned) are deliberately more
       compact than a standalone toolbar/form button; sizing purely from its
       own padding+font-size, exactly as before that token existed. */
    height: auto;
}

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

.assignment-card + .assignment-card {
    margin-top: var(--space-4);
}

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

/* Section header sitting directly on the table/block below it (e.g. the
   Students/Assignments sections on a class page): larger, with clear
   separation from the block above and a tight gap to its own content. */
.panel-title.section-title {
    font-size: var(--fs-base);
    margin: var(--space-6) 0 var(--space-1);
}

/* ---------- data table ---------- */
/* Real regression found 2026-09-28: the original fix here set display:block
   directly on <table> so the table itself could be the horizontal-scroll
   container, with no wrapper <div> needed. That works at narrow widths (where
   content is WIDER than the viewport — confirmed at the time via a 375px
   check) but silently breaks the opposite case: when the table's natural
   content is NARROWER than its container, the <tbody>/<tr>/<td> descendants
   (which keep their own UA-stylesheet table-part display values regardless
   of the <table> element's own display) form an anonymous table-fixup box
   per CSS2.1's table box generation rules, and THAT anonymous box shrinks to
   its own content width — ignoring the outer display:block box's own
   width:100%. Confirmed live: every World Editor list table rendered its
   header/rows at roughly 1/3 the page width on a normal desktop viewport,
   with dead space filling the rest of the (genuinely 100%-wide) outer table
   box. Tried and rejected before landing on this fix: table-layout:fixed
   (doesn't help — the anonymous box's width still isn't the outer table's),
   and plain overflow-x:auto with display:table left as default (fixes the
   width, but then a wide table scrolls the whole page instead of being
   contained, reintroducing the exact bug this rule originally existed to
   fix). The only combination that gets both right, confirmed live at both a
   1920px desktop width and the original 375px mobile check: a real wrapper
   <div class="table-scroll"> around the table, table left as its default
   display:table. This does mean every page needed the wrapper markup added
   (not a single global rule anymore) — a real cost, but the alternative is a
   table that's either too narrow or that breaks page-level scroll containment. */
.table-scroll {
    overflow-x: auto;
    width: 100%;
}

table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-mono);
    font-size: var(--fs-md);
}

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

td {
    padding: var(--space-2) var(--space-2);
    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: var(--space-1);
}

/* 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;
}

/* ---------- list rows (World Editor lists) ----------
   The shared pattern for "a list of things you manage": one raised row per
   entity instead of a table. Identity first (.list-row-id badge + .list-row-title),
   facts as quiet secondary text (.list-row-sub), status as chips (.badge-*),
   and one visible action (Edit) with the rest behind a "..." menu. Separation
   comes from space + a surface step, not full-width rules. Use a real <table>
   only for dense, many-column, comparable data (Admin lists, logs). */
.row-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.list-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}

.list-row:hover {
    border-color: var(--accent);
    box-shadow: var(--shadow-sm);
}

.list-row-id {
    min-width: 64px;
    padding: var(--space-2) var(--space-2);
    border-radius: var(--radius);
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-family: var(--font-mono);
    font-size: var(--fs-md);
    font-weight: 600;
    text-align: center;
}

.list-row-title {
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.list-row-sub {
    margin-top: calc(var(--space-1) / 2);
    font-size: var(--fs-sm);
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.list-row-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    justify-content: flex-end;
}

.list-row-meta .badge-published {
    margin-left: 0;
}

.list-row-actions {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

/* "..." overflow menu — details/summary, closed on outside click/pick by js/row-menu.js. */
.row-menu {
    position: relative;
}

.row-menu > summary {
    list-style: none;
    cursor: pointer;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius);
    color: var(--text-muted);
    font-weight: 700;
    line-height: 1;
}

.row-menu > summary::-webkit-details-marker {
    display: none;
}

.row-menu > summary:hover,
.row-menu[open] > summary {
    background: var(--surface-2);
    color: var(--text);
}

.row-menu-panel {
    position: absolute;
    top: 100%;
    right: 0;
    z-index: var(--z-dropdown);
    min-width: 150px;
    margin-top: var(--space-1);
    padding: var(--space-1);
    display: flex;
    flex-direction: column;
    gap: calc(var(--space-1) / 2);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}

.row-menu-panel button {
    width: 100%;
    justify-content: flex-start;
    text-align: left;
    background: transparent;
    border: none;
    font-weight: 600;
}

.row-menu-panel button:hover {
    background: var(--surface-3);
}

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

.row-menu-panel .btn-danger:hover {
    background: var(--critical-soft);
}

.list-row-no-id {
    grid-template-columns: minmax(0, 1fr) auto auto;
}

/* Long free-text rows (log messages): let the title and subtitle wrap instead of truncating. */
.list-row-wrap {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
}

.list-row-wrap .list-row-title,
.list-row-wrap .list-row-sub {
    white-space: normal;
    overflow-wrap: anywhere;
}

.log-exception {
    margin-top: var(--space-2);
    font-size: var(--fs-sm);
}

.log-exception pre {
    overflow-x: auto;
    padding: var(--space-3);
    background: var(--surface-2);
    border-radius: var(--radius-sm);
}

@media (max-width: 640px) {
    .list-row {
        grid-template-columns: auto minmax(0, 1fr) auto;
    }

    .list-row-no-id {
        grid-template-columns: minmax(0, 1fr) auto;
    }

    .list-row-no-id .list-row-meta {
        grid-column: 1;
    }

    .list-row-meta {
        grid-column: 2 / -1;
        grid-row: 2;
        justify-content: flex-start;
    }
}

/* ---------- page building blocks (phase 0 of the UI pass) ----------
   Shared chrome so every page is assembled from the same few parts:
   PageHeader (title + subtitle + page actions), Card (titled panel),
   .page-grid (ONE 12-column grid for dashboards — spans, never a second
   ad-hoc grid), MasterDetail (list left, detail right), EmptyState, and
   .detail-* (sections inside a detail panel). */
.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
    flex-wrap: wrap;
}

.page-header h1 {
    margin: 0;
}

.page-header .page-subtitle {
    margin: var(--space-1) 0 0;
}

.page-header-actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.card-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

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

.card-link {
    font-size: var(--fs-sm);
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}

.card-link:hover {
    text-decoration: underline;
}

/* Highlighted "do this next" card — a soft accent wash, not a filled block
   (the one filled/primary element on a page stays the button inside it). */
.card-accent {
    background: var(--accent-soft);
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.card-accent .panel-title {
    color: var(--accent-strong);
}

.page-grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--space-4);
    align-items: start;
}

.page-grid > .span-12 { grid-column: span 12; }
.page-grid > .span-8 { grid-column: span 8; }
.page-grid > .span-6 { grid-column: span 6; }
.page-grid > .span-4 { grid-column: span 4; }

@media (max-width: 900px) {
    .page-grid > .span-8,
    .page-grid > .span-6,
    .page-grid > .span-4 {
        grid-column: span 12;
    }
}

.badge-info {
    display: inline-block;
    padding: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Inviting empty state: says what the space is for and offers the next step. */
.empty-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-6) var(--space-4);
    border: 1px dashed var(--border);
    border-radius: var(--radius-lg);
    text-align: center;
}

.empty-card-title {
    font-size: var(--fs-lg);
    font-weight: 600;
    color: var(--text);
}

.empty-card-body {
    max-width: 52ch;
    color: var(--text-muted);
    font-size: var(--fs-md);
}

/* ---------- master/detail ----------
   List on the left (fixed width), detail panel on the right (sticky, takes the
   rest). On narrow screens only one shows at a time: the list until something
   is selected, then the detail with a Back button. */
.master-detail {
    display: grid;
    grid-template-columns: minmax(300px, 400px) minmax(0, 1fr);
    gap: var(--space-4);
    align-items: start;
}

.md-list {
    min-width: 0;
}

.md-detail {
    position: sticky;
    top: var(--space-4);
    min-width: 0;
    max-height: calc(100vh - 2 * var(--space-4));
    overflow-y: auto;
}

.md-back {
    display: none;
    margin-bottom: var(--space-3);
}

@media (max-width: 900px) {
    .master-detail {
        grid-template-columns: minmax(0, 1fr);
    }

    .master-detail.has-selection .md-list {
        display: none;
    }

    .master-detail:not(.has-selection) .md-detail {
        display: none;
    }

    .md-detail {
        position: static;
        max-height: none;
    }

    .md-back {
        display: inline-flex;
    }
}

/* Selectable rows are <button>s, so undo the global button look (fixed height,
   centered, bold) that would otherwise squash the two-line row layout. */
.md-list .list-row {
    grid-template-columns: minmax(0, 1fr) auto;
    cursor: pointer;
    text-align: left;
    width: 100%;
    height: auto;
    justify-content: normal;
    font-size: var(--fs-base);
    font-weight: 400;
    letter-spacing: normal;
    color: var(--text);
}

.list-row > div {
    min-width: 0;
}

.md-list .list-row.is-selected {
    border-color: var(--accent);
    background: var(--accent-soft);
}

/* ---------- detail panel sections ---------- */
.detail-title {
    margin: 0;
    font-size: var(--fs-xl);
    color: var(--text);
}

.detail-sub {
    margin: var(--space-1) 0 0;
    color: var(--text-muted);
    font-size: var(--fs-md);
}

.detail-section {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
}

.detail-section-title {
    margin: 0 0 var(--space-3);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-3) var(--space-4);
    margin: 0;
}

.detail-grid dt {
    font-size: var(--fs-xs);
    color: var(--text-muted);
    margin-bottom: calc(var(--space-1) / 2);
}

.detail-grid dd {
    margin: 0;
    font-size: var(--fs-md);
    color: var(--text);
    overflow-wrap: anywhere;
}

.detail-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
}

/* Compact list inside a detail section (students, assignment steps): one line each,
   name first, quiet secondary text, optional actions on the right. */
.detail-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.detail-list > li {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--border-subtle);
    font-size: var(--fs-md);
}

.detail-list > li:last-child {
    border-bottom: none;
}

.detail-list-main {
    flex: 1 1 auto;
    min-width: 0;
}

.detail-list-step {
    flex: 0 0 auto;
    min-width: 28px;
    padding: calc(var(--space-1) / 2) 0;
    border-radius: var(--radius-xs);
    background: var(--accent-soft);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: 600;
    text-align: center;
}

.detail-list > li.detail-subpanel-item {
    display: block;
    padding: 0;
    border-bottom: none;
}

.detail-list-actions {
    display: flex;
    gap: var(--space-1);
    flex: 0 0 auto;
}

/* An expanded sub-panel under a list item (e.g. one assignment's results matrix). */
.detail-subpanel {
    margin: var(--space-2) 0 var(--space-3);
    padding: var(--space-3);
    background: var(--surface-2);
    border-radius: var(--radius);
}

.detail-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-4);
}

/* Destructive actions live together at the bottom, away from Save. */
.danger-zone {
    border-color: color-mix(in srgb, var(--critical) 35%, transparent);
}

.danger-zone .detail-section-title {
    color: var(--critical);
}

/* ---------- world editor CRUD forms ---------- */
.toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
    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: var(--space-2);
    flex: 1 1 auto;
    min-width: 0;
}

.filter-input {
    flex: 1 1 240px;
    max-width: 320px;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    padding: var(--space-2) var(--space-2);
}

/* 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. That
   was fixed here as a local, toolbar-only pin at the time; superseded
   2026-09-09 by the app-wide --control-height token (input/select and
   button rules above) — both selectors below already resolve to the same
   34px through that token now, so this toolbar-scoped override is redundant
   and kept removed. */

/* ---------- 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: var(--space-1);
    padding: calc(var(--space-1) / 2) var(--space-1);
    border-radius: var(--radius-pill);
    background: var(--accent);
    color: var(--on-accent);
    font-size: var(--fs-xs);
    font-weight: 700;
}

.filter-popout {
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: var(--space-1);
    min-width: 260px;
    max-width: 320px;
    /* Fallback only, not the normal case: with .ms-dropdown pulled into
       normal flow below, the card already grows tall enough on its own to
       fit every group plus one open ~10-row dropdown. This just keeps a
       pathologically tall popout (many groups, all expanded-looking at
       once) from running off the bottom of a short window — viewport-
       relative so it still behaves there instead of a fixed px cap fighting
       with legitimately-tall content. */
    max-height: 80vh;
    overflow-y: auto;
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    z-index: var(--z-dropdown);
}

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

.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: var(--space-1);
    min-height: var(--control-height);
    padding: var(--space-1) var(--space-1);
    /* 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: var(--radius-sm);
    cursor: text;
}

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

.ms-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: calc(var(--space-1) / 2) var(--space-1) calc(var(--space-1) / 2) var(--space-2);
    background: var(--surface-3);
    border-radius: var(--radius-pill);
    font-size: var(--fs-sm);
    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: var(--fs-md);
    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: var(--fs-sm);
    padding: calc(var(--space-1) / 2) var(--space-1);
}

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

.ms-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: calc(var(--space-1) / 2);
    /* ~10 rows before scrolling (.ms-dropdown-option's own padding/font-size
       puts each row at ~30px). */
    max-height: 320px;
    overflow-y: auto;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lg);
    z-index: var(--z-popover);
}

/* Inside a FilterPopout specifically, an open dropdown no longer floats
   absolutely over the rest of the popout (where it got clipped by
   .filter-popout's own overflow:auto, forcing a scrollbar nested inside a
   scrollbar at the exact same spot — confirmed 2026-10-02). Pulling it into
   normal flow instead makes it push .filter-popout-group, and so
   .filter-popout itself (a flex column), taller — the "expand to fit"
   behavior the card should have. Still capped to ~10 rows by the
   max-height/overflow-y above, so a long options list scrolls on its own
   rather than growing the popout without bound. MultiSelectCoordinator (see
   its own doc comment) ensures only one group's dropdown is ever expanded
   at once, so at most one of these is adding height at a time. Standalone
   MultiSelect usages outside FilterPopout (Tiers/Roles/Users/Scenarios
   forms) aren't matched by this selector and keep the original
   overlay-on-top-of-the-page behavior. */
.filter-popout .ms-dropdown {
    position: static;
    margin-top: var(--space-1);
}

.ms-dropdown-option {
    padding: var(--space-1) var(--space-2);
    font-size: var(--fs-sm);
    cursor: pointer;
}

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

.ms-dropdown-empty {
    padding: var(--space-1) var(--space-2);
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.form-card {
    margin-bottom: var(--space-4);
}

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

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

.sim-overrides-grid .field {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: 0;
    font-size: var(--fs-sm);
}

.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: var(--space-2);
    text-transform: none;
    letter-spacing: normal;
    font-size: var(--fs-md);
    color: var(--text);
}

.runways-title {
    margin-top: var(--space-4);
}

.runway-row {
    align-items: end;
    padding: var(--space-2);
    margin-bottom: var(--space-2);
    background: var(--surface-2);
    border-radius: var(--radius-sm);
}

/* These are the repeatable add/remove rows (route legs, region points,
   scenario spawn flows/scheduled events) — one item per row, and the whole
   point of a row is to see it at a glance. The standardized .field-row
   GRID above (v0.63.1) fixes every row at 3 columns, which wraps any row
   with 4+ fields onto a second line, inflating a long list's height and
   losing oversight of it. .runway-row.field-row (2 classes, so it always
   outranks the plain .field-row rule above, no !important needed) reverts
   these rows only, to a FLEX layout instead of grid — deliberately not
   just a grid auto-fit: a grid track is sized for the widest/most-constrained
   item, so the compact .row-actions button trio (about 110px of real
   content) would still claim a full field-width column, wasting exactly
   the space this change is trying to reclaim. Flex lets .row-actions size
   to its own content while .field items flex-wrap at their own basis, so a
   row stays on one line whenever there's room and only wraps at genuinely
   narrow widths (e.g. Routes/Regions' own split-pane-with-live-map layout
   at a laptop-width viewport). */
.runway-row.field-row {
    display: flex;
    flex-wrap: wrap;
}

.runway-row.field-row > .field {
    flex: 1 1 90px;
    min-width: 90px;
}

/* RowActionButtons.razor's add-above/add-below/delete trio is a bare flex
   child (no .field label above it), so align-items:end (on .runway-row
   above) lands it against the row's own cross-end — which sits 14px below
   a sibling .field's own content, since that 14px is the .field's own
   margin-bottom counted into its box height. Matching that margin here
   keeps its content bottom flush with the inputs. flex:0 0 auto sizes the
   group to its own content instead of growing/shrinking like a .field —
   it's a fixed ~110px of icon buttons, not a field that benefits from
   claiming leftover row width. */
.runway-row .row-actions {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin-bottom: var(--space-3);
    flex: 0 0 auto;
}

/* Per-row validation error (ValidationErrorFormatter + a page's own
   legRowErrors/pointRowErrors/etc. dictionary) — reuses the app's existing
   .alert.alert-error look so it matches every other error message in the
   app, with flex-basis:100% to force it onto its own new line below the
   row's fields/actions regardless of how many of those fit before it,
   since .runway-row.field-row is flex-wrap. Surfacing a save error next to
   the specific row it belongs to (e.g. "Speed constraint (kt) must be
   between 0 and 999" right under that leg) beats a generic banner at the
   very bottom of a long Legs/Points/SpawnFlows list the user has to
   scroll back up from to even find. */
.runway-row .row-error {
    flex: 1 1 100%;
    margin-top: var(--space-1);
    padding: var(--space-1) var(--space-2);
    font-size: var(--fs-sm);
}

/* 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: var(--space-2);
    margin-bottom: var(--space-2);
    background: var(--surface-2);
    border-radius: var(--radius-sm);
}

.runway-row-group .field-row {
    margin-bottom: var(--space-2);
}

.runway-end-label {
    font-size: var(--fs-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    margin-bottom: var(--space-1);
}

.form-actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-4);
}

.form-alert {
    margin-top: var(--space-3);
}

/* ---------- chips / badges ---------- */
.chip {
    display: inline-block;
    font-family: var(--font-display);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    padding: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--surface-3);
    color: var(--text-muted);
}

.chip-error {
    background: color-mix(in srgb, var(--critical) 12%, transparent);
    color: var(--critical);
}

.chip-success {
    background: color-mix(in srgb, var(--success) 12%, transparent);
    color: var(--success);
}

.chip-accent {
    background: color-mix(in srgb, var(--accent) 15%, transparent);
    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: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--success) 15%, transparent);
    color: var(--success);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

.badge-warning {
    display: inline-block;
    padding: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--warning) 15%, transparent);
    color: var(--warning);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

.badge-danger {
    display: inline-block;
    padding: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--critical) 15%, transparent);
    color: var(--critical);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Muted variant of the same pill shape — a closed/inactive status that
   isn't good or bad (e.g. a Finished class on Organization/Classes.razor). */
.badge-neutral {
    display: inline-block;
    padding: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Open-Feature roadmap items (RoadmapItemCard.razor) — same shape, --feature-purple. */
.badge-purple {
    display: inline-block;
    padding: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--feature-purple) 15%, transparent);
    color: var(--feature-purple);
    font-size: var(--fs-xs);
    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: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--stripe-purple) 15%, transparent);
    color: var(--stripe-purple);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* ---------- alerts ---------- */
.alert {
    border-radius: var(--radius);
    padding: var(--space-2) var(--space-3);
    font-size: var(--fs-md);
    border: 1px solid transparent;
}

.alert-error, .alert-danger {
    background: color-mix(in srgb, var(--critical) 12%, transparent);
    border-color: color-mix(in srgb, var(--critical) 40%, transparent);
    color: var(--critical);
}

.alert-success {
    background: color-mix(in srgb, var(--success) 12%, transparent);
    border-color: color-mix(in srgb, var(--success) 40%, transparent);
    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: var(--space-3) var(--space-4);
    border-radius: var(--radius-pill);
    font-size: var(--fs-md);
    line-height: 1.5;
}

.status-pill-success {
    background: color-mix(in srgb, var(--success) 12%, transparent);
    color: var(--success);
}

/* ChangePassword.razor's password-age warning — same shape as .status-pill-success,
   warning-toned for "this is dated, not broken." */
.status-pill-warning {
    background: color-mix(in srgb, var(--warning) 12%, transparent);
    color: var(--warning);
}

/* ChangePassword.razor's always-shown password-age pill, next to the
   above when it's actually old — plain informational, .chip's own
   muted look. */
.password-age-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

.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: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

.badge-owner-builtin {
    background: color-mix(in srgb, var(--accent) 15%, transparent);
    color: var(--accent-strong);
}

.badge-owner-organization {
    background: color-mix(in srgb, var(--indigo) 16%, transparent);
    color: var(--indigo);
}

.badge-owner-personal {
    background: color-mix(in srgb, var(--violet) 16%, transparent);
    color: var(--violet);
}

/* Curated — ATC-Trainer team content, deliberately gold rather than another
   step in the blue/violet OwnershipType family: it isn't a fourth ownership
   category, it's a cross-cutting "official, locked" marker that can land on
   either a Built-in original or a user's own locked clone of one (see
   OwnershipBadge.razor's ShowsCurated). */
.badge-owner-curated {
    background: color-mix(in srgb, var(--gold) 16%, transparent);
    color: var(--gold);
}

.badge-published {
    display: inline-block;
    margin-left: var(--space-1);
    padding: calc(var(--space-1) / 2) var(--space-2);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--success) 15%, transparent);
    color: var(--success);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
}

.alert-warning {
    background: color-mix(in srgb, var(--warning) 12%, transparent);
    border-color: color-mix(in srgb, var(--warning) 40%, transparent);
    color: var(--warning);
}

/* WorldEditorWorkspace.razor's "which workspace am I in" banner for
   OrgAdmins — tinted with the same hue as .badge-owner-organization /
   .badge-owner-personal so the banner and each row's ownership pill read
   as the same category. */
.alert-workspace {
    margin-bottom: var(--space-3);
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
}

.alert-dismiss {
    flex: none;
    background: none;
    border: none;
    padding: 0 calc(var(--space-1) / 2);
    margin: calc(calc(var(--space-1) / 2) * -1) 0 0;
    font-size: var(--fs-xl);
    line-height: 1;
    color: inherit;
    opacity: 0.7;
    cursor: pointer;
}

.alert-dismiss:hover {
    opacity: 1;
}

.alert-workspace-organization {
    background: color-mix(in srgb, var(--indigo) 12%, transparent);
    border-color: color-mix(in srgb, var(--indigo) 40%, transparent);
    color: var(--indigo);
}

.alert-workspace-personal {
    background: color-mix(in srgb, var(--violet) 10%, transparent);
    border-color: color-mix(in srgb, var(--violet) 35%, transparent);
    color: var(--violet);
}

.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: var(--fs-sm);
    margin-top: var(--space-1);
}

/* ---------- auth (login/register) card contents ---------- */
.auth-heading {
    font-size: var(--fs-xl);
    font-weight: 700;
    margin: 0 0 var(--space-1);
}

.auth-subheading {
    color: var(--text-muted);
    font-size: var(--fs-sm);
    margin: 0 0 var(--space-5);
}

.auth-form .field {
    margin-bottom: var(--space-4);
}

.auth-checkbox-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: calc(var(--space-1) * -1) 0 var(--space-4);
    font-size: var(--fs-sm);
    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: var(--space-2) 0 0;
    font-size: var(--fs-sm);
    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 {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    padding: var(--space-3);
    font-size: var(--fs-md);
}

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

.auth-submit-hint {
    display: none;
    margin-top: var(--space-2);
    text-align: center;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.auth-divider {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-5) 0;
    color: var(--text-muted);
    font-size: var(--fs-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

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

/* Row, not a column — with only one provider configured, a single .auth-oauth-btn's
   flex-grow:1 still fills the whole row (nothing to share the width with), so this
   looks identical to the old stacked layout in that case; with two or more it splits
   them evenly side by side to save vertical space, wrapping only if the row genuinely
   can't fit them (e.g. a narrow phone viewport). */
.auth-oauth-row {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.auth-oauth-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    flex: 1 1 140px;
    padding: var(--space-2);
    font-size: var(--fs-md);
    font-weight: 600;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    white-space: nowrap;
}

.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: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    text-align: center;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

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

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

.auth-turnstile {
    margin: var(--space-1) 0 var(--space-4);
    display: flex;
    justify-content: center;
}

.empty-state {
    color: var(--text-muted);
    text-align: center;
    padding: var(--space-6) var(--space-4);
    font-size: var(--fs-md);
}

/* 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: var(--space-2);
    color: var(--text-muted);
    padding: var(--space-6) var(--space-4);
    font-size: var(--fs-md);
}

.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: var(--radius-xs);
}

/* ---------- radar scope shell (Simulator + Replay pages) ---------- */
.sim-shell {
    display: grid;
    /* --console-width is set inline (per-page component state) by whichever
       page renders a drag splitter next to .console-panel — Replay.razor as
       of 2026-09-07. Falls back to the original fixed 320px when unset, so
       a page that never sets it (Simulator.razor, so far) behaves exactly
       as before. */
    grid-template-columns: 1fr var(--console-width, 320px);
    gap: var(--space-4);
    /* 100vh sizes to the LARGEST possible iOS Safari viewport (chrome
       hidden) — with the URL bar/tabs actually showing, that overshoots the
       real visible height and the shell overflows (reported: fits only in
       fullscreen). 100dvh tracks the real, currently-visible viewport
       instead. Kept as a second declaration, not a replacement — a browser
       that doesn't understand dvh (or doesn't support small/large/dynamic
       viewport units at all) just ignores that line and keeps the vh value
       above, so this is a pure enhancement with no separate fallback logic
       needed. */
    height: calc(100vh - 54px - 48px);
    height: calc(100dvh - 54px - 48px);
    min-height: 600px;
}

/* Stacks the console below the radar instead of squeezing it into a
   permanently-320px column — reuses the same 900px breakpoint the World
   Editor's own map-preview split-pane (Regions/Routes.razor.css) already
   established for the identical problem (a fixed-width side panel competing
   with a canvas for space). Covers iPad portrait (~768-834px); iPad
   landscape (~1024px+) stays on the 2-column desktop layout above, since an
   iPad Pro 12.9" in portrait (1024px) is wider than many iPads' own
   landscape width — orientation alone can't reliably predict whether there's
   room for a 320px console, width can. --console-width (Replay.razor's own
   drag splitter) simply has nothing to size once the grid collapses to one
   column — inert, not broken, needs no code change of its own. */
@media (max-width: 900px) {
    .sim-shell {
        grid-template-columns: 1fr;
        /* Same vh-then-dvh pattern as .sim-shell's own height above — the
           stacked mobile layout's radar row has the identical iOS Safari
           chrome-visible overflow risk. */
        grid-template-rows: minmax(50vh, 1fr) auto;
        grid-template-rows: minmax(50dvh, 1fr) auto;
        height: auto;
        min-height: auto;
    }
}

.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: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border);
    flex: 0 0 auto;
}

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

.conn-status {
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-pill);
}

.conn-status.connected {
    color: var(--success);
    background: color-mix(in srgb, var(--success) 14%, transparent);
}

.conn-status.disconnected {
    color: var(--warning);
    background: color-mix(in srgb, var(--warning) 14%, transparent);
}

#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: var(--space-1);
    margin-bottom: var(--space-4);
}

.strip {
    display: grid;
    grid-template-columns: 1.4fr 0.8fr 0.6fr 0.6fr 0.8fr;
    gap: var(--space-1);
    align-items: center;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    text-align: left;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-2);
    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: var(--fs-2xs);
    color: var(--text-muted);
    text-transform: uppercase;
    text-align: right;
}

/* ---------- Blazor framework hooks (kept, restyled) ---------- */
.blazor-error-boundary {
    background: var(--critical);
    padding: var(--space-4) var(--space-4) var(--space-4) var(--space-4);
    color: var(--on-critical);
    border-radius: var(--radius);
    margin: var(--space-4);
}

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

#blazor-error-ui {
    background: var(--critical);
    color: var(--on-critical);
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: var(--space-3) var(--space-4);
    display: none;
}

#blazor-error-ui .reload {
    color: var(--on-critical);
    text-decoration: underline;
}

#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.

   Deliberately does NOT dim the rest of the page (2026-09-09 — an earlier
   version used box-shadow's "0 0 0 9999px" spread to darken everything
   outside this box; dropped per feedback that it made the rest of the UI
   too hard to read while a step was showing) — just a bordered box, sized/
   positioned exactly over the target (see tutorial.js), with a soft glow
   right around its own edge instead of a screen-wide backdrop.
   pointer-events stays none throughout — 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: var(--z-modal);
    border-radius: var(--radius-lg);
    border: 3px solid var(--accent);
    background: transparent;
    pointer-events: none;
    transition: top var(--dur-base) ease, left var(--dur-base) ease, width var(--dur-base) ease, height var(--dur-base) ease;
    box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent) 45%, transparent);
    animation: atc-tutorial-pulse 1.6s ease-in-out infinite;
}

/* Target-less "intro"-style steps — nothing to draw a ring around, so
   there's simply nothing to show here; the callout itself is still fully
   visible on its own. */
.atc-tutorial-ring.atc-tutorial-ring-full {
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
    animation: none;
}

@keyframes atc-tutorial-pulse {
    0%, 100% {
        box-shadow: 0 0 10px 1px color-mix(in srgb, var(--accent) 45%, transparent);
        border-color: var(--accent);
    }
    50% {
        box-shadow: 0 0 20px 4px color-mix(in srgb, var(--accent) 65%, 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 12px 2px color-mix(in srgb, var(--accent) 40%, 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: var(--z-modal-top);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--scrim);
}

.idle-timeout-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-5);
    max-width: 380px;
    box-shadow: var(--shadow-lg);
}

.idle-timeout-card p {
    margin: var(--space-2) 0;
    font-size: var(--fs-md);
    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: var(--space-1);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-pill);
    background: var(--warning);
    color: var(--on-warning);
    font-size: var(--fs-sm);
    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: var(--z-overlay);
    box-shadow: var(--shadow-lg);
}

/* ---------- 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: var(--z-tour);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--scrim);
    padding: var(--space-4);
}

.modal-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    width: 100%;
    max-width: 720px;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    /* Standard flexbug hardening, not cosmetic — a flex item's default
       min-width/min-height is `auto` (its content's own intrinsic size),
       which for a body containing a fixed-size canvas plus absolutely-
       positioned descendants (LibraryPreviewMap's map/canvas stack) is a
       plausible contributor to the runaway 33554433px canvas dimension
       reported live 2026-09-30 (Edge) — see radar.js's own
       MAX_SANE_CANVAS_DIM_CSS_PX comment for that bug's full story. Zeroing
       both here is a no-op for every other modal (none rely on content-based
       min sizing to avoid clipping) so this carries no visible risk. */
    min-width: 0;
    min-height: 0;
}

.modal-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-4);
    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: var(--fs-2xl);
    line-height: 1;
    cursor: pointer;
    padding: 0 var(--space-1);
}

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

.modal-card-body {
    padding: var(--space-4) var(--space-4);
    overflow-y: auto;
    min-width: 0;
    min-height: 0;
}

.modal-card-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border);
}

/* ---------- Delete confirm dialog (DeleteConfirmDialog.razor) ---------- */
/* Same .modal-card as the Report dialog above, just narrower — a delete
   confirmation is one sentence, not a scrollable breakdown. */
.delete-confirm-card {
    max-width: 420px;
}

/* ---------- Library clone dialog (LibraryCloneDialog.razor) ---------- */
/* Same .modal-card as the Report dialog above, just a bit wider — hosts a
   preview map plus a content list below it. */
.library-clone-dialog-card {
    max-width: 820px;
}

.library-clone-content-table {
    margin-top: var(--space-3);
    max-height: 240px;
    overflow-y: auto;
}

/* 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: var(--space-2) var(--space-3);
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border);
}

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

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

.report-student-section {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}
