/* ==========================================================================
   vaBase Admin Portal - dark-first UI foundation (M3A.1)
   Based on the M2.1 Account Portal foundation: same semantic tokens per
   docs/13, --vb-primary is the vaBase brand colour. Plain CSS, no frameworks.
   Admin-specific additions live at the end of this file.
   ========================================================================== */

:root {
    color-scheme: dark;

    /* Backgrounds & surfaces */
    --vb-bg: #0c0d0f;
    --vb-bg-soft: #141519;
    --vb-surface: #17181c;
    --vb-surface-raised: #1d1e23;

    /* Borders */
    --vb-border: #292a30;
    --vb-border-strong: #3c3e46;

    /* Text */
    --vb-text: #eceded;
    --vb-text-muted: #979aa1;

    /* Brand */
    /* product/43 (PR #191 review round 1): the airline's Primary Colour EXACTLY as saved, for the
       route lines (route-colours.js reads it). --vb-primary below is the UI's readable fill, which
       AdminAccent lightens for a very dark brand - a map route must not be. */
    --vb-brand: #5e00ea;
    --vb-primary: #5e00ea;
    --vb-primary-hover: #7326ff;
    --vb-primary-soft: rgba(94, 0, 234, 0.16);
    /* Brand tint readable as TEXT on dark backgrounds (buttons/focus use
       --vb-primary itself; #5e00ea lacks contrast as body-text/link colour). */
    --vb-primary-bright: #a883ff;
    /* product/43 S2: the link hover (lighter than bright), and the text drawn ON an accent fill.
       TenantShell overrides this whole family from the airline's Primary Colour (AdminAccent);
       these values are the default, and AdminAccentTests requires the two to agree. */
    --vb-primary-brighter: #c3aaff;
    --vb-on-primary: #fff;
    /* Scrollbars are STRUCTURAL CHROME, neutral grey, never the airline's accent (Myles, 2026-10-02:
       the tenant accent on scrollbars was "too visually distracting"; the accent is for actions,
       selected states and emphasis). Built from the existing neutral tokens: the thumb is the strong
       border grey, its hover a step towards the muted text grey. The hover shade is used only where
       the browser draws the WebKit scrollbar (Safari); scrollbar-color, which Chrome and Firefox use,
       has no hover state. AdminAccent does NOT write these. */
    --vb-scrollbar: var(--vb-border-strong);
    --vb-scrollbar-hover: color-mix(in srgb, var(--vb-border-strong) 70%, var(--vb-text-muted));

    /* Semantic status */
    --vb-success: #3ec27d;
    --vb-warning: #d9a13c;
    --vb-danger: #e35d6a;

    /* Shape & depth */
    --vb-radius: 12px;
    --vb-radius-sm: 8px;
    --vb-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);

    /* Type & spacing rhythm */
    --vb-font: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
    --vb-mono: "Cascadia Mono", Consolas, monospace;
    /* M24.3 (G2): the content column grows to 1440px beside the sidebar; reading widths (.lede,
       .form-card, .form-card-wide) keep their own measures. */
    --vb-content-width: 1440px;
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

* { box-sizing: border-box; }

html, body {
    margin: 0;
    min-height: 100vh;
    background: var(--vb-bg);
    color: var(--vb-text);
    font-family: var(--vb-font);
    font-size: 16px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
}

body {
    /* Subtle brand glow anchoring the top of every page. */
    background:
        radial-gradient(900px 340px at 50% -120px, color-mix(in srgb, var(--vb-primary) 14%, transparent), transparent 70%),
        var(--vb-bg);
    display: flex;
    flex-direction: column;
}

a {
    color: var(--vb-primary-bright);
    text-decoration: none;
}
a:hover { color: var(--vb-primary-brighter); }
a:focus-visible, button:focus-visible {
    outline: 2px solid var(--vb-primary);
    outline-offset: 2px;
    border-radius: var(--vb-radius-sm);
}

h1 { font-size: 1.7rem; font-weight: 650; letter-spacing: -0.02em; margin: 0 0 0.4rem; }
h1:focus { outline: none; }
h2 { font-size: 1.1rem; font-weight: 600; margin: 0 0 0.5rem; }
h3 { font-size: 0.95rem; font-weight: 600; margin: 0 0 0.4rem; }

.mono { font-family: var(--vb-mono); font-size: 0.9em; }
.muted { color: var(--vb-text-muted); }

/* --------------------------------------------------------------------------
   App shell
   -------------------------------------------------------------------------- */

.topbar {
    position: sticky;
    top: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.7rem 1.5rem;
    background: color-mix(in srgb, var(--vb-bg) 78%, transparent);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--vb-border);
}

.brand {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--vb-text);
    font-weight: 700;
    font-size: 1.05rem;
}
.brand:hover { color: var(--vb-text); }

/* The vaBase mark (Components/Shared/BrandMark.razor): the SVG carries its own rounded square and
   gradient, so the box only sizes it - the Account Portal's rule. */
.brand-mark {
    display: inline-flex;
    flex-shrink: 0;
    width: 28px;
    height: 28px;
}
.brand-mark svg { width: 100%; height: 100%; display: block; }

/* The pages with no airline (sign-in, the airline chooser): a window-high column, so the footer
   sits at the foot of a short page. */
.plain-shell { min-height: 100vh; display: flex; flex-direction: column; }
.plain-shell > .page { flex: 1; }

/* The footer on every page (Components/Shared/SiteFooter.razor), as the Account Portal draws it:
   quiet, one line, the vaBase website, support and the year. */
.site-footer { border-top: 1px solid var(--vb-border); padding: 1.1rem 1.5rem; }
.site-footer-inner { max-width: var(--vb-content-width); margin: 0 auto; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.4rem; font-size: 0.82rem; color: var(--vb-text-muted); }
.site-footer a { color: var(--vb-text-muted); display: inline-flex; align-items: center; gap: 0.3rem; }
.site-footer a:hover { color: var(--vb-text); }
.site-footer a .icon { width: 13px; height: 13px; }
.site-footer-copy { margin-left: auto; }
@media (max-width: 640px) {
    .site-footer-inner { gap: 0.4rem 1rem; }
    .site-footer-copy { margin-left: 0; width: 100%; }
}

.brand-sub {
    color: var(--vb-text-muted);
    font-weight: 400;
    font-size: 0.85rem;
    border-left: 1px solid var(--vb-border-strong);
    padding-left: 0.6rem;
}

.topbar nav {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
}

.nav-link {
    color: var(--vb-text-muted);
    padding: 0.35rem 0.75rem;
    border-radius: var(--vb-radius-sm);
    font-size: 0.95rem;
}
.nav-link:hover {
    color: var(--vb-text);
    background: var(--vb-surface-raised);
}

.user-chip {
    color: var(--vb-text-muted);
    background: var(--vb-surface);
    border: 1px solid var(--vb-border);
    border-radius: 999px;
    padding: 0.25rem 0.8rem;
    font-size: 0.85rem;
    max-width: 200px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.page {
    width: 100%;
    max-width: var(--vb-content-width);
    margin: 0 auto;
    padding: 2.25rem 1.5rem 3rem;
    flex: 1;
}

/* Page header pattern: title + supporting text + optional actions. */
.page-header {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.75rem;
}
/* M11H.1: an optional leading element - the pilot page puts the pilot avatar here.
   ⚠️ margin-right: auto is what keeps the ACTIONS on the right. .page-header is a
   space-between row, so a third child would otherwise spread the title into the middle and
   every page with a Leading element would lay out differently from every page without one. */
.page-header-leading { flex: none; display: flex; align-items: center; }
.page-header-leading + div { margin-right: auto; }
.page-header .lede { margin: 0; }
/* See PageHeader.razor: never rely on inter-element whitespace for spacing, because Razor
   removes it at @if boundaries. */
.page-header-actions {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}

.lede {
    color: var(--vb-text-muted);
    max-width: 46rem;
    margin: 0 0 1.5rem;
}

.section-title {
    margin: 2.5rem 0 0.25rem;
}
.section-title + .muted { margin-top: 0; }

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    background: var(--vb-surface-raised);
    color: var(--vb-text);
    padding: 0.5rem 1.1rem;
    font-size: 0.95rem;
    font-family: inherit;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}
.btn:hover { background: var(--vb-border); color: var(--vb-text); }
.btn:active { transform: translateY(1px); }

.btn-primary {
    background: var(--vb-primary);
    border-color: var(--vb-primary);
    color: var(--vb-on-primary);
    font-weight: 600;
    box-shadow: 0 2px 12px color-mix(in srgb, var(--vb-primary) 35%, transparent);
}
.btn-primary:hover {
    background: var(--vb-primary-hover);
    border-color: var(--vb-primary-hover);
    color: var(--vb-on-primary);
}

.btn-ghost {
    background: transparent;
    border-color: transparent;
    color: var(--vb-text-muted);
}
.btn-ghost:hover { color: var(--vb-text); background: var(--vb-surface-raised); }

/* A button that cannot be pressed now LOOKS it (Myles, 2026-09-22). Until this rule a disabled Save looked exactly
   like a live one - and lit up on hover - so pressing it and getting nothing read as a Save that does nothing. Dimmed,
   a not-allowed cursor, no lift, no glow, and each variant's hover pinned back to its resting look. A page that
   disables its main action also says why beside it (.page-action-hint). */
.btn:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
.btn:disabled:active { transform: none; }
.btn:disabled:hover { background: var(--vb-surface-raised); border-color: var(--vb-border-strong); color: var(--vb-text); }
.btn-primary:disabled:hover { background: var(--vb-primary); border-color: var(--vb-primary); color: var(--vb-on-primary); }
.btn-ghost:disabled:hover { background: transparent; border-color: transparent; color: var(--vb-text-muted); }

/* Beside a page's actions: what still stands between the form and its main button. */
.page-action-hint { font-size: 0.85rem; color: var(--vb-text-muted); max-width: 30ch; text-align: right; line-height: 1.35; }

.inline-form { display: inline; margin: 0; }

/* --------------------------------------------------------------------------
   Cards
   -------------------------------------------------------------------------- */

.card {
    background: var(--vb-surface);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    padding: 1.4rem;
    box-shadow: var(--vb-shadow);
    transition: border-color 0.15s ease, transform 0.15s ease;
}
/* The same lift the PILOT SITE gives every card (theme.css .card:hover), brought across so the
   two surfaces of one product do not feel like two products (Myles). The portal previously
   reserved hover for things that navigate - .tenant-card and .area-link - which was a defensible
   rule and simply not the one the template follows. Identical values to the template, not merely
   similar ones: a lift that is 2px on one surface and 3px on the other is worse than none. */
.card:hover {
    border-color: var(--vb-border-strong);
    transform: translateY(-2px);
}
/* ...but NEVER the docked decision bar. It is a card that floats over the page, so a lift would
   move Approve/Hold/Reject two pixels the moment the cursor approached them - a target that
   steps aside as you reach for it, in the one place on the platform where a mis-click is
   expensive. The border still responds; only the movement is refused. */
.card.review-bar:hover { transform: none; }

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 1rem;
    margin-top: 1rem;
}

/* Clickable airline/tenant cards on the dashboard. */
.tenant-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
    gap: 1rem;
    margin-top: 1rem;
}

.tenant-card {
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
    background: var(--vb-surface);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    padding: 1.25rem 1.4rem;
    box-shadow: var(--vb-shadow);
    color: var(--vb-text);
    transition: border-color 0.15s ease, transform 0.15s ease;
}
.tenant-card:hover {
    color: var(--vb-text);
    border-color: var(--vb-border-strong);
    transform: translateY(-2px);
}
.tenant-card:hover .tenant-card-cta { color: var(--vb-primary-bright); }

.tenant-card-name { font-size: 1.05rem; font-weight: 650; }

.tenant-card-slug {
    align-self: flex-start;
    font-family: var(--vb-mono);
    font-size: 0.8rem;
    color: var(--vb-text-muted);
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border);
    border-radius: 6px;
    padding: 0.1rem 0.5rem;
}

.tenant-card-meta {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.tenant-card-cta {
    margin-top: auto;
    padding-top: 0.35rem;
    font-size: 0.85rem;
    color: var(--vb-text-muted);
}

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

.form-card {
    max-width: 27rem;
    margin-top: 0.5rem;
}
/* A form card sitting straight under a LIST needs the section rhythm (matching .stat-grid): 0.5rem
   reads as stuck to the table. SCOPED to that adjacency on purpose - a blanket .form-card margin
   would silently add a rem above every card that already gets its spacing from a page header or a
   section title. */
.table-wrap + .form-card {
    margin-top: 1.5rem;
}

/* Centred narrow column for auth pages. */
.auth-page {
    max-width: 27rem;
    margin: 2.5rem auto 0;
    text-align: center;
}
.auth-page .card { text-align: left; margin-top: 1.5rem; }
.auth-page .lede { margin: 0 auto; }

.field { margin-bottom: 1.1rem; }
.field label {
    display: block;
    color: var(--vb-text);
    font-size: 0.88rem;
    font-weight: 550;
    margin-bottom: 0.35rem;
}

/* Blazor's InputText renders no type attribute by default, so target all inputs
   within a field rather than input[type=...]. */
/* .data-input shares this declaration rather than copying it: an input inside a TABLE CELL
   (the M9.2 scoring band editor) cannot use .field, whose 1.1rem bottom margin has no business
   in a table row - and a second copy of the box styling is a second thing to keep in step.
   Its own compactness overrides follow the table rules further down. */
.field input,
.data-input,
.condition-row input,
.automation-search,
.webhook-event-search,
.template-rows input,
.variable-picker-search {
    width: 100%;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.55rem 0.8rem;
    font-size: 0.95rem;
    font-family: inherit;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input::placeholder { color: var(--vb-text-muted); }
input:focus {
    outline: none;
    border-color: var(--vb-primary);
    box-shadow: 0 0 0 3px var(--vb-primary-soft);
}
/* Keep dark autofill on Chromium. Chromium paints its own background on an autofilled field and
   ignores `background` entirely, so only an inset box-shadow big enough to cover the field gets
   through.

   The :hover/:focus/:active states are named explicitly. Chromium keeps the autofill styling
   through all of them, and `input:focus` above sets box-shadow at the SAME specificity - so
   without them this rule wins only because it sits later in the file, and the field would snap
   back to Chromium's white the moment it was clicked. That is not hypothetical: it is exactly
   the bug that was reported on the customer template, which had no autofill rule at all. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:focus {
    -webkit-box-shadow: 0 0 0 100px var(--vb-bg-soft) inset;
    box-shadow: 0 0 0 100px var(--vb-bg-soft) inset;
    -webkit-text-fill-color: var(--vb-text);
    caret-color: var(--vb-text);
    transition: background-color 100000s ease-in-out 0s;
}

/* A form's closing actions (Myles, 2026-09-28: Save and Cancel were full-width bars stacked on
   top of each other): one row at the buttons' own width, on the right, Cancel (or the secondary
   action) first and the primary last - the order the page header's Save and the modals use. The
   sign-in card keeps its one full-width button (below). */
.form-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.6rem; margin-top: 1.35rem; }
.form-actions .btn { width: auto; }
.auth-page .form-actions .btn { width: 100%; }
@media (max-width: 640px) {
    /* Phone: the row's buttons share its width, each a full touch target. */
    .form-actions .btn { flex: 1 1 auto; }
}

/* The chart's hover readout. It holds ONE line's height whether or not anything is hovering,
   and never wraps: the text changes on every pointer move, and a line that grew or shrank would
   shift the whole card under the reader (measured - the page's scrollbar came and went, taking
   fifteen pixels of chart width with it). */
.debrief-readout {
    min-height: 1.35em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The derived flight timeline (M6I.3): its heading and its own in-memory filter sit on one
   line, because the count has to say what the filter is showing. */
.timeline-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.6rem;
    margin: 1.4rem 0 0.7rem;
}
.timeline-head h3 { margin: 0; }
.timeline-search { flex: 0 1 18rem; }

/* M6J.8r - the compact timeline, the pilot page's M6J.8m shape: a plain box under the heading
   holding the category chips and a bounded, scrolling list of dense flush rows (a busy flight
   derives several hundred events, and the reviewer's decision sits below). The padding is
   INSIDE the scroll box, so it sits between the altitude column and the scrollbar. */
.timeline-box {
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    background: var(--vb-surface-raised);
    padding: 10px 14px 12px;
    margin-bottom: 1.25rem;
}
.timeline-box .timeline-filters { margin-bottom: 8px; }
.timeline-empty { margin: 4px 0 0; }
.timeline-list {
    display: flex;
    flex-direction: column;
    max-height: 22rem;
    overflow-y: auto;
    padding-right: 12px;
}
.timeline-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 6px 0;
    border-top: 1px solid var(--vb-border);
    font-size: 0.92rem;
    scroll-margin-top: 8px;
}
.timeline-row:first-child { border-top: 0; }
.timeline-time { flex: 0 0 auto; color: var(--vb-text-muted); font-size: 0.82rem; white-space: nowrap; }
.timeline-event { flex: 0 1 auto; }
.timeline-detail { flex: 1 1 auto; min-width: 0; font-size: 0.82rem; }
.timeline-alt { flex: 0 0 auto; margin-left: auto; font-size: 0.82rem; white-space: nowrap; }

/* The row an evidence link just scrolled to. It FADES on its own (timeline-focus.js removes the
   class): the mark answers "which one did I click", and a highlight that never clears becomes a
   second permanent kind of state on a page that already carries flags and outcomes. */
.timeline-row.timeline-row-focus {
    background: var(--vb-primary-soft);
    transition: background 0.4s ease;
}

/* Server-side list filtering + paging (schedules first, M3C.2; the pattern other
   admin lists adopt as their data grows). */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.7rem;
    padding: 0.9rem 1.1rem;
    margin-bottom: 1.1rem;
}
.filter-bar .field { margin-bottom: 0; flex: 1 1 140px; }
.filter-bar .field:first-child { flex: 2 1 260px; }
.filter-bar .field label { font-size: 0.78rem; color: var(--vb-text-muted); }
.filter-bar-actions { display: flex; gap: 0.5rem; }

.pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.9rem;
    margin-top: 1.1rem;
}

/* A form with one primary save plus alternative save flows (schedule create's
   "start another / start return leg"): the primary keeps the full-width emphasis,
   the alternatives sit in a quieter, smaller row beneath it. */
.form-actions-multi { display: grid; gap: 0.7rem; justify-items: end; }
@media (max-width: 640px) { .form-actions-multi { justify-items: stretch; } }
.form-actions-alternatives {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}
.form-actions-alternatives-label {
    color: var(--vb-text-muted);
    font-size: 0.8rem;
    white-space: nowrap;
}
.form-actions-multi .form-actions-alternatives .btn {
    width: auto;
    flex: 1 1 auto;
    font-size: 0.85rem;
    padding: 0.4rem 0.9rem;
    color: var(--vb-text-muted);
}
.form-actions-multi .form-actions-alternatives .btn:hover { color: var(--vb-text); }

.hint { color: var(--vb-text-muted); font-size: 0.8rem; margin-top: 0.35rem; }
/* The reconciliation line under a utilisation panel needs air: it follows a table or an
   expander, and .hint's 0.35rem is sized for sitting under a form field (2026-09-01, Myles:
   "add spacing the the total at the bottom"). */
.util-note { margin-top: 0.9rem; }
/* The month rows fold away while the chart above them does not. The expander is the portal's
   own; only the spacing below it is new, so a closed panel does not sit flush on what follows. */
.util-months { margin-top: 0.9rem; }
.util-months > summary { padding: 0.7rem 0.9rem; }
.util-months .table-wrap { margin: 0; }
.hint strong { color: var(--vb-text); }

.form-footer {
    margin: 1.1rem 0 0;
    padding-top: 1rem;
    border-top: 1px solid var(--vb-border);
    color: var(--vb-text-muted);
    font-size: 0.85rem;
}

.invalid { border-color: var(--vb-danger) !important; }
.validation-message {
    display: block;
    color: var(--vb-danger);
    font-size: 0.83rem;
    margin-top: 0.3rem;
}

.alert-error {
    display: flex;
    gap: 0.5rem;
    background: color-mix(in srgb, var(--vb-danger) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--vb-danger) 55%, transparent);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.65rem 0.9rem;
    margin-bottom: 1.1rem;
    font-size: 0.9rem;
}

/* --------------------------------------------------------------------------
   Data display
   -------------------------------------------------------------------------- */

table.data {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background: var(--vb-surface);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    overflow: hidden;
    box-shadow: var(--vb-shadow);
}
table.data th, table.data td {
    text-align: left;
    padding: 0.65rem 1rem;
    border-bottom: 1px solid var(--vb-border);
    font-size: 0.93rem;
}
table.data th {
    color: var(--vb-text-muted);
    font-weight: 600;
    font-size: 0.83rem;
    background: var(--vb-bg-soft);
    white-space: nowrap;
}
table.data tr:last-child th,
table.data tr:last-child td { border-bottom: none; }
/* A badge that annotates a cell's text (a name plus "Default") needs air between the two - the
   collapsed markup whitespace alone reads as though the pill is stuck to the word. Scoped to a
   FOLLOWING sibling so a badge that is the whole cell (the Status column) stays flush with its
   header. */
table.data td > .badge:not(:first-child) { margin-left: 0.45rem; }
/* The same for a fact on a details card (a hub's name plus "Default"): the name is wrapped in its
   own element so the pill is a FOLLOWING sibling, because :first-child ignores text nodes. */
.settings-facts dd > .badge:not(:first-child) { margin-left: 0.45rem; }
/* Two codes side by side (an airport's ICAO then IATA): a space typed inside the second span is
   stripped by Blazor, so the gap is CSS's. */
.settings-facts dd > .mono + .mono { margin-left: 0.35rem; }

/* An EDITABLE table (the M9.2 scoring band editor): rows of small inputs rather than text.
   The cells tighten up because a text-sized input already carries its own padding, and the
   numeric columns are held narrow so a band row reads as one line of figures rather than four
   sprawling boxes. */
table.data.data-editable th,
table.data.data-editable td {
    padding: 0.4rem 0.6rem;
    vertical-align: middle;
}
.data-input {
    padding: 0.35rem 0.55rem;
    font-size: 0.9rem;
}
.data-col-num { width: 7.5rem; }
.data-col-actions { width: 1%; white-space: nowrap; }

/* Status badges - colour conventions per doc 13; label always present. */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    border-radius: 999px;
    padding: 0.14rem 0.7rem;
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--vb-text-muted);
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
}
.badge::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
}
.badge-success { color: var(--vb-success); background: rgba(62, 194, 125, 0.12); border-color: rgba(62, 194, 125, 0.4); }
.badge-warning { color: var(--vb-warning); background: rgba(217, 161, 60, 0.12); border-color: rgba(217, 161, 60, 0.4); }
.badge-danger  { color: var(--vb-danger);  background: rgba(227, 93, 106, 0.12); border-color: rgba(227, 93, 106, 0.4); }
.badge-info    { color: var(--vb-primary-bright); background: var(--vb-primary-soft); border-color: color-mix(in srgb, var(--vb-primary) 45%, transparent); }
.badge-muted   { color: var(--vb-text-muted); }
/* A badge following inline text (the history's Preview pill): Razor emits no whitespace
   between adjacent nodes, so the gap lives here rather than as a literal space in markup. */
.history-kind-badge { margin-left: 0.45rem; }
/* The note beside a pilot's join date when the airline has stated none (product/66 section 10): the same
   rule - the gap is CSS, because Razor strips a typed space at the edge of the note. */
.joined-default { margin-left: 0.35rem; }
/* product/66 S2: the imported-history reconciliation - the file on its line, the date beneath it, and a
   badge spaced by the history rule above. */
.imported-run-file { display: block; font-weight: 600; }
.imported-run-date { font-size: 0.82rem; }
.imported-run-notes { white-space: normal; min-width: 11rem; }
/* product/66 S3: the removal setup sits under the reconciliation, and the progress line follows "Working" with
   a CSS gap (a typed space beside an @if block is stripped). */
.removal-setup { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--vb-border); }
.removal-progress { margin-left: 0.35rem; }
.imported-run-remove { display: block; margin-top: 0.45rem; white-space: nowrap; }
.history-open:not(:first-child) { margin-left: 0.75rem; }
/* product/66 S4: Restore sits under a finished removal, and the result link follows the sentence with a
   CSS gap rather than a typed space. */
.removal-restore { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--vb-border); }
.removal-result-link { margin-left: 0.35rem; }

/* --------------------------------------------------------------------------
   Landing hero
   -------------------------------------------------------------------------- */

.hero {
    text-align: center;
    padding: 3.5rem 0 2.5rem;
}
.hero h1 {
    font-size: clamp(1.9rem, 4vw, 2.7rem);
    letter-spacing: -0.03em;
    margin-bottom: 0.75rem;
}
.hero .accent {
    background: linear-gradient(100deg, var(--vb-primary-bright), #d3beff);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
.hero .lede { margin: 0 auto 1.75rem; }
.hero-actions { display: flex; justify-content: center; gap: 0.75rem; flex-wrap: wrap; }

/* --------------------------------------------------------------------------
   Framework chrome & responsive
   -------------------------------------------------------------------------- */

.blazor-error-boundary {
    background: var(--vb-danger);
    padding: 1rem;
    color: #fff;
    border-radius: var(--vb-radius-sm);
}
.blazor-error-boundary::after { content: "An error has occurred."; }

@media (max-width: 640px) {
    .topbar { padding: 0.6rem 1rem; }
    .brand-sub { display: none; }
    .user-chip { display: none; }
    .page { padding: 1.5rem 1rem 2.5rem; }
    .page-header { align-items: stretch; flex-direction: column; }
}

/* --------------------------------------------------------------------------
   Admin Portal additions (M3A.1)
   -------------------------------------------------------------------------- */

/* (The tenant context bar's rules went at M24.6: the shell draws the airline, its state and your
   role, once, for every page.) */

/* Settings hub facts grid. */
.settings-summary { margin-bottom: 1.5rem; }

.settings-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 1rem;
    margin: 0;
}
.settings-facts dt {
    color: var(--vb-text-muted);
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    margin-bottom: 0.3rem;
}
/* A value with no break opportunity - an email, a URL, a long livery link - has nowhere to wrap,
   so its glyphs paint straight over the next column (mylesknight83+jimmytest@gmail.com landing
   on top of "United Kingdom"). Note the BOX stays the right width; it is the text that escapes,
   which is why this looks like a layout bug but is not one.

   overflow-wrap: anywhere is the fix - it lets the break happen mid-string. min-width: 0 is here
   because a grid item's default min-width is AUTO, which can force a track wider than its share
   when content refuses to shrink; measured here the auto-fit track already held, so it is a
   guard for the longer values rather than the thing that fixed this. */
.settings-facts > div { min-width: 0; }
.settings-facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

/* A fact with more than one part to it (the aircraft's location: where it is, how we know, and
   when). Adjacent inline spans in a <dd> emit no whitespace between them, so they run together
   into one unreadable string - these stack with real rhythm instead. */
.fact-stack {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
}
.fact-line {
    display: flex;
    align-items: baseline;
    gap: 0.45rem;
    flex-wrap: wrap;
}
/* M14.1: a pilot's location in the roster. Wide enough that "EGCC Manchester Airport" does not
   break after every word in an eight-column table; the gap is tighter than a details fact. */
.roster-location { min-width: 11rem; gap: 0.1rem; }

.fact-note {
    font-size: 0.8rem;
    line-height: 1.4;
}

/* --------------------------------------------------------------------------
   Admin Portal additions (M3A.2): pilots & staff
   -------------------------------------------------------------------------- */

/* Wide tables scroll inside their wrapper instead of breaking the page. */
.table-wrap { overflow-x: auto; }

/* Empty states: a centred card stating what belongs here and the way forward. */
.empty-state {
    text-align: center;
    /* More vertical than horizontal, on purpose: an empty state should read as deliberately
       spacious rather than accidentally empty. */
    padding: 3rem 1.25rem;
}
/* ⚠️ THE MEASURE IS THE WHOLE FIX. Centred text only works over a short line; the events list
   centred ~90 characters across a 1000px card and the result read as text adrift in a
   rectangle rather than an invitation. Everything inside is centred within this, not within
   the card. */
.empty-state-inner {
    max-width: 22rem;
    margin: 0 auto;
}
/* Nested inside a card that already has its own heading and padding: no border, no background,
   and far less vertical air - the surrounding card supplies both, and 3rem inside it just
   pushes the message away from the heading it belongs to. */
.empty-state-bare {
    padding: 1.5rem 0 0.5rem;
}
.empty-state h2 {
    font-size: 0.95rem;
    font-weight: 600;
    margin: 0 0 0.35rem;
}
.empty-state p { margin: 0 0 0.5rem; font-size: 0.875rem; line-height: 1.6; }
.empty-state .empty-state-action { margin: 1.25rem 0 0; }
/* The anchor: without it the box is a heading and a sentence floating in a large panel. Uses
   the same primary-at-16% fill the badges do, so it reads as vaBase rather than ornament. */
.empty-state-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: 0 auto 1rem;
    border-radius: 10px;
    background: var(--vb-primary-soft);
    border: 1px solid color-mix(in srgb, var(--vb-primary) 45%, var(--vb-border));
    color: var(--vb-primary-bright);
}
/* The passive tone: a list that fills through somebody else's doing gets no accent and no
   button, because there is nothing here for the reader to press. */
.empty-state-icon-muted {
    background: color-mix(in srgb, var(--vb-text-muted) 14%, transparent);
    border-color: var(--vb-border-strong);
    color: var(--vb-text-muted);
}
/* Quieter than the description: a footnote, not a second sentence of explanation. */
.empty-state-footnote {
    margin: 0.9rem 0 0;
    font-size: 0.78rem;
    color: var(--vb-text-muted);
}
/* Icons are stroke-only and inherit colour and weight from whatever they sit beside. */
.icon {
    width: 20px;
    height: 20px;
    flex: none;
}

/* Selects share the input styling (Blazor InputSelect renders a bare select). A condition row's boxes
   sit in a row, not a .field, and share it too (M19.4), as does a rule's or a group's all / any (M21.1). */
.field select,
.condition-row select,
.condition-match-select,
.template-rows select,
.pirep-sections-jump select {
    width: 100%;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    /* The arrow is OURS. padding-right alone does nothing - Chromium pins the native arrow to
       the border edge and ignores it - so the gap is made by replacing the arrow: appearance:none
       drops the native one and the chevron below is placed exactly. A data URI, so there is no
       asset to fetch. The portal is dark-only (color-scheme: dark on :root), so one colour. */
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23979aa1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.85rem center;
    padding: 0.55rem 2.2rem 0.55rem 0.8rem;
    font-size: 0.95rem;
    font-family: inherit;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.field select:focus,
.pirep-sections-jump select:focus {
    outline: none;
    border-color: var(--vb-primary);
    box-shadow: 0 0 0 3px var(--vb-primary-soft);
}

/* Success counterpart of .alert-error, for completed admin actions. */
.alert-success {
    display: flex;
    gap: 0.5rem;
    background: color-mix(in srgb, var(--vb-success) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--vb-success) 55%, transparent);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.65rem 0.9rem;
    margin-bottom: 1.1rem;
    font-size: 0.9rem;
}

/* Compact single-row form (label + select + button), e.g. pilot status changes. */
.status-form {
    display: flex;
    align-items: flex-end;
    gap: 0.6rem;
    flex-wrap: wrap;
}
.status-form .field {
    margin-bottom: 0;
    flex: 0 1 220px;
}

/* Textareas share the input styling (Blazor InputTextArea renders a bare textarea). */
.field textarea,
.template-section textarea {
    width: 100%;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.55rem 0.8rem;
    font-size: 0.95rem;
    font-family: inherit;
    resize: vertical;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.field textarea:focus,
.template-section textarea:focus {
    outline: none;
    border-color: var(--vb-primary);
    box-shadow: 0 0 0 3px var(--vb-primary-soft);
}

/* File chooser (M5E - the portal's FIRST, so there was no style to match and one had to be
   made). The native control cannot be restyled as a whole, but its button half can, so the input
   keeps its own behaviour, keyboard handling and filename display while the button matches .btn.
   Restyling by hiding the input behind a label is the usual trick and is worse: it loses the
   filename, and gets keyboard focus wrong unless carefully rebuilt. */
.file-field input[type="file"] {
    width: 100%;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text-muted);
    padding: 0.45rem 0.6rem;
    font-size: 0.9rem;
    font-family: inherit;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.file-field input[type="file"]::file-selector-button {
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    background: var(--vb-surface-raised);
    color: var(--vb-text);
    padding: 0.4rem 0.9rem;
    margin-right: 0.75rem;
    font-size: 0.9rem;
    font-family: inherit;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s ease;
}

.file-field input[type="file"]::file-selector-button:hover {
    background: var(--vb-border);
}

.file-field input[type="file"]:focus {
    outline: none;
    border-color: var(--vb-primary);
    box-shadow: 0 0 0 3px var(--vb-primary-soft);
}

/* A collapsed reference table inside a card. Collapsed by default because it is long and most
   visits do not need it, but present on the page rather than in documentation nobody opens - the
   values it lists (which words an enum column accepts) are otherwise undiscoverable from a
   spreadsheet full of header names. */
.column-reference {
    margin-bottom: 1.1rem;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    background: var(--vb-bg-soft);
}

.column-reference > summary {
    padding: 0.6rem 0.9rem;
    cursor: pointer;
    color: var(--vb-text);
    font-size: 0.88rem;
    font-weight: 550;
}

.column-reference > summary:hover { color: var(--vb-primary-bright); }
.column-reference[open] > summary { border-bottom: 1px solid var(--vb-border); }
.column-reference .table-wrap { margin: 0; border: 0; }
.column-reference table { font-size: 0.85rem; }

/* The import-order guidance on the Import and export page (product/87): the recommended sequence
   as pills joined by arrows, wrapping on a narrow screen. Pills are spaced by the gap, never a
   typed space. */
.order-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 0.5rem;
    align-items: center;
    font-size: 0.9rem;
}
.order-list .badge { font-weight: 500; }
.order-list .arrow { color: var(--vb-text-muted); }

/* The Map columns card (product/87 section 7): a column's first values stacked small, a field
   select wide enough for "DepartureCode · required", and the required fields as pills. */
.map-samples {
    display: grid;
    gap: 0.15rem;
    font-size: 0.82rem;
    color: var(--vb-text-muted);
}
.map-samples span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 22ch;
}
.map-select { min-width: 16rem; }

/* The Historical crediting card (product/87 boards 3 and 4): a heading per table with its count
   beside it, the tick column, the preview's progress, and the suggestion after an import. */
.scope-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin: 1rem 0 0.4rem;
}
.scope-head h3 { margin: 0; font-size: 1rem; }
td.tick, th.tick { width: 2.5rem; }
.progress-line {
    height: 6px;
    border-radius: 999px;
    background: var(--vb-surface-raised);
    overflow: hidden;
    margin: 0.6rem 0 0.3rem;
}
.progress-line > span { display: block; height: 100%; background: var(--vb-primary-bright); }
.card-suggestion { border-color: var(--vb-primary-bright); }
/* Pills beside text are spaced by CSS, never by a typed space (the pill-spacing rule). */
.roster-import-pill { margin-left: 0.5rem; }
.heading-pill { margin-left: 0.6rem; vertical-align: middle; font-size: 0.75rem; }
.consequence-list { margin: 0.6rem 0 0.8rem 1.2rem; display: grid; gap: 0.4rem; }
.card-suggestion h3 { margin-top: 0; }
.req-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin: 0.9rem 0;
}
@media (max-width: 640px) {
    .map-select { min-width: 0; width: 100%; }
}

/* Full-width cards stacked down a page. .card carries no margin on purpose - most cards live in a
   grid whose gap already spaces them, and a blanket margin would double up there - so a page that
   stacks them needs a container. Same 1.25rem rhythm as .form-card-row, which is the side-by-side
   equivalent. */
.card-stack {
    display: grid;
    /* One column that may shrink (M24.6): an implicit column is as wide as its widest child's
       minimum, so one bar chart made every card on the phone Streaks page 580px wide. */
    grid-template-columns: minmax(0, 1fr);
    gap: 1.25rem;
}

/* M10.6 tour route map. Fixed height rather than an aspect ratio: MapLibre needs a laid-out
   box before it can size its canvas, and a container that is zero-height at init renders a
   blank map that only fixes itself on a resize. */
.route-map {
    height: 340px;
    border-radius: var(--vb-radius-sm);
    overflow: hidden;
    border: 1px solid var(--vb-border);
}
/* The airport chip on the map. Not .badge: this sits on a basemap rather than in the page's
   own surfaces, so it needs its own contrast against whatever tile is underneath.

   ⚠️ THE ELEMENT IS THE DOT, and the label is positioned OUT of the layout. MapLibre centres
   the whole marker element on the coordinate, so a flex row of [dot][label] centres the PAIR -
   which pushed the dot sideways by half the label's width and left it visibly detached from
   the line that ends at the real point (Myles spotted it at EGNX). Sizing the element to the
   dot and floating the label absolutely means the dot sits exactly on the coordinate however
   long the code is. It takes no position of its own: MapLibre's .maplibregl-marker makes it
   absolute (how a marker is placed), and that positioned box is what the dot and label sit in.
   A position here would tie with the vendor's and, coming later, win - stacking every marker. */
.route-map-airport {
    width: 9px;
    height: 9px;
}
.route-map-dot {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--vb-primary-bright);
    border: 1px solid rgba(0, 0, 0, 0.55);
}
/* ⚠️ BELOW the dot, not beside it - see the template's copy of this rule for the full reason.
   The short version: the marker really is centred on its coordinate, but an opaque label sitting
   to its right covered the start of any line heading that way, which reads as a misplaced dot. */
.route-map-code {
    position: absolute;
    left: 50%;
    top: 13px;
    transform: translateX(-50%);
    font-family: var(--vb-mono);
    font-size: 0.7rem;
    color: var(--vb-text);
    background: rgba(12, 13, 15, 0.8);
    border: 1px solid var(--vb-border);
    border-radius: 4px;
    padding: 0.05rem 0.3rem;
    white-space: nowrap;
    /* The label is a caption, not a target - clicks and drags belong to the map underneath. */
    pointer-events: none;
}

/* Rendered rich text - a sanitised briefing or terms block, as opposed to .rich-text-editor
   which styles Quill while it is being typed into. The Admin Portal's first MarkupString
   render (M10 event and tour briefings), so these elements arrive with browser defaults
   otherwise: an <h3> twice the size of the card's own heading, and lists with the default
   40px indent. Only spacing and scale are set - colour comes from the card. */
.rich-text > :first-child { margin-top: 0; }
.rich-text > :last-child { margin-bottom: 0; }
.rich-text p { margin: 0 0 0.75rem; }
.rich-text h1,
.rich-text h2,
.rich-text h3 {
    /* Deliberately below the card's own <h2>: a briefing's headings organise its content, they
       do not compete with the heading of the panel they sit inside. */
    font-size: 1rem;
    font-weight: 600;
    margin: 1.1rem 0 0.5rem;
}
.rich-text ul,
.rich-text ol { margin: 0 0 0.75rem; padding-left: 1.25rem; }
.rich-text li { margin: 0.2rem 0; }
/* Links need no rule here: the base `a` already carries --vb-primary-bright, and restating it
   is a second place for the link colour to drift from the first. */
.rich-text blockquote {
    margin: 0 0 0.75rem;
    padding-left: 0.9rem;
    border-left: 2px solid var(--vb-border);
    color: var(--vb-text-muted);
}

/* A short list of guidance inside a card - the list equivalent of .hint, which is a <p> style and
   leaves a <ul> with the browser's default bullets and indent. */
.hint-list {
    margin: 0.35rem 0 1.1rem;
    padding-left: 1.1rem;
    color: var(--vb-text-muted);
    font-size: 0.85rem;
}

.hint-list li + li { margin-top: 0.35rem; }
.hint-list code {
    font-family: var(--vb-mono);
    font-size: 0.95em;
    color: var(--vb-text);
}

/* The website verification token: 64 hex characters with no break opportunity, so it would run
   out of its card. It wraps anywhere, and one click selects all of it for copying. */
.site-token {
    overflow-wrap: anywhere;
    user-select: all;
}

/* An inline row of actions inside a card. Distinct from .form-actions, which stretches its button
   to full width because it ends a FORM - a download link in a card is not that, and stretching it
   reads as a primary submit. */
.card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 1.1rem;
}

/* The live preview beside an event or tour's promo image link (M10). Capped rather than
   full-width: the point is to confirm the link resolves to the right picture, and an
   unbounded remote image would push the rest of the form off the screen. It is a REMOTE
   image the airline hosts, so it may well fail to load - the alt text carries that. */
.promo-preview {
    display: block;
    max-width: min(100%, 340px);
    max-height: 200px;
    width: auto;
    height: auto;
    margin: 0.6rem 0 0;
    border-radius: var(--vb-radius-sm);
    border: 1px solid var(--vb-border);
    object-fit: cover;
}

/* Checkbox rows: the label wraps the box so the whole line is clickable. */
.checkbox-field label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    text-transform: none;
    letter-spacing: normal;
    font-size: 0.95rem;
    color: var(--vb-text);
    cursor: pointer;
}
.checkbox-field input[type="checkbox"] {
    width: 1.05rem;
    height: 1.05rem;
    accent-color: var(--vb-primary);
}

/* Airport selection. The chosen airport reads as a filled slot, not as another form row. */
.airport-selected {
    background: var(--vb-primary-soft);
    border: 1px solid var(--vb-primary);
    border-radius: var(--vb-radius-sm);
    padding: 0.75rem 0.9rem;
    margin-bottom: 1.1rem;
}
.airport-selected-main {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}
.airport-selected p { margin: 0.35rem 0 0; font-size: 0.85rem; }
.airport-code { font-size: 1.05rem; font-weight: 650; }
.airport-results { margin-top: 1.1rem; }
.airport-no-results { margin-top: 1.1rem; }

/* Compact airport search results (M3C.2 rework): one bounded, vertically scrolling
   list - a wide multi-column table forced horizontal scrolling in card layouts. */
.airport-result-list {
    list-style: none;
    margin: 1.1rem 0 0;
    padding: 0;
    border-top: 1px solid var(--vb-border);
    max-height: 22rem;
    overflow-y: auto;
}
.airport-result-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.9rem;
    padding: 0.7rem 0.15rem;
    border-bottom: 1px solid var(--vb-border);
}
.airport-result-info { min-width: 0; }
.airport-result-main {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
}
.airport-result-meta { font-size: 0.8rem; margin-top: 0.2rem; }
.airport-result-list .btn { flex: 0 0 auto; }

/* ONE scrollbar for the whole portal (Myles, 2026-10-02): neutral, narrow and quiet - the sidebar,
   the Settings rail, modals, tables and every panel that scrolls inside itself, the page included.
   It used to be a purple brand bar on four named panels (and, with product/43 S2, the airline's
   accent), with the map overlays on a hand-picked grey of their own. Both syntaxes: scrollbar-width
   and scrollbar-color are Firefox and current Chromium (which then ignores ::-webkit-*);
   ::-webkit-* is Safari. Only the look is set here - nothing scrolls that did not before. */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--vb-scrollbar) transparent;
}
*::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}
*::-webkit-scrollbar-track,
*::-webkit-scrollbar-corner {
    background: transparent;
}
*::-webkit-scrollbar-thumb {
    background: var(--vb-scrollbar);
    border-radius: 999px;
}
*::-webkit-scrollbar-thumb:hover {
    background: var(--vb-scrollbar-hover);
}

/* Type-ahead results OVERLAY the form instead of pushing everything below them down the page
   (the customer template's .suggest-list does the same for the pilot-facing forms).

   ⚠️ .suggest-wrap wraps the INPUT ALONE, not the whole .field. It used to wrap the field, so
   top:100% put the panel below the label, the input AND the hint together - floating a hint's
   height clear of the box it belongs to, which reads as a detached card rather than a dropdown
   (Myles, on the schedule form). Anchored to the input, it sits 4px under it as a dropdown
   should, and the hint stays in normal flow underneath. */
.suggest-wrap { position: relative; }
.suggest-popout {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 30;
    margin-top: 4px;
    /* ⚠️ A SMALL EVEN GUTTER, not a wide horizontal one, because THE ROW carries the highlight.
       With the padding out here the row stopped 14.6px short of the panel on each side, so
       hovering drew a dark square floating inside the row instead of highlighting it (Myles, on
       the file picker). The rows below take the horizontal padding back, which puts the
       highlight edge-to-edge inside this gutter. */
    padding: 0.3rem;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
    max-height: 22rem;
    overflow-y: auto;
}
/* Inside a popout the PANEL scrolls, so the list itself is a plain flow of rows: no second
   scroller, no top rule under the field, and no trailing rule against the panel edge. */
.suggest-popout .airport-result-list {
    margin: 0;
    border-top: none;
    max-height: none;
    overflow: visible;
}
/* The row owns its own horizontal padding here so its highlight can reach the panel's inner
   edge, and rounds to match the panel it sits in.
   ⚠️ THE DIVIDERS GO WITH THE RADIUS, and it is one decision rather than two: a border follows
   border-radius, so a rounded row draws a divider that curves away 8px short at both ends -
   which is the "line that does not reach" version of the very complaint this rule fixes. Rows
   separate on the highlight and the spacing instead, the way the library rows and every menu in
   the portal already do. */
.suggest-popout .airport-result-list li {
    padding: 0.6rem 0.7rem;
    border-radius: var(--vb-radius-sm);
    border-bottom: none;
}
.suggest-popout .airport-no-results { margin: 0.85rem 0; }

/* Airport codes are uppercase everywhere in vaBase, so a field that takes one reads back
   uppercase whatever case it was typed in. */
.input-uppercase { text-transform: uppercase; }

/* Wide form layout for data-heavy forms (schedule create/edit): the 27rem form-card
   default suits short forms; here fields flow in two columns and long text fields
   span the full width. */
.form-card-wide { max-width: 64rem; }

/* Management form cards share a row and fill the width on wide screens (pilot
   details) instead of stacking as narrow left-aligned boxes. */
.form-card-row {
    display: grid;
    /* M24.3: at most three across, as the 1080px column allowed - the 1440px column would otherwise
       fit four narrow cards where the forms were designed for three. */
    grid-template-columns: repeat(auto-fit, minmax(max(320px, calc((100% - 2 * 1.25rem) / 3)), 1fr));
    gap: 1.25rem;
    align-items: stretch;
    margin-top: 0.5rem;
    margin-bottom: 1rem;
}
/* M14.1: a short facts card beside a tall form (the pilot Location panel) keeps its own height
   instead of stretching into a tall empty box. */
.form-card-row-top { align-items: start; }
.form-card-row .form-card {
    max-width: none;
    margin-top: 0;
}

/* Toggle/radio option cards flow across a wide form card instead of stacking
   as one narrow column (the form-card-row rationale, applied to the options
   INSIDE a card - settings hub flight operations, flying policy). */
.option-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 0.9rem;
    align-items: stretch;
    margin-bottom: 1rem;
}
.option-grid .field { margin: 0; }
.option-grid .radio-option { height: 100%; }

/* A short input keeps its natural width inside a wide form card. */
.field-narrow { max-width: 20rem; }

/* Modal dialog for interactive pages: dimmed backdrop, centred card. Clicking the
   backdrop cancels; the card itself stops click propagation. */
.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(6, 7, 8, 0.7);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    z-index: 100;
}
.modal {
    width: 100%;
    max-width: 30rem;
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.6rem;
    margin-top: 1.2rem;
}
.field-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 1.5rem;
    align-items: start;
}
.field-grid .field-span { grid-column: 1 / -1; }
/* THREE small numeric fields on one row (pilot details: credit hours / flights / flight
   points). The default 1fr 1fr leaves the third field on a row of its own, and inside a
   .form-card-row column that extra row makes the card tall enough to set the height of every
   card beside it - measured at 571px against 487px with this modifier, while its neighbours
   need 404 and 421. Only for genuinely small values: at a card width of ~340px the columns
   come out around 80px, which suits integers and nothing else.

   align-items: END, overriding .field-grid's start, because at that width a two-word label
   ("Flight points") wraps on some renderings and not others - it depends on the font that
   actually loaded. Aligning the row to its bottom edge puts every INPUT on the same line
   whether a label wrapped or not, so the layout cannot depend on guessing text metrics. A
   ragged label edge is fine; inputs at three different heights read as broken. */
.field-grid-3 {
    grid-template-columns: repeat(3, 1fr);
    align-items: end;
}
/* Settings › Flight operations, the three expiry limits (product/53 board 4). Aligning to the
   BOTTOM would let one field's own validation message lift its input above its neighbours; these
   labels are short and do not wrap at the card's width, so the row aligns to the TOP instead. */
.field-grid-3.field-grid-top { align-items: start; }
/* A SHORT value beside a SENTENCE (M9.4: credited minutes + reason, adjusted points + reason).
   The default 1fr 1fr gives an integer box the same width as a line of prose, which looks
   accidental and wastes the half the number does not need. */
.field-grid-narrow-first { grid-template-columns: 10rem 1fr; }

/* A form following the card's explanatory copy needs a real break. .muted is a colour only,
   so the only gap was the paragraph's own ~16px bottom margin, and at that distance the first
   label reads as part of the sentence above it rather than as the start of something new
   (Myles, on the reviewer notes card). */
.card > p + .field,
.card > p + .field-grid,
/* ...and after a TABLE, which is the case the paragraph selector missed: the reviewer-notes
   card shows the existing notes first, so once a pilot has one the "Add a note" label sat
   flush against the last row of the table above it. */
.card > .table-wrap + .field,
.card > .table-wrap + .field-grid { margin-top: 1.5rem; }
@media (max-width: 760px) {
    .field-grid { grid-template-columns: 1fr; }
    /* Needed explicitly: .field-grid-3 has the same specificity and is declared later, so
       without this it would keep three columns on a phone. */
    .field-grid-3 { grid-template-columns: 1fr; }
    /* Same reason - and a fixed 10rem column beside 1fr is worse on a phone than anywhere,
       because the sentence field ends up narrower than the number. */
    .field-grid-narrow-first { grid-template-columns: 1fr; }
}
/* Aircraft type checkboxes flow in columns instead of one long stack. */
.checkbox-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0.25rem 1.25rem;
}

/* --------------------------------------------------------------------------
   Admin Portal additions (M3C): fleet
   -------------------------------------------------------------------------- */

/* Headline stat tiles (doc 13 dashboard convention: stat tiles on surfaces,
   restrained accent use). First user: the fleet aircraft list. */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 1rem;
    margin-bottom: 1.5rem;
}
.stat-tile {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 1rem 1.2rem;
}
/* Long forms group related fields under small section headings (M3C.1 fleet forms). */
.form-section-title {
    margin: 1.75rem 0 0.85rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--vb-border);
    color: var(--vb-text-muted);
    font-size: 0.8rem;
    font-weight: 650;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.form-section-title:first-of-type { margin-top: 0.5rem; }
/* One sentence under a section title saying what the fields below it do (product/53 board 4). */
.form-section-lede { margin: 0 0 0.9rem; }

.stat-value { font-size: 1.5rem; font-weight: 650; letter-spacing: -0.02em; }
.stat-label {
    color: var(--vb-text-muted);
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* THE ADMIN HUB MAP (M4D). It carries .route-map as well, deliberately: the dark map-chrome
   overrides further down are scoped to #live-map and .route-map, so a container class of its own
   would have rendered MapLibre stock WHITE controls on a dark basemap - the silent-CSS-failure
   class this project keeps paying for.

   The height is a COMPOUND selector for the M4C reason: .hub-network-map lost its 420px to
   .route-map 320px purely on file order at equal specificity, and the tell was a canvas
   measuring one thing while the rule said another. Taller than a route map because this one is
   ZOOMABLE - staff come here to look at the airfield, not to glance at a line. */
.route-map.airport-map { height: 420px; }

/* THE FLIGHT DEBRIEF EMBED (M6I.4): the flown-track map and the profile chart on the PIREP
   review page. The map container carries .route-map for the airport-map's own reason (the
   dark map-chrome overrides are scoped to it), with the height on the compound selector.
   Slightly taller than a route glance: a reviewer inspects where events happened. */
.route-map.debrief-map { height: 380px; margin-bottom: 14px; }

/* The profile chart: a fixed-viewBox SVG stretched to the card, with everything textual as
   absolutely-positioned HTML at percentages (SVG text would be squashed by the stretch - the
   recorded lesson, same as the template's twin). */
.debrief-chart {
    position: relative;
    height: 220px;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    background: var(--vb-bg-soft);
    overflow: hidden;
}
.debrief-chart-svg { display: block; width: 100%; height: 100%; }
.debrief-alt-area {
    fill: rgba(126, 87, 255, 0.28);
    stroke: var(--vb-primary-bright);
    stroke-width: 1.5;
    /* The stroke must not stretch with the viewBox, or a wide card draws a smeared line. */
    vector-effect: non-scaling-stroke;
}
.debrief-spd-line {
    fill: none;
    stroke: #4fd1c5;
    stroke-width: 1.5;
    vector-effect: non-scaling-stroke;
    opacity: 0.9;
}
.debrief-band-label {
    position: absolute;
    top: 6px;
    transform: translateX(4px);
    font-size: 0.65rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--vb-text-muted);
    pointer-events: none;
}
.debrief-chart-ticks {
    position: relative;
    height: 18px;
    margin-top: 4px;
    font-family: var(--vb-mono);
    font-size: 0.7rem;
    color: var(--vb-text-muted);
}
.debrief-tick {
    position: absolute;
    transform: translateX(-50%);
}
/* The first and last tick sit at the edges, so centring them would clip half the label. */
.debrief-tick:first-child { transform: none; }
.debrief-tick:last-child { transform: translateX(-100%); }

/* The licence line under the map, plus the coordinates it drew from - so the panel still answers
   "where is this" on a browser that cannot reach the tile host. */
.map-credit {
    margin: 0.6rem 0 0;
    color: var(--vb-text-muted);
    font-size: 0.78rem;
}

/* --------------------------------------------------------------------------
   Admin Portal additions (M3D): PIREP review
   -------------------------------------------------------------------------- */

/* Status filter tabs on the PIREP queue: one row of pill links, the active
   filter rendered solid (btn-primary) by the page. */
.filter-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
}

/* PIREP details redesign (2026-08-06, Myles: "the UI is a bit messy"): the evidence reads
   top-to-bottom in paired columns, and the DECISION docks at the viewport bottom while the
   evidence scrolls - the reviewer finishes reading exactly where the buttons are. */

/* Two evidence cards side by side (as filed | as recorded; fuel | touchdowns). auto-FIT, not
   auto-fill: a lone card (a manual PIREP with no facts, a flight with no touchdowns) collapses
   the empty track and fills the row instead of sitting beside a hole. */
.pirep-columns {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: 1rem;
    margin-bottom: 1.5rem;
    align-items: start;
}
.pirep-columns .card { margin-bottom: 0; }
.pirep-columns h3 { margin-top: 0; }

/* A table INSIDE one of these cards is content, not another container (Myles: "not keen on the
   containers within containers") - table.data's own surface/border/shadow made every card hold
   a second box. Flat here: the rows keep their hairline separators and nothing else. */
.pirep-columns table.data {
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
}
.pirep-columns table.data th { background: transparent; }

/* Six headline fact tiles share ONE row (the global stat-grid's 160px minimum wrapped the
   sixth tile onto its own line at ordinary widths - an accidental-looking orphan). auto-FIT
   with a smaller floor keeps all six together and lets them share the full width.
   ⚠️ `.streak-stats` (M11G.8's engagement panel) JOINS this selector rather than copying the
   declaration - the `.data-input` idiom - because two rules for "six tiles, one row" are two
   things that drift; the class keeps its own name so nobody reads a streak panel as a PIREP one.
   Measured on the harness: 5 + 1 before, 6 across after. */
.pirep-stats,
.streak-stats {
    grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
}

/* The docked decision bar. position: sticky with bottom pins it to the viewport bottom while
   its natural position is still below the fold, then it settles into flow at the end of the
   page - no JS, and a shorter page simply shows it in place. The shadow separates it from the
   evidence scrolling beneath. */
.review-bar {
    position: sticky;
    bottom: 0.75rem;
    z-index: 30;
    /* .card carries no margin by design (most live in a grid whose gap spaces them), so the
       docked bar sat flush against whatever card preceded it - and a bar that floats over the
       page needs to look separate from the evidence it floats over. */
    margin-top: 1.75rem;
    padding: 0.9rem 1.2rem;
    box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.45), var(--vb-shadow);
}
/* The reversal bar's own note. The bar is sticky, so a paragraph inside it floats over the
   report for the whole scroll - the summary carries the consequence and the detail folds away
   (Myles). Deliberately lighter than .column-reference, which is a bordered block for a page
   body rather than a line inside a docked bar. */
.review-bar-note { margin-bottom: 0.65rem; }
.review-bar-note > summary {
    cursor: pointer;
    color: var(--vb-text-muted);
    font-size: 0.86rem;
}
.review-bar-note > summary:hover { color: var(--vb-primary-bright); }
.review-bar-note > p {
    margin: 0.55rem 0 0;
    font-size: 0.86rem;
}

.review-bar-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
}
/* The ONE shared comment box. Styled here because the portal's input styling is scoped to
   .field (a labelled block this bar deliberately does not use) - without this the box rendered
   as a bare browser input (Myles: "the comments box style is off"). Same recipe as .field
   input; flexes to fill the row so the bar stays one line wherever it fits. */
.review-comment {
    flex: 1 1 260px;
    min-width: 200px;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.55rem 0.8rem;
    font-size: 0.95rem;
    font-family: inherit;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.review-bar-error {
    margin: 0 0 0.6rem;
    color: var(--vb-danger);
    font-size: 0.9rem;
}

/* The queue's bulk-review controls share the individual PIREP decision bar instead of becoming
   a second form card. The accent edge and translucent raised surface make the temporary controls
   read as a floating layer, while the guidance stays subordinate to the actions. */
.bulk-review-bar {
    margin-top: 1rem;
    border-color: color-mix(in srgb, var(--vb-primary-bright) 42%, var(--vb-border));
    background: color-mix(in srgb, var(--vb-surface-raised) 94%, transparent);
    backdrop-filter: blur(14px);
}
.bulk-review-bar .btn { white-space: nowrap; }
.bulk-review-count {
    flex: 0 0 auto;
    white-space: nowrap;
}
.bulk-review-guidance {
    margin: 0.65rem 0 0;
    padding-top: 0.65rem;
    border-top: 1px solid var(--vb-border);
    color: var(--vb-text-muted);
    font-size: 0.82rem;
    line-height: 1.45;
}
.bulk-review-outcome {
    margin-top: 1rem;
}
.bulk-review-outcome .alert-success:first-child { margin-top: 0; }

@media (max-width: 700px) {
    .bulk-review-bar {
        bottom: 0.4rem;
        padding: 0.8rem;
    }
    .bulk-review-count,
    .bulk-review-bar .review-comment {
        flex-basis: 100%;
    }
    .bulk-review-bar .btn {
        flex: 1 1 auto;
    }
}

/* Destructive review action (reject): unmistakably not the primary action. */
.btn-danger {
    background: transparent;
    border-color: color-mix(in srgb, var(--vb-danger) 55%, transparent);
    color: var(--vb-danger);
}
.btn-danger:hover {
    background: color-mix(in srgb, var(--vb-danger) 14%, transparent);
    border-color: var(--vb-danger);
    color: var(--vb-danger);
}
.btn-danger:disabled:hover {
    background: transparent;
    border-color: color-mix(in srgb, var(--vb-danger) 55%, transparent);
    color: var(--vb-danger);
}

/* In-form picker panels (M3D PIREP form): airport/aircraft search and select run
   through the SAME POST form as the rest of the fields via named submit buttons, so
   no search or select ever resets what has been typed. Panels reuse the airport-*
   result styles. */
.picker-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1rem;
    margin-bottom: 0.75rem;
}
.picker-panel {
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    padding: 1rem 1.1rem;
}
.picker-panel h4 { margin: 0 0 0.65rem; }
.picker-panel .airport-selected { margin-bottom: 0.85rem; }
/* The clear button reads as a link inside the selected summary, not a boxed button. */
.picker-panel .airport-selected .btn-ghost { padding: 0; border: none; }

/* Radio option rows (M3D pilot flying policy): each choice is one clickable card -
   radio aligned with the title, description on its own line, the selected row
   highlighted with the accent so the current policy is obvious at a glance. */
.radio-options {
    display: grid;
    gap: 0.6rem;
    margin-bottom: 1rem;
}
/* Qualified `label.radio-option` deliberately. `.field label` is (0,1,1) with display:block and
   would beat a bare `.radio-option` (0,1,0) on SPECIFICITY however late this appears - the card
   then stacks its control ABOVE its wording instead of beside it, which is what a checkbox
   option looked like everywhere these cards sit inside a .field. Matching that specificity lets
   source order decide, which was always the intent. The class is only ever used on a label. */
label.radio-option {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.85rem 1rem;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    cursor: pointer;
    /* The block label above a field normally supplies this; an option card is the label, so it
       carries its own separation from whatever explains it above. */
    margin-bottom: 0;
}
.radio-option:hover { background: var(--vb-surface-raised); }
.radio-option:has(input:checked) {
    border-color: var(--vb-primary);
    background: color-mix(in srgb, var(--vb-primary) 9%, transparent);
}
.radio-option input {
    /* Reset the .field input text-box styling (full width, background, border,
       padding): inside an option card the input is a native checkbox/radio, not
       a text field, so it must render as the small control accent-color styles. */
    width: auto;
    background: none;
    border: 0;
    padding: 0;
    box-shadow: none;
    margin-top: 0.25rem;
    accent-color: var(--vb-primary);
    flex: 0 0 auto;
}
.radio-option-body {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}
.radio-option-desc {
    color: var(--vb-text-muted);
    font-size: 0.85rem;
    line-height: 1.45;
}

/* Live search at the end of the queue's status-tab row. */
.filter-tabs-search {
    flex: 1 1 260px;
    max-width: 26rem;
    margin: 0 0 0 auto;
}
.filter-tabs-search input { width: 100%; }

/* Inline fact runs (name · id · badge) inside definition lists: explicit flex gaps
   instead of relying on inter-element whitespace, which Razor trims at @if-block
   boundaries (the "John SmithVBA001" bug). */
.inline-facts {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.45rem;
}

/* --------------------------------------------------------------------------
   File manual PIREP (product/73): the compact one-line selected state of a lookup, the route
   line, the external aircraft, date + time pairs, hours + minutes, the from-plan tag, the
   pilot card and the summary. The grid is the shipped .detail-grid.detail-grid-form.
   -------------------------------------------------------------------------- */

/* ---- The compact selected state of a lookup (AirportField Compact, the aircraft field, the
        schedule lookup): one line in the primary-soft slot, the value then its facts then Change. */
.apf-selected {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.35rem 0.6rem;
    min-height: 2.6rem;
    padding: 0.45rem 0.75rem;
    background: var(--vb-primary-soft);
    border: 1px solid var(--vb-primary);
    border-radius: var(--vb-radius-sm);
}
.apf-selected .airport-code { font-size: 1rem; }
.apf-selected .apf-sub { color: var(--vb-text-muted); font-size: 0.85rem; min-width: 0; }
.apf-selected .btn-ghost { margin-left: auto; padding: 0.1rem 0.4rem; font-size: 0.82rem; }
/* A locked value (the schedule's airports) is the same slot without Change, muted border. */
.apf-selected.is-locked { background: var(--vb-surface-raised); border-color: var(--vb-border); }
.apf-selected.is-locked .apf-lock { color: var(--vb-text-muted); font-size: 0.8rem; margin-left: auto; }

/* ---- The route line under the two airport fields. */
.apf-route { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 0.6rem; margin: -0.3rem 0 0.9rem; font-size: 0.9rem; color: var(--vb-text-muted); grid-column: 1 / -1; }
.apf-route .mono { color: var(--vb-text); font-weight: 600; }
.apf-route-arrow { color: var(--vb-primary); }
/* The swap control sits between the two airport fields at the label line. */
.apf-swap { grid-column: 1 / -1; display: flex; justify-content: center; margin: -0.4rem 0 0.2rem; }
.apf-swap .btn { padding: 0.15rem 0.6rem; font-size: 0.8rem; }

/* ---- Lookup rows: a registration / type row for the aircraft and type lookups, a schedule row.
        They are .airport-result-list rows with these extra cells. */
.apf-row-type { color: var(--vb-text-muted); font-size: 0.85rem; }
.apf-row-name { color: var(--vb-text-muted); }
.airport-result-list li.is-off { opacity: 0.55; }
.airport-result-list li.is-off .btn { visibility: hidden; }
.apf-group-head { font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); padding: 0.5rem 0.7rem 0.1rem; }
/* The last row of the aircraft lookup: record the typed registration as an external aircraft. */
.apf-external-row { border-top: 1px solid var(--vb-border); margin-top: 0.3rem; padding-top: 0.6rem; }
.apf-external-row .airport-result-main strong { color: var(--vb-primary-bright); }

/* ---- The external aircraft's second row: type then registration, with the mode pill. */
.apf-mode { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; font-weight: 600; padding: 0.1rem 0.6rem; border-radius: 999px; border: 1px solid var(--vb-border-strong); background: var(--vb-surface-raised); color: var(--vb-text); }
.apf-mode-external { border-style: dashed; }
.apf-mode-fleet { border-color: var(--vb-primary); color: var(--vb-primary-bright); background: var(--vb-primary-soft); }

/* ---- Date + time on one line, and hours + minutes. */
.apf-dt { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) auto; gap: 0.5rem; align-items: center; }
.apf-dt .apf-plus { font-size: 0.78rem; color: var(--vb-warning); font-weight: 600; white-space: nowrap; }
.apf-dt .apf-plus:empty { display: none; }
/* product/74 P8: every time is UTC - one guidance line and the current UTC as a reference under the Times
   heading; the fields carry "(UTC)" in their labels. */
.apf-utc { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.25rem 1rem; margin: -0.35rem 0 0.75rem; }
.apf-utc .hint { margin: 0; }
.apf-utc-clock { margin: 0; font-size: 0.8rem; color: var(--vb-text-muted); white-space: nowrap; }
.apf-utc-clock .mono { color: var(--vb-text); margin-left: 0.4rem; }
.apf-hm { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto; gap: 0.4rem; align-items: center; max-width: 16rem; }
.apf-hm span { color: var(--vb-text-muted); font-size: 0.9rem; }
.apf-hm input { text-align: right; }

/* ---- A value from a plan. In the label row; cleared the moment the field is edited. */
.apf-tag { display: inline-flex; align-items: center; margin-left: 0.5rem; padding: 0 0.5rem; border-radius: 999px; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; border: 1px dashed var(--vb-border-strong); color: var(--vb-text-muted); vertical-align: middle; }
.apf-tag-locked { border-style: solid; }

/* ---- The hint that WARNS (block time against the times) rather than informs. */
.hint.apf-warn { color: var(--vb-warning); }
/* The external mode's line: the pill, its sentence and the way back, spaced by the gap. */
.hint.apf-mode-note { display: flex; align-items: center; flex-wrap: wrap; gap: 0.35rem 0.5rem; margin: 0 0 -0.4rem; }
.hint.apf-ok { color: var(--vb-success); }

/* ---- The Pilot card's selected pilot: identity plus facts. */
.apf-pilot { display: flex; align-items: center; gap: 0.8rem; }
.apf-pilot-facts { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.apf-pilot-facts strong { font-size: 0.98rem; }
.apf-pilot-facts .muted { font-size: 0.85rem; }
.apf-pilot .btn-ghost { margin-left: auto; }

/* ---- The Summary card. */
.apf-summary .apf-sum-flight { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.3rem 0.7rem; margin-bottom: 0.7rem; }
.apf-summary .apf-sum-flight .airport-code { font-size: 1.2rem; }
.apf-summary .key-value-list { grid-template-columns: 7.5rem minmax(0, 1fr); font-size: 0.875rem; }
.apf-summary .apf-sum-plan { margin: 0.9rem 0 0; padding: 0.7rem 0.8rem; border: 1px dashed var(--vb-border-strong); border-radius: var(--vb-radius-sm); font-size: 0.85rem; }
.apf-summary .apf-sum-plan strong { display: block; font-size: 0.78rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--vb-text-muted); margin-bottom: 0.3rem; }
.apf-summary .apf-sum-plan ul { margin: 0; padding-left: 1.1rem; }
.apf-summary .apf-sum-next { margin: 0.9rem 0 0; padding-top: 0.8rem; border-top: 1px solid var(--vb-border); font-size: 0.875rem; }
.apf-summary .apf-sum-next strong { display: block; font-size: 0.78rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--vb-text-muted); margin-bottom: 0.2rem; }
.apf-summary .apf-sum-empty { color: var(--vb-text-muted); font-style: normal; }

/* ---- The side column sticks while the long main column scrolls (product/51's grid aligns to the
        top; this keeps the summary and the filing choice in view). Only where there are two columns. */
@media (min-width: 1101px) { .detail-grid-form .detail-side.apf-sticky { position: sticky; top: calc(var(--vb-topbar-height) + 1rem); } }

/* --------------------------------------------------------------------------
   M3C.4 settings hub: section summary cards, setup checklist, brand swatch.
   -------------------------------------------------------------------------- */
.settings-section-card {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
}
.settings-section-card h3 { margin: 0; }
.settings-section-card .settings-facts {
    grid-template-columns: 1fr;
    gap: 0.65rem;
    flex: 1 1 auto;
}
/* Every settings card had exactly ONE action until M5D added a second (Pilot registration ->
   Edit + Questions), and two inline-blocks separated by nothing but whitespace wrapped into what
   looked like a stray link under a button. A wrapping row with a real gap, so a narrow card
   stacks them deliberately instead of by accident. A single action is unaffected. */
.settings-section-actions {
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.checklist-card { margin-bottom: 1.5rem; }
/* The finish-setup action, ruled off from the checklist above it: it acts on the whole list
   rather than being another item in it. */
.checklist-finish {
    margin-top: 1.25rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--vb-border);
}
.checklist-finish p { margin: 0 0 0.75rem; }
.checklist {
    list-style: none;
    margin: 1rem 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 0.6rem 1.25rem;
}
.checklist li {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}
.checklist .badge { flex: 0 0 auto; min-width: 3.4rem; text-align: center; }
.checklist a { color: inherit; }

/* Colour swatch next to hex values (branding form + settings summary). */
.swatch {
    display: inline-block;
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 0.3rem;
    border: 1px solid var(--vb-border);
    vertical-align: -0.25rem;
}
.swatch-input {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}
.swatch-input input { flex: 1 1 auto; }

/* Live "next generated ID" preview on the pilot identifier settings page. */
.identifier-preview {
    margin: 0;
    padding: 0.55rem 0.75rem;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    font-size: 1.05rem;
}

/* Standalone heading between card groups on hub-style pages. */
.section-heading { margin: 2rem 0 0.25rem; }

/* An ACARS flight whose recording was interrupted and resumed: the track jumps across the gap.
   Surfaced on the PIREP so a reviewer never has to puzzle over an unexplained line on the map. */
/* M6E.4 live map, rebuilt as a radar-style ops display (2026-08-06): one full-bleed shell with
   the hover reveal, sidebar and empty state OVERLAID on it (no strips or lists - the planes
   are the interface). The shell needs an
   explicit height - MapLibre renders into whatever box it is given, and an auto-height div
   collapses to nothing (a silently blank map, the CSS-class lesson's cousin). The portal is
   dark-only, so the overlays read the portal's own tokens (product/64): --vb-brand, the colour as
   saved, for the planes and lines; --vb-primary, the readable fill, for the bar, dot, altitude
   series and active borders; --vb-primary-bright for accent text; --vb-primary-soft for tints;
   and the neutral --vb-surface / --vb-border / --vb-text family for everything structural. */
.live-shell {
  /* The sidebar's width, read by live-map.js to keep a framed flight clear of it. */
  --live-side-width: 256px;
  position: relative;
  height: 72vh;
  min-height: 500px;
  border: 1px solid var(--vb-border);
  border-radius: 14px;
  overflow: hidden;
  background: #0d0d13;
}
.live-canvas { position: absolute; inset: 0; }

/* Aircraft: the shared silhouette (plane-marker.js), nose up and rotated to the heading, a flat
   solid fill in the airline's exact colour (--vb-brand) with no shadow, glow or ring. The outline
   appears only where the colour is close to the tiles (a near-black airline); the selected aircraft
   keeps its colour, grows 18% and takes a thin edge in white or near-black, whichever stands
   further from it, and its trail is drawn heavier. plane-marker.js sets the colour and outline per
   marker through CSSOM. Hover and selection scale the inner svg only: MapLibre owns the marker
   element's transform (product/64). */
.live-plane { color: var(--plane-colour, var(--vb-brand)); cursor: pointer; line-height: 0; }
.live-plane svg { display: block; overflow: visible; filter: none; transition: transform 0.12s ease; }
.live-plane .plane-body { fill: currentColor; stroke: var(--plane-outline, #0b0c10); stroke-opacity: var(--plane-outline-opacity, 0); stroke-width: 1.25px; stroke-linejoin: round; paint-order: stroke; vector-effect: non-scaling-stroke; }
.live-plane:hover svg { transform: scale(1.12); }
.live-plane.live-plane-selected { color: var(--plane-colour, var(--vb-brand)); }
/* The selected size holds while the pointer is still on the plane (PR #329 review): the hovered form
   is named so it outranks the hover rule above. */
.live-plane.live-plane-selected svg, .live-plane.live-plane-selected:hover svg { transform: scale(1.18); }
.live-plane-selected .plane-body { stroke: var(--plane-select-outline, #ffffff); stroke-opacity: 1; stroke-width: 1.25px; }
.live-plane-stale, .live-plane-stale.live-plane-selected { color: var(--vb-warning); opacity: 0.8; }

/* Empty state, centred on the glass. */
.live-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  pointer-events: none;
}
/* The empty state sits on GLASS, never as bare text over map labels (Myles: it looked bad). */
.live-overlay-box {
  text-align: center;
  max-width: 320px;
  background: rgba(20, 21, 25, 0.92);
  border: 1px solid var(--vb-border);
  border-radius: 12px;
  padding: 16px 20px;
}
.live-overlay-text { color: var(--vb-text-muted); font-size: 0.84rem; line-height: 1.55; margin: 0; }

/* Airport endpoint chips (DOM markers - no glyph dependency on the tile style). */
.live-map-airport {
  font-family: var(--vb-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--vb-text-muted);
  background: rgba(20, 21, 25, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 6px;
  padding: 1px 5px;
  pointer-events: none;
}
/* A chip that knows its airport names it on hover, as the sidebar's codes do. It sits beneath the
   aircraft (live-map.js places it there), and comes above them only while it is being read. */
/* Absolute, as the map places every marker: the tooltip's own relative position would put the chip
   back in the page's flow and stretch it across the map. */
.live-map-airport.live-tip { position: absolute; pointer-events: auto; }
.live-map-airport.live-tip:hover { z-index: 1; color: var(--vb-text); }
.live-map-airport.live-tip:hover::after { letter-spacing: normal; }

/* The hover mini card (a restyled MapLibre popup - chrome overrides below). */
.live-hover { min-width: 175px; }
.live-hover-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.live-hover-callsign { font-size: 0.82rem; font-weight: 600; color: var(--vb-text); }
.live-hover-route { font-family: var(--vb-mono); font-size: 0.69rem; color: var(--vb-text-muted); }
.live-hover-meta { font-size: 0.75rem; color: var(--vb-text-muted); margin-top: 4px; }
.live-hover-age { font-size: 0.69rem; color: #7f838c; margin-top: 4px; }

/* Shared thin progress bar (hover card + sidebar hero). */
.live-bar {
  position: relative;
  height: 3px;
  background: var(--vb-border);
  border-radius: 2px;
  margin-top: 7px;
}
.live-bar-fill { height: 100%; background: var(--vb-primary); border-radius: 2px; }

/* The map overlays' scrollbars are the portal's one neutral scrollbar (see the rule beside
   --vb-scrollbar's use above). */

/* The flight sidebar, overlaid on the map's right edge. */
.live-sidebar {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--live-side-width);
  background: rgba(20, 21, 25, 0.97);
  border-left: 1px solid var(--vb-border);
  padding: 14px 16px;
  overflow-y: auto;
  z-index: 4;
}
.live-stale-banner {
  background: rgba(217, 161, 60, 0.12);
  border: 1px solid rgba(217, 161, 60, 0.4);
  color: #e6b45c;
  border-radius: 8px;
  padding: 7px 10px;
  font-size: 0.72rem;
  line-height: 1.45;
  margin-bottom: 12px;
}
.live-side-head { display: flex; justify-content: space-between; align-items: center; }
.live-side-title { font-size: 1rem; font-weight: 600; color: var(--vb-primary-bright); }
.live-side-close {
  background: none;
  border: none;
  color: var(--vb-text-muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}
.live-side-close:hover { color: var(--vb-text); }
.live-side-phase { font-size: 0.75rem; color: var(--vb-text-muted); margin-top: 2px; }

.live-side-route { margin-top: 14px; }
.live-side-codes { display: flex; justify-content: space-between; }
.live-side-code { font-family: var(--vb-mono); font-size: 1rem; font-weight: 600; color: var(--vb-text); }
.live-side-bar { height: 4px; margin-top: 8px; }
.live-side-dot {
  position: absolute;
  top: -4px;
  width: 11px;
  height: 11px;
  margin-left: -6px;
  border-radius: 50%;
  background: var(--vb-primary);
  border: 2px solid var(--vb-bg-soft);
  box-shadow: 0 0 6px var(--vb-primary-soft);
}
.live-side-routemeta { font-size: 0.69rem; color: var(--vb-text-muted); margin-top: 6px; }
/* The ETA, made obvious: a bold accent span riding the distance line. */
.live-side-eta {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--vb-primary-bright);
}

.live-side-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin-top: 14px;
}
.live-side-cell { background: var(--vb-surface-raised); border-radius: 7px; padding: 7px 10px; }
.live-side-label { font-size: 0.63rem; color: var(--vb-text-muted); }
.live-side-value { font-family: var(--vb-mono); font-size: 0.82rem; color: var(--vb-text); margin-top: 1px; }

.live-side-pilot {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--vb-border);
}
.live-side-pilotname { font-size: 0.82rem; font-weight: 600; color: var(--vb-text); }

.live-side-facts { margin-top: 13px; padding-top: 12px; border-top: 1px solid var(--vb-border); }
.live-fact { display: flex; justify-content: space-between; font-size: 0.75rem; padding: 3px 0; gap: 10px; }
.live-fact-label { color: var(--vb-text-muted); flex: 0 0 auto; }
.live-fact-value { color: #b4b6bc; text-align: right; }

/* The filed route: mono, wrapped, quietly scrollable when a long airways string arrives. */
.live-side-routesection { margin-top: 13px; padding-top: 12px; border-top: 1px solid var(--vb-border); }
.live-side-routetext {
  font-family: var(--vb-mono);
  font-size: 0.69rem;
  line-height: 1.5;
  color: #b4b6bc;
  overflow-wrap: anywhere;
  max-height: 76px;
  overflow-y: auto;
  margin-top: 4px;
}

/* The ICAO hover tooltip: pure CSS from a data attribute (attr() renders TEXT, so a server
   string can never become markup here). */
.live-tip { position: relative; cursor: help; }
.live-tip:hover::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 7px);
  left: 0;
  background: var(--vb-surface);
  border: 1px solid var(--vb-border-strong);
  border-radius: 7px;
  padding: 6px 10px;
  font-family: var(--vb-font, inherit);
  font-size: 0.69rem;
  font-weight: 400;
  line-height: 1.4;
  color: #b4b6bc;
  width: max-content;
  max-width: 190px;
  z-index: 12;
}
.live-tip-right:hover::after { left: auto; right: 0; }
.live-tip-centre:hover::after { left: 50%; transform: translateX(-50%); }

/* Speed & altitude over the flight so far: two self-scaled series from the trail's own
   samples, ceilings + time span beneath. Altitude in the airline's readable fill; ground speed in
   amber, or a cool blue beside a yellow, orange or amber airline (plane-marker.js secondSeries;
   live-map.js sets --chart-second on the sidebar), so the two lines never share a hue. */
.live-side-graphsection { margin-top: 13px; padding-top: 12px; border-top: 1px solid var(--vb-border); }
.live-graph-head { display: flex; justify-content: space-between; align-items: baseline; }
.live-graph-legend { display: flex; gap: 8px; font-size: 0.63rem; }
.live-graph-key { text-transform: uppercase; letter-spacing: 0.05em; }
.live-graph-key-alt { color: var(--vb-primary-bright); }
.live-graph-key-gs { color: var(--chart-second, #e8a33d); }
.live-graph {
  display: block;
  width: 100%;
  height: 76px;
  margin-top: 6px;
  background: var(--vb-bg-soft);
  border-radius: 7px;
}
.live-graph-altfill { fill: var(--vb-primary-soft); stroke: none; }
.live-graph-alt { fill: none; stroke: var(--vb-primary); stroke-width: 1.6; }
.live-graph-gs { fill: none; stroke: var(--chart-second, #e8a33d); stroke-width: 1.4; }
.live-graph-scale {
  display: flex;
  justify-content: space-between;
  font-size: 0.63rem;
  color: var(--vb-text-muted);
  margin-top: 4px;
}

.live-side-actions { display: flex; gap: 8px; margin-top: 15px; }
.live-action {
  flex: 1;
  font-size: 0.75rem;
  color: #b4b6bc;
  background: none;
  border: 1px solid var(--vb-border-strong);
  border-radius: 7px;
  padding: 7px 0;
  cursor: pointer;
}
.live-action:hover { border-color: var(--vb-primary); color: var(--vb-text); }
.live-action-active {
  background: var(--vb-primary-soft);
  border-color: var(--vb-primary);
  color: var(--vb-primary-bright);
}

/* The "not reporting" tray on the Live map: a chip on the glass, top-left, that opens a short list
   of the flights ACARS has stopped hearing from. It is the exceptions tray, not a flight strip -
   the planes stay the interface (Myles, 2026-08-06) - so it lists ONLY silent flights and is
   absent when there are none. */
.live-quiet { position: absolute; top: 12px; left: 12px; z-index: 3; max-width: min(340px, calc(100% - 24px)); }
.live-quiet-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  color: #e6b45c;
  background: rgba(20, 21, 25, 0.92);
  border: 1px solid rgba(217, 161, 60, 0.45);
  border-radius: 999px;
  padding: 5px 12px 5px 10px;
  cursor: pointer;
}
.live-quiet-chip:hover, .live-quiet-chip[aria-expanded="true"] { background: rgba(43, 33, 14, 0.95); border-color: rgba(217, 161, 60, 0.7); }
.live-quiet-dot { width: 7px; height: 7px; border-radius: 50%; background: #d9a13c; flex: none; }
.live-quiet-panel {
  margin-top: 8px;
  background: rgba(20, 21, 25, 0.97);
  border: 1px solid var(--vb-border);
  border-radius: 12px;
  padding: 10px 12px 12px;
}
.live-quiet-lede { color: var(--vb-text-muted); font-size: 0.74rem; line-height: 1.5; margin: 0 0 6px; }
.live-quiet-list { list-style: none; margin: 0; padding: 0; }
.live-quiet-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 10px; align-items: center; padding: 8px 0; border-top: 1px solid var(--vb-border); }
.live-quiet-row:first-child { border-top: none; }
.live-quiet-flight { font-size: 0.84rem; font-weight: 600; color: var(--vb-text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-quiet-flight .mono { font-weight: 400; color: var(--vb-text-muted); margin-left: 6px; font-size: 0.75rem; }
.live-quiet-meta { grid-column: 1; font-size: 0.72rem; color: var(--vb-text-muted); }
.live-quiet-meta strong { color: #e6b45c; font-weight: 600; }
.live-quiet-row .live-action { grid-column: 2; grid-row: 1 / span 2; flex: none; padding: 5px 12px; }

/* End flight: the one destructive action in the sidebar, offered only for a quiet flight, on a row
   of its own under Focus and Follow (a third of 256px clips its label). */
.live-side-actions-end { flex-wrap: wrap; }
.live-side-actions-end .live-action-danger { flex: 1 0 100%; }
.live-action-danger { color: #f19aa3; border-color: rgba(229, 72, 77, 0.45); }
.live-action-danger:hover { color: #fff; border-color: var(--vb-danger); background: rgba(229, 72, 77, 0.18); }

/* Narrow screens: the sidebar takes the full glass, and the quiet list spans it (product/53). */
@media (max-width: 700px) {
  .live-shell { --live-side-width: 100%; }
  .live-sidebar { border-left: none; }
  .live-quiet { right: 12px; max-width: none; }
}

/* product/72, the Fleet Map: the Fleet aircraft page's Map view, on the live map's shell and sidebar. */

/* The List | Map control on the Fleet list's filter bar, the finder's shape in the portal's tokens. */
.afm-seg { display: inline-flex; margin-left: auto; border: 1px solid var(--vb-border-strong); border-radius: var(--vb-radius-sm); overflow: hidden; }
.afm-seg button { display: inline-flex; align-items: center; gap: 7px; height: 36px; padding: 0 14px; border: none; background: transparent; color: var(--vb-text-muted); font: inherit; font-size: 0.84rem; font-weight: 600; cursor: pointer; }
.afm-seg button + button { border-left: 1px solid var(--vb-border-strong); }
.afm-seg button[aria-pressed="true"] { background: var(--vb-surface-raised); color: var(--vb-text); }
.afm-seg svg { width: 14px; height: 14px; }

/* The summary line under the filter bar, and the two groups the map cannot draw. */
.afm-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin: 0 0 0.75rem; font-size: 0.9rem; color: var(--vb-text-muted); }
.afm-fig b { color: var(--vb-text); font-weight: 700; font-size: 1rem; }
.afm-fig-btn { display: inline-flex; align-items: center; gap: 7px; padding: 4px 12px; border-radius: 999px; border: 1px solid var(--vb-border-strong); background: var(--vb-surface); color: inherit; font: inherit; font-size: 0.86rem; cursor: pointer; }
.afm-fig-btn:hover { border-color: var(--vb-primary-bright); color: var(--vb-text); }
.afm-fig-btn.on { border-color: var(--vb-primary-bright); background: var(--vb-primary-soft); color: var(--vb-text); }
.afm-bar-sep { width: 1px; height: 18px; background: var(--vb-border-strong); }

/* The shell is the live map's (.live-shell) with a wider sidebar for the fleet's facts. */
.afm-shell { --live-side-width: 340px; }
.afm-tools { position: absolute; top: 12px; right: 12px; z-index: 3; display: flex; gap: 6px; }
.afm-has-side .afm-tools { right: calc(var(--live-side-width) + 12px); }
.afm-tool { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 11px; border-radius: 9px; border: 1px solid var(--vb-border-strong); background: rgba(20, 21, 25, 0.92); color: var(--vb-text); font: inherit; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
.afm-tool svg { width: 14px; height: 14px; }
.afm-tool:hover { border-color: var(--vb-primary-bright); }
.afm-has-side .maplibregl-ctrl-bottom-right { right: var(--live-side-width); }
.afm-expanded .afm-shell { position: fixed; inset: 0; z-index: 60; height: auto; min-height: 0; border: none; border-radius: 0; }

/* Airport chips and clusters: the website's, in the portal's tokens. Accent when an aircraft there
   is Available; dashed when every aircraft there is only assumed. */
.afm-chip { display: inline-flex; flex-direction: column; align-items: stretch; gap: 4px; padding: 5px 8px 6px; border-radius: 10px; border: 1px solid var(--vb-border-strong); background: rgba(20, 21, 25, 0.93); color: var(--vb-text); font: inherit; cursor: pointer; box-shadow: var(--vb-shadow); white-space: nowrap; }
.afm-chip:hover { border-color: var(--vb-primary-bright); }
.afm-chip-top { display: flex; align-items: center; gap: 8px; }
.afm-chip-code { font-family: var(--vb-mono); font-size: 11.5px; font-weight: 700; letter-spacing: 0.05em; }
.afm-chip-n { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; font-size: 11px; font-weight: 700; background: var(--vb-surface-raised); color: var(--vb-text-muted); }
.afm-chip-avail { border-color: color-mix(in srgb, var(--vb-primary) 60%, transparent); }
.afm-chip-avail .afm-chip-n { background: var(--vb-primary); color: var(--vb-on-primary); }
.afm-chip-assumed { border-style: dashed; }
.afm-chip-sel { background: var(--vb-primary); border-color: var(--vb-primary); color: var(--vb-on-primary); }
.afm-chip-sel .afm-chip-n { background: rgba(255, 255, 255, 0.22); color: inherit; }
.afm-chip-mix { display: flex; gap: 1px; height: 2px; border-radius: 2px; overflow: hidden; }
.afm-chip-mix i { display: block; height: 100%; }
.afm-mix-a { background: var(--vb-success); }
.afm-mix-r { background: var(--vb-warning); }
.afm-mix-o { background: var(--vb-border-strong); }
.afm-cluster { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--vb-primary-bright) 70%, transparent); background: rgba(20, 21, 25, 0.93); color: var(--vb-text); font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; box-shadow: var(--vb-shadow); }
.afm-dest { font-family: var(--vb-mono); font-size: 10.5px; font-weight: 700; letter-spacing: 0.05em; padding: 2px 7px; border-radius: 6px; border: 1px solid color-mix(in srgb, var(--vb-primary) 60%, transparent); background: rgba(20, 21, 25, 0.9); color: var(--vb-text); pointer-events: none; }

/* The sidebar's contents: the three levels share the live map's sidebar chrome. */
.afm-back { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 8px; padding: 0; border: none; background: none; color: var(--vb-primary-bright); font: inherit; font-size: 0.84rem; font-weight: 600; cursor: pointer; }
.afm-back svg { width: 14px; height: 14px; }
.afm-side-sub { font-size: 0.8rem; color: var(--vb-text-muted); margin-top: 2px; }
.afm-side-counts { display: flex; flex-wrap: wrap; gap: 6px 10px; margin: 8px 0 4px; font-size: 0.8rem; color: var(--vb-text-muted); }
.afm-side-counts b { color: var(--vb-text); font-weight: 600; }
.afm-rows { display: flex; flex-direction: column; margin-top: 6px; }
.afm-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 10px; align-items: center; width: 100%; padding: 9px 0; border: none; border-top: 1px solid var(--vb-border); background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; text-decoration: none; }
.afm-row:first-child { border-top: none; }
.afm-row:hover .afm-row-reg { color: var(--vb-primary-bright); }
.afm-row-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.afm-row-reg { display: flex; align-items: center; gap: 8px; font-family: var(--vb-mono); font-size: 0.92rem; font-weight: 700; }
.afm-row-sub { font-size: 0.78rem; color: var(--vb-text-muted); line-height: 1.4; }
.afm-row-end { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.afm-row-end .badge { font-size: 0.7rem; }
.afm-row-end .afm-track { font-size: 0.8rem; font-weight: 600; white-space: nowrap; }
.afm-ident { display: flex; flex-direction: column; gap: 3px; margin: 6px 0 10px; }
.afm-ident-badges { display: flex; flex-wrap: wrap; gap: 6px; }
.afm-ident-name { font-size: 0.95rem; color: var(--vb-text); }
.afm-ident-type { font-size: 0.84rem; color: var(--vb-text-muted); }
.afm-card { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; margin-top: 8px; border-radius: 10px; border: 1px solid var(--vb-border); background: var(--vb-surface); }
.afm-card-assumed { border-style: dashed; }
.afm-k { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase; color: var(--vb-text-muted); }
.afm-v { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 0.92rem; }
.afm-v .mono { font-weight: 700; }
.afm-sent { margin: 0; font-size: 0.8rem; line-height: 1.45; color: var(--vb-text-muted); }
.afm-sent-warn { color: var(--vb-warning); }
.afm-h { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin: 12px 0 4px; }
.afm-h h3 { margin: 0; font-size: 0.9rem; }
.afm-h a { font-size: 0.8rem; font-weight: 600; white-space: nowrap; }
.afm-flts { display: flex; flex-direction: column; gap: 6px; }
.afm-flt { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 8px 10px; align-items: center; padding: 8px 10px; border-radius: 9px; border: 1px solid var(--vb-border); background: var(--vb-surface); color: inherit; font-size: 0.84rem; text-decoration: none; }
.afm-flt:hover { border-color: var(--vb-border-strong); text-decoration: none; }
.afm-flt-no { font-weight: 700; font-family: var(--vb-mono); }
.afm-flt-route { font-family: var(--vb-mono); color: var(--vb-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.afm-flt-end { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; font-size: 0.76rem; color: var(--vb-text-muted); white-space: nowrap; }
.afm-flt-end .badge { font-size: 0.68rem; }
.afm-flt-sub { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; font-size: 0.78rem; color: var(--vb-text-muted); }
.afm-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 12px; }
.afm-actions .afm-sep { flex-basis: 100%; height: 0; }
.afm-foot { margin: 10px 0 0; font-size: 0.78rem; color: var(--vb-text-muted); }

@media (max-width: 640px) {
  .afm-seg { margin-left: 0; }
  .afm-shell { --live-side-width: 100%; }
  .afm-has-side .afm-tools { display: none; }
  .afm-has-side .maplibregl-ctrl-bottom-right { right: 0; }
}

/* Fleet operations (product/76): the Fleet operations settings card, the switch dialog's facts,
   the activity line under an Availability pill and the fleet map's per-flight rows. Merged from
   design/fleet-operations/proposal.css (its mono token named as this file names it, --vb-mono);
   nothing here restyles a shipped class. */
/* The Fleet operations card: the hint above the three mode cards, and the one line beneath them
   that says aircraft rules are not pilot rules. Both are the settings page's own .hint voice. */
.fops-hint { margin: 0 0 0.9rem; }
.fops-pilot-note { margin: 0.9rem 0 0; }
/* A mode card's name carries the mode in bold and today's mode in a quiet badge beside it. */
.radio-option-body .fops-current { margin-left: 0.5rem; vertical-align: middle; }

/* The switch dialog's facts: a plain list, one consequence per line, the contested aircraft as
   the review page's amber panel with a small table inside it. */
.fops-facts { margin: 0.5rem 0 0; padding-left: 1.1rem; display: grid; gap: 0.35rem; }
.fops-facts li { margin: 0; }
.fops-contested { margin: 0.6rem 0 0.2rem; }
.fops-contested td, .fops-contested th { padding: 0.3rem 0.6rem; }

/* The activity line under an Availability pill: the flights using the aircraft right now, each a
   link to its dispatch, as one quiet line that wraps. A table cell and the map's rows share it. */
.fleet-activity { display: grid; gap: 0.1rem; font-size: 0.82rem; color: var(--vb-text-muted); line-height: 1.4; }
.fleet-activity-head { font-weight: 600; }
.fleet-activity-flight { white-space: nowrap; }
/* The Fleet list's identity cells: the registration (or type code) over one muted line of detail, so the
   table keeps seven columns and its Status and Edit stay on screen at a desktop width. */
.fleet-aircraft-id { display: grid; gap: 0.1rem; }
.fleet-aircraft-id .muted { font-size: 0.82rem; }
/* The aircraft page's one sentence under its statistics and its utilisation chart (shared fleets). */
.stat-grid-note { margin: 0.5rem 0 0; font-size: 0.85rem; }
/* A flight the reader cannot open is the same code, unlinked. */
.fleet-activity-flight a, .fleet-activity-flight .fleet-activity-no { font-family: var(--vb-mono); }

/* The fleet map's Active flights group: one row per flight (not per aircraft), so a tail flown by
   three pilots is three rows, each with its own Track flight. Reuses the sidebar's .afm-row; the
   registration never breaks at its hyphen, the badges wrap after it. */
.afm-row-flight .afm-row-reg { flex-wrap: wrap; row-gap: 0.25rem; }
.afm-row-flight .afm-row-reg a, .afm-row-flight .afm-row-reg b { white-space: nowrap; }
.afm-row-flight .afm-row-reg .badge { margin-left: 0.4rem; }
.afm-act { font-size: 0.78rem; color: var(--vb-text-muted); margin-left: 0.5rem; white-space: nowrap; }

/* MapLibre's stock chrome is WHITE and glares on the dark map. ONE quiet set for EVERY map in the
   portal (product/44, Myles on PR #193: "tone them way down", the customer template's rule): the
   lists that named #live-map and .route-map missed the airport and replay maps. `.maplibregl-map` is
   on every map container, and (0,2,0) outranks the vendor stylesheet (which App.razor loads before
   app.css, the M5D Quill lesson); the compact attribution is named at three classes to beat the
   vendor's two. The zoom keeps its size but is see-through and dim until the pointer is on it; the
   attribution is never hidden (the OpenStreetMap licence), just one small dim line. */
.maplibregl-map .maplibregl-ctrl-group {
  background: rgba(16, 17, 21, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 8px;
  box-shadow: none;
  backdrop-filter: blur(4px);
}
.maplibregl-map .maplibregl-ctrl-group button { background-color: transparent; }
.maplibregl-map .maplibregl-ctrl-group button:not(:disabled):hover { background-color: rgba(255, 255, 255, 0.07); }
.maplibregl-map .maplibregl-ctrl-group button + button { border-top: 1px solid rgba(255, 255, 255, 0.08); }
.maplibregl-map .maplibregl-ctrl button .maplibregl-ctrl-icon { filter: invert(1); opacity: 0.6; }
.maplibregl-map .maplibregl-ctrl button:hover .maplibregl-ctrl-icon { opacity: 0.95; }
.maplibregl-map .maplibregl-ctrl-attrib,
.maplibregl-map .maplibregl-ctrl-attrib.maplibregl-compact,
.maplibregl-map .maplibregl-ctrl-attrib.maplibregl-compact-show {
  background: rgba(12, 13, 15, 0.35);
  color: #9a9da5;
  font-size: 10px;
  line-height: 1.4;
  border-radius: 6px 0 0 0;
  opacity: 0.7;
  transition: opacity 0.15s ease;
}
.maplibregl-map .maplibregl-ctrl-attrib:hover { opacity: 1; }
.maplibregl-map .maplibregl-ctrl-attrib a { color: inherit; }
.maplibregl-map .maplibregl-ctrl-attrib-button { filter: invert(1); opacity: 0.4; }
/* The hover mini card rides a MapLibre popup, whose stock chrome is WHITE - restyled dark to
   belong to the map (same load-order-proof #live-map scoping as the buttons above).
   .route-map joined at M6I.4 exactly as the template's own copy of these rules already had:
   the debrief map's event popups rendered stock white on a dark basemap, caught by hovering
   the harness rather than by any test - the silent-CSS class again. */
#live-map .maplibregl-popup-content,
.route-map .maplibregl-popup-content {
  background: var(--vb-surface);
  border: 1px solid var(--vb-border-strong);
  border-radius: 9px;
  padding: 10px 12px;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45);
}
#live-map .maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
.route-map .maplibregl-popup-anchor-bottom .maplibregl-popup-tip { border-top-color: var(--vb-border-strong); }
#live-map .maplibregl-popup-anchor-top .maplibregl-popup-tip,
.route-map .maplibregl-popup-anchor-top .maplibregl-popup-tip { border-bottom-color: var(--vb-border-strong); }
#live-map .maplibregl-popup-anchor-left .maplibregl-popup-tip,
.route-map .maplibregl-popup-anchor-left .maplibregl-popup-tip { border-right-color: var(--vb-border-strong); }
#live-map .maplibregl-popup-anchor-right .maplibregl-popup-tip,
.route-map .maplibregl-popup-anchor-right .maplibregl-popup-tip { border-left-color: var(--vb-border-strong); }

/* .notice-warning is the same block outside the review page - an amber "worth knowing" panel
   that is not an error (M10.2's inactive-event notice was the first). It joins this rule's
   SELECTOR LIST rather than copying the declarations, so the two can never drift apart; the
   child and scroll-margin rules below stay PIREP-only because they exist for the decision
   bar's jump-to-flag popup. */
.pirep-warning,
.notice-warning,
.notice-warning-strong {
    margin: 0.85rem 0;
    padding: 0.75rem 0.9rem;
    border-radius: var(--vb-radius-sm);
    border: 1px solid color-mix(in srgb, var(--vb-warning) 45%, var(--vb-border));
    background: color-mix(in srgb, var(--vb-warning) 10%, transparent);
}
/* The loud variant, for the one case where an edit silently changes what pilots already
   earned. It inherits the amber block above and then raises it: a solid left edge, a stronger
   fill and a heading, because "worth knowing" and "you are about to move somebody's award"
   should not look the same. Full border stays, so the left accent is a widened edge rather
   than a single-sided border with rounded corners. */
.notice-warning-strong {
    border-width: 1px 1px 1px 4px;
    border-left-color: var(--vb-warning);
    background: color-mix(in srgb, var(--vb-warning) 16%, transparent);
    padding: 0.9rem 1.1rem;
}
/* Only the warning's heading - a strong that is a direct child - is a block: a bold word inside one
   of its paragraphs ("both", "finish") stays in its sentence. */
.notice-warning-strong > strong { display: block; margin-bottom: 0.3rem; }
.pirep-warning p { margin: 0 0 0.35rem; }
.notice-warning p, .notice-warning-strong p { margin: 0 0 0.35rem; }
.notice-warning p:last-child, .notice-warning-strong p:last-child { margin-bottom: 0; }
/* Data Ops: the pilots an import would count twice (product/20). The number is spaced from the
   name by CSS, never a typed space (the pill-spacing rule). */
.notice-warning-strong .overlap-table { margin: 0.6rem 0 0.75rem; }
.notice-warning-strong .checkbox-field { margin: 0.6rem 0 0; }
.overlap-number { margin-right: 0.5rem; color: var(--vb-text-muted); }
.pirep-warning ul { margin: 0.35rem 0; padding-left: 1.1rem; font-size: 0.9rem; }
/* Each warning is an anchor target for the decision bar's flag popup, and the topbar is sticky -
   without this the block a reviewer jumped to would land underneath it. */
.pirep-warning { scroll-margin-top: 5rem; }
/* The jump is from the docked bar at the bottom of the page to a warning at the top, which
   lands with no sense of where you went. Sliding there says it. Honours the OS setting, because
   a long animated scroll is exactly what someone who asked for less motion is asking to avoid. */
@media (prefers-reduced-motion: no-preference) {
    html { scroll-behavior: smooth; }
}

/* The flag popup on the decision bar: hovering (or focusing) the count names every flag and
   links to its warning. CSS-only, because a tooltip that needs a round trip to the server to
   say what is already on the page is a worse tooltip.

   Not .live-tip, which is the other hover tooltip on this platform: that one renders attr() into
   a pseudo-element, and a pseudo-element cannot contain the links this needs. */
.flag-tip { position: relative; cursor: help; }
.flag-tip-panel {
    /* The rise, named once: the panel sits this far above the badge AND its invisible bridge is
       exactly this tall, so the two can never disagree (see ::after below). Enough to clear the
       docked bar's top edge, so the panel floats above the bar rather than merging into it. */
    --flag-tip-rise: 1.75rem;
    display: none;
    position: absolute;
    /* Upward. The bar is docked at the BOTTOM of the viewport, so a panel below it would open
       off-screen. left:0 keeps it inside the page - the badge sits at the bar's left edge. */
    bottom: calc(100% + var(--flag-tip-rise));
    left: 0;
    z-index: 40;
    /* Wide enough that the longest flag title this page produces stays on one line - a two-line
       wrap in a list of links reads as two entries. */
    min-width: 20rem;
    max-width: min(26rem, 80vw);
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.7rem 0.85rem;
    border-radius: var(--vb-radius);
    border: 1px solid var(--vb-border-strong);
    background: var(--vb-surface-raised);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
    /* The badge is small, uppercase-ish and centred; the panel is prose and must not inherit it. */
    font-size: 0.82rem;
    font-weight: 500;
    letter-spacing: normal;
    text-align: left;
    line-height: 1.35;
    cursor: auto;
}
/* ⚠️ THE BRIDGE. Hover propagates from a descendant, so the panel stays open while the pointer is
   inside it - but the pointer has to GET there, and crossing empty space between the badge and
   the panel is over neither, which closes the panel before it can be reached. An invisible child
   spanning exactly the rise keeps the pointer inside .flag-tip the whole way. It exists only
   while the panel does, so it never sits over the bar catching clicks at rest. */
.flag-tip-panel::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: var(--flag-tip-rise);
}
.flag-tip:hover .flag-tip-panel,
.flag-tip:focus-visible .flag-tip-panel,
.flag-tip:focus-within .flag-tip-panel { display: flex; }
.flag-tip-heading {
    color: var(--vb-text-muted);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 0.2rem;
}
/* Comfortable rows rather than a dense stack: these are click targets, not a paragraph. */
.flag-tip-panel a {
    color: var(--vb-primary-bright);
    padding: 0.16rem 0;
}
.flag-tip-panel a:hover { text-decoration: underline; }

/* --------------------------------------------------------------------------
   Rich text editor (M5D)
   --------------------------------------------------------------------------
   Quill's snow theme ships light-mode chrome and draws its toolbar and content
   as TWO separately bordered boxes, so out of the box it sits in a dark form
   as a pale double-bordered rectangle beside the rounded inputs above it.
   These overrides make it read as one control matching .field input: the same
   surface, the same border token, the same radius, and rounding only on the
   outer corners so the two boxes look like one. */

.rich-text-editor .ql-toolbar.ql-snow,
.rich-text-editor .ql-container.ql-snow {
    border-color: var(--vb-border-strong);
    background: var(--vb-bg-soft);
    font-family: inherit;
}

.rich-text-editor .ql-toolbar.ql-snow {
    border-top-left-radius: var(--vb-radius-sm);
    border-top-right-radius: var(--vb-radius-sm);
    /* The toolbar's bottom edge and the content's top edge would otherwise draw
       two adjacent 1px lines. */
    border-bottom: 0;
    background: var(--vb-surface-raised);
}

.rich-text-editor .ql-container.ql-snow {
    border-bottom-left-radius: var(--vb-radius-sm);
    border-bottom-right-radius: var(--vb-radius-sm);
    border-top: 1px solid var(--vb-border-strong);
    color: var(--vb-text);
    font-size: 0.95rem;
}

.rich-text-editor .ql-editor {
    min-height: 9rem;
    padding: 0.55rem 0.8rem;
}

.rich-text-editor .ql-editor.ql-blank::before {
    color: var(--vb-text-muted);
    font-style: normal;
    left: 0.8rem;
    right: 0.8rem;
}

/* Toolbar icons are SVG strokes/fills, so they need recolouring rather than a
   colour property. Resting muted, hover full-strength text, ACTIVE the brand.

   Two things worth knowing before editing any of this:

   1. --vb-primary-BRIGHT for the active state, not --vb-primary. The token comments at the top of
      this file say it outright - #5e00ea lacks contrast as a text colour on dark and is meant for
      button/focus FILLS. A toolbar icon is iconography on a dark surface, so it takes the tint.

   2. The active rules below are prefixed `.ql-toolbar.ql-snow` where Quill's own are
      `.ql-snow.ql-toolbar`, which puts every one of ours exactly one class AHEAD. That is
      deliberate. Matching Quill's specificity leaves the outcome decided purely by which
      stylesheet loads last - which is how the active colour stayed Quill's blue until the link
      order in App.razor was corrected. Load order is now right AND these win on their own, so
      reordering the links can no longer silently un-theme the editor. */
.rich-text-editor .ql-snow .ql-stroke { stroke: var(--vb-text-muted); }
.rich-text-editor .ql-snow .ql-fill { fill: var(--vb-text-muted); }
.rich-text-editor .ql-snow .ql-picker { color: var(--vb-text-muted); }

.rich-text-editor .ql-toolbar.ql-snow button:hover .ql-stroke,
.rich-text-editor .ql-toolbar.ql-snow button:hover .ql-stroke-miter,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-label:hover .ql-stroke { stroke: var(--vb-text); }
.rich-text-editor .ql-toolbar.ql-snow button:hover .ql-fill,
.rich-text-editor .ql-toolbar.ql-snow button:hover .ql-stroke.ql-fill,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-label:hover .ql-fill { fill: var(--vb-text); }
.rich-text-editor .ql-toolbar.ql-snow button:hover,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-label:hover,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-item:hover { color: var(--vb-text); }

/* Every shape Quill paints #06c on. Note it also sets a plain colour on the picker LABEL (the
   "Normal" heading text), which is not an SVG and would otherwise stay blue beside the recoloured
   icons next to it - so the `color` rules are not redundant with the stroke/fill ones. */
.rich-text-editor .ql-toolbar.ql-snow button.ql-active .ql-stroke,
.rich-text-editor .ql-toolbar.ql-snow button.ql-active .ql-stroke-miter,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-label.ql-active .ql-stroke,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-item.ql-selected .ql-stroke,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-item.ql-selected .ql-stroke-miter { stroke: var(--vb-primary-bright); }
.rich-text-editor .ql-toolbar.ql-snow button.ql-active .ql-fill,
.rich-text-editor .ql-toolbar.ql-snow button.ql-active .ql-stroke.ql-fill,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-label.ql-active .ql-fill,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-item.ql-selected .ql-fill,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-item.ql-selected .ql-stroke.ql-fill { fill: var(--vb-primary-bright); }
.rich-text-editor .ql-toolbar.ql-snow button.ql-active,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-label.ql-active,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-item.ql-selected { color: var(--vb-primary-bright); }

/* Links WRITTEN INTO the terms also render in Quill's blue while being edited. */
.rich-text-editor .ql-editor a { color: var(--vb-primary-bright); }
/* ...and blockquote is in the toolbar, so its light rule shows up on dark too. `.ql-snow` in the
   middle for the same reason as the toolbar rules above: Quill's is `.ql-snow .ql-editor
   blockquote`, and matching it exactly left the #ccc edge winning on load order alone. */
.rich-text-editor .ql-snow .ql-editor blockquote { border-left-color: var(--vb-border-strong); }

/* The heading dropdown renders as a floating panel on the page background. Qualified past
   `.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options`, which is how Quill sets the
   open panel's border - a plain .ql-picker-options rule loses to it and the panel keeps a pale
   edge only once opened, which is the only state anyone ever sees it in. */
.rich-text-editor .ql-toolbar.ql-snow .ql-picker-options,
.rich-text-editor .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
    background: var(--vb-surface-raised);
    border-color: var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
}
.rich-text-editor .ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label { border-color: var(--vb-border-strong); }

/* The link tooltip. Quill draws it white-on-#444 with a #ccc border, so adding a link - the whole
   reason the terms box is rich text - popped a light panel over the dark form. */
.rich-text-editor .ql-snow .ql-tooltip {
    background: var(--vb-surface-raised);
    border-color: var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
    color: var(--vb-text);
}
.rich-text-editor .ql-snow .ql-tooltip input[type=text] {
    background: var(--vb-bg-soft);
    border-color: var(--vb-border-strong);
    border-radius: 6px;
    color: var(--vb-text);
}
.rich-text-editor .ql-snow .ql-tooltip a { color: var(--vb-primary-bright); }
.rich-text-editor .ql-snow .ql-tooltip a.ql-action::after { border-right-color: var(--vb-border-strong); }
.rich-text-editor .ql-snow .ql-picker-item:hover { color: var(--vb-text); }

/* The link tooltip is a Quill-owned popup, otherwise white-on-white here. */
.rich-text-editor .ql-snow .ql-tooltip {
    background: var(--vb-surface-raised);
    border-color: var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    box-shadow: var(--vb-shadow);
    color: var(--vb-text);
}
.rich-text-editor .ql-snow .ql-tooltip input[type="text"] {
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
}
.rich-text-editor .ql-snow .ql-tooltip a.ql-action,
.rich-text-editor .ql-snow .ql-tooltip a.ql-remove { color: var(--vb-primary); }

/* Source view: a plain textarea holding the raw HTML, for pasting markup or
   fixing something the toolbar cannot express. Same box as the rich view so
   toggling does not make the field jump. */
.rich-text-editor textarea.rich-text-source {
    width: 100%;
    min-height: 12rem;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.55rem 0.8rem;
    font-family: var(--vb-mono);
    font-size: 0.88rem;
    line-height: 1.5;
    resize: vertical;
}

.rich-text-modes {
    display: flex;
    justify-content: flex-end;
    gap: 0.4rem;
    margin-bottom: 0.35rem;
}

/* Country + flag (M5D). The flag is decoration beside the name, never the only thing carrying
   the meaning - it is alt="" and aria-hidden, so a screen reader gets the country ONCE, from the
   text. A flag alone would be unreadable to anyone who cannot see it and ambiguous to plenty who
   can (several look near-identical at 20px). */
.country {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
}
.country-flag {
    /* Fixed box so a column of countries lines up regardless of each flag's own proportions,
       and a 1px edge because several flags are mostly white and would otherwise dissolve into
       the card behind them. */
    width: 20px;
    height: 15px;
    flex: 0 0 auto;
    object-fit: cover;
    border-radius: 2px;
    border: 1px solid var(--vb-border);
}

/* A pilot's name beside their airline identifier (Components/Shared/PilotLink.razor).

   The GAP is the whole point of the rule. Three M10 rosters rendered the name and the number as
   adjacent elements and Razor emitted no whitespace between them, so they read as one word
   ("Myles KnightM3C012"). A literal space in the markup is invisible to review and gets tidied
   away; a gap here cannot be. Baseline alignment so the smaller monospaced code sits on the
   name's line rather than floating, and wrapping allowed so a narrow table cell drops the code
   underneath instead of widening the column. */
.pilot-link {
    display: inline-flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.5rem;
}

/* M11.4b: the searchable dropdown (js/combobox.js), for lists that grow - uploaded images
   first, and anything else that ends up long.

   ⚠️ ALMOST NOTHING IS NEEDED HERE, and that is the point. The script builds AIRPORTFIELD'S OWN
   markup (.suggest-wrap, .suggest-popout, .airport-result-list, .airport-selected), so it
   inherits that control's look rather than defining a second one - Myles's own instruction, and
   the M10.8 rule either way. What follows is only the two things a keyboard-driven list needs
   that the airport picker never did, because there every row carries its own button. */

/* Keyboard and pointer land on the SAME highlight: two different "you are here" colours in one
   list is how somebody arrowing down loses track of where they are. */
.suggest-popout .airport-result-list li:not(.is-disabled):not(.airport-no-results):hover,
.suggest-popout .airport-result-list li.is-active {
    /* ⚠️ THE BRAND TINT, not a darker step. The popout is already --vb-surface-raised, so the
       first cut went the other way to --vb-bg-soft - and a highlight DARKER than the panel it
       sits on reads as a hole punched in the row rather than a row picked out (Myles: "a weird
       dark background"). This is the tint a chosen .radio-option already uses, so it is the
       portal's own answer to "this row is the one", not a third colour. */
    background: color-mix(in srgb, var(--vb-primary) 9%, transparent);
    cursor: pointer;
}

.suggest-popout .airport-result-list li.is-selected strong { color: var(--vb-primary-hover); }

/* Visible and unpickable, matching what the native select does with a disabled option. */
.suggest-popout .airport-result-list li.is-disabled {
    color: var(--vb-text-muted);
    cursor: not-allowed;
}
.suggest-popout .airport-result-list li.is-disabled strong { font-weight: 400; }

/* --------------------------------------------------------------------------
   M11.5b: the File Library redesign (Myles's mockup review). The landing page is an asset
   manager - tabs, thumbnails, preview and upload modals - with the entry form moved to its own
   page. Everything below is the handful of shapes the existing components do not already have;
   the tabs are .filter-tabs, the modals are .modal/.modal-backdrop, the facts list is
   .settings-facts, and the file picker on the publish page is the combobox's own markup.
   -------------------------------------------------------------------------- */

/* A wider modal for the upload list and previews; 30rem fits a confirmation, not an image. */
.modal-wide { max-width: 42rem; }

/* .settings-facts carries margin: 0 because on a settings page its card supplies the spacing.
   In a modal it follows the dropzone directly, so it needs its own - measured at 0px against
   the box above it before this existed. */
.modal .settings-facts { margin: 1.1rem 0; }

/* The leading file cell: 40px thumbnail or type glyph, then the name. */
.file-cell { display: flex; align-items: center; gap: 0.8rem; min-width: 240px; }
.thumb {
    width: 40px;
    height: 40px;
    border-radius: var(--vb-radius-sm);
    border: 1px solid var(--vb-border-strong);
    object-fit: cover;
    flex-shrink: 0;
    cursor: pointer;
    background: var(--vb-bg-soft);
    display: block;
}
.thumb-icon {
    width: 40px;
    height: 40px;
    border-radius: var(--vb-radius-sm);
    border: 1px solid var(--vb-border-strong);
    background: var(--vb-bg-soft);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.thumb-icon .icon { width: 20px; height: 20px; color: var(--vb-text-muted); }
.file-name { font-weight: 550; cursor: pointer; overflow-wrap: anywhere; }
.file-name:hover { color: var(--vb-primary-bright); }

/* Right-aligned action cluster on a row. Wraps rather than truncating on narrow screens. */
.row-actions { display: flex; gap: 0.15rem; justify-content: flex-end; flex-wrap: wrap; }

/* 🚨 A <td> WITH display:flex STOPS BEING A TABLE CELL, and that is what broke the divider
   between rows: its border-bottom then draws only as wide as the buttons inside it, so the line
   under every staff row stopped short of the right-hand edge (Myles, on the staff table). It has
   been that way wherever .row-actions sits on a cell, which is most admin tables.

   Cells keep table-cell display and right-align INLINE instead. The buttons are inline-flex, so
   they still sit in a row and still wrap on a narrow screen - the gap becomes a margin, because
   `gap` belongs to a flex or grid container and this is neither any more. */
td.row-actions,
th.row-actions {
    display: table-cell;
    text-align: right;
}
td.row-actions > * + *,
th.row-actions > * + * { margin-left: 0.15rem; }

/* The type filter chips under the tab row. */

/* The upload drop zone. ⚠️ The REAL InputFile sits invisibly over the whole zone, so click-to-
   browse and native drag-drop both hit it - no JS beyond what the browser already does, and if
   anything fails the input is still a working file control underneath. */
.dropzone {
    position: relative;
    border: 2px dashed var(--vb-border-strong);
    border-radius: var(--vb-radius);
    padding: 2.2rem 1rem;
    text-align: center;
    color: var(--vb-text-muted);
    transition: border-color 0.15s ease, background 0.15s ease;
}
.dropzone:hover { border-color: var(--vb-primary); background: var(--vb-primary-soft); color: var(--vb-text); }
.dropzone strong { color: var(--vb-text); }
.dropzone input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}
.dropzone-types { font-size: 0.82rem; margin-top: 0.4rem; }
/* 🚨 THE FILE INPUT IS INVISIBLE, SO THE ZONE WEARS ITS FOCUS (review round 4, P2). The input
   covers the whole zone at opacity 0; tabbing reached a working control and drew nothing. The
   outline is the portal's own focus ring (a:focus-visible, button:focus-visible), on the zone
   the reader can see, and only for KEYBOARD focus - a mouse click is not asked to wear it. */
.dropzone:has(input[type="file"]:focus-visible) {
    outline: 2px solid var(--vb-primary);
    outline-offset: 2px;
    border-color: var(--vb-primary-bright);
}

/* Per-file rows inside the upload modal: name, size, and how that one upload went. */
.up-list { margin-top: 1rem; border-top: 1px solid var(--vb-border); }
.up-row {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.6rem 0.1rem;
    border-bottom: 1px solid var(--vb-border);
    font-size: 0.92rem;
}
.up-row:last-child { border-bottom: none; }
/* One upload's outcome, coloured by what happened - always at the row's far end. */
.up-state-ok { color: var(--vb-success); font-size: 0.85rem; margin-left: auto; white-space: nowrap; }
.up-state-busy { color: var(--vb-text-muted); font-size: 0.85rem; margin-left: auto; white-space: nowrap; }
.up-state-err { color: var(--vb-danger); font-size: 0.85rem; margin-left: auto; text-align: right; }
/* A quiet note beside an outcome - "this replaced a file with itself" is worth saying and is
   not worth shouting. It shrinks before the file name does on a narrow modal. */
.up-note { color: var(--vb-text-muted); font-size: 0.85rem; margin-left: auto; text-align: right; }
.up-note + .up-state-ok { margin-left: 0.6rem; }

/* M16.1: the airline's shared storage meter (Components/Shared/StorageMeter.razor), the one
   component the Library upload dialog and the Cabin announcements page both show (AdminList and
   AdminStates frames 7-8). Card, badge, alert and figure styles are the portal's own; these rules
   are only its layout and the bar. Status is always the badge's WORDS as well as the bar's colour. */
.storage-meter-title {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--vb-text-muted);
    margin: 0 0 0.6rem;
}
.storage-meter-figures { display: flex; align-items: baseline; gap: 0.5rem; }
.storage-meter-used { font-variant-numeric: tabular-nums; }
.storage-meter-badge { margin-left: auto; align-self: center; }
.storage-meter-bar {
    height: 8px;
    border-radius: 999px;
    background: var(--vb-border);
    overflow: hidden;
    margin: 0.65rem 0 0.5rem;
}
.storage-meter-fill { display: block; height: 100%; border-radius: 999px; background: var(--vb-primary); }
.storage-meter-bar-nearly .storage-meter-fill { background: var(--vb-warning); }
.storage-meter-bar-full .storage-meter-fill { background: var(--vb-danger); }
.storage-meter-line { font-size: 0.85rem; margin: 0; }
.storage-meter-alerts { margin-top: 0.75rem; display: grid; gap: 0.6rem; }
.storage-meter-alerts > .alert-error,
.storage-meter-alerts > .notice-warning { margin: 0; }
.modal .storage-meter { margin-bottom: 1.1rem; }

/* The preview modal's stage: the image or PDF, bounded so a tall file scrolls the VIEWER
   rather than the page. */
.preview-stage {
    background: var(--vb-bg);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    max-height: 62vh;
    overflow: hidden;
}
.preview-stage img { max-width: 100%; max-height: 60vh; display: block; }
.preview-pdf { width: 100%; height: 60vh; border: none; display: block; }

/* The strip under a preview: visibility badge, facts, and the actions at the far end. */
.modal-footer-facts {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    flex-wrap: wrap;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--vb-border);
    font-size: 0.88rem;
    color: var(--vb-text-muted);
}
.modal-footer-actions { margin-left: auto; display: flex; gap: 0.5rem; }

/* Radio pair for a two-way visibility choice (the upload modal and the publish page). */
.radio-row { display: flex; gap: 1.4rem; margin: 0.35rem 0 0.2rem; flex-wrap: wrap; }
.radio-row label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.95rem; font-weight: 400; color: var(--vb-text); white-space: nowrap; }
/* ⚠️ The reset .radio-option input already carries, for the same reason (Myles caught the
   symptom on the publish page): these sit inside a .field, and `.field input` styles EVERY
   input as a full-width text box - so an unreset radio renders as a stretched empty bar that
   squeezes its own label into a one-word column. Width back to a control, and the portal's
   checkbox sizing and accent so it matches every other tick-box here. */
.radio-row input[type="radio"] {
    width: 1.05rem;
    height: 1.05rem;
    background: none;
    border: 0;
    padding: 0;
    box-shadow: none;
    accent-color: var(--vb-primary);
    flex: 0 0 auto;
}

/* Small screens: the type and date columns go, the name keeps the row identifiable, and the
   actions wrap under it - the same stacking rule the template's library rows use. */
@media (max-width: 700px) {
    .col-optional { display: none; }
    .row-actions { justify-content: flex-start; }
}

/* A file the preview cannot render: the type glyph and the honest sentence, centred where the
   image would have been. */
.preview-none {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.6rem;
    padding: 2.6rem 1rem;
    color: var(--vb-text-muted);
}
.preview-none .icon { width: 34px; height: 34px; }

/* ---- rank insignia and award badges in the portal (M11.6) ----
   Myles: "ranks should also be seen on the dash... maybe show smaller versions on the roster.
   Its the whole point of the images to make the saas come alive."

   🚨 object-fit: CONTAIN, like the template's own rule. A badge is a mark on a transparent
   background; cropping one to fill a square cuts the wings off it. A poster covers, because its
   edges are scenery - which is why .promo-preview is a different rule rather than this one with
   a different size. */
.rank-insignia,
.award-emblem-sm {
    flex: none;
    object-fit: contain;
    background: none;
}
.rank-insignia { width: 24px; height: 24px; }
.award-emblem-sm { width: 22px; height: 22px; }

/* A table cell carrying an insignia beside its text: one row, image never stretched by the
   cell's height. */
/* product/54 decision 16: the roster's email with its confirmed mark, spaced by the gap (never a typed space). */
.email-cell {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-width: 0;
}
.email-confirmed {
    display: inline-flex;
    color: var(--vb-success);
}
.email-confirmed .icon {
    width: 0.95rem;
    height: 0.95rem;
}

.rank-cell,
.award-cell {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
}

/* ---- bulk selection on a table (M11.6) ----
   Myles: "we need bulk delete in the file library". The bar appears only with something
   selected; a permanently visible row of disabled buttons is a reminder of an action nobody is
   taking. */
.bulk-bar {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-bottom: 0.85rem;
    padding: 0.6rem 0.9rem;
    border-radius: var(--vb-radius-sm);
    border: 1px solid var(--vb-primary);
    background: color-mix(in srgb, var(--vb-primary) 9%, transparent);
}
/* Pushes the actions to the far end, leaving the count on the left. */
.bulk-bar .btn:first-of-type { margin-left: auto; }

/* The tick-box column: as narrow as its control, and the control keeps its native size rather
   than being stretched into a text box by the table's own input styling. */
.col-select { width: 2.4rem; }
.col-select input[type="checkbox"] {
    width: 1.05rem;
    height: 1.05rem;
    background: none;
    border: 0;
    padding: 0;
    box-shadow: none;
    accent-color: var(--vb-primary);
}
/* A selected row is tinted, so the selection survives scrolling past the bar. */
table.data tr.is-selected > td { background: color-mix(in srgb, var(--vb-primary) 7%, transparent); }

/* M8.8 route library suggestions: the airline's stored routes for the pair a schedule form has
   chosen, offered as pickable rows above the route box. Bounded and scrolling like
   .airport-result-list, because a busy pair can hold a dozen routes and a form must not grow to
   fit them. Each row is a BUTTON (picking copies the text), so the browser's button defaults are
   reset back to the surrounding type. */
.route-suggestions {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    max-height: 14rem;
    overflow-y: auto;
}
.route-suggestion {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    width: 100%;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
    padding: 0.55rem 0.7rem;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
}
.route-suggestion:hover { border-color: var(--vb-primary); }
.route-suggestion .mono { word-break: break-word; }

/* M8.8 route library. The LIST is the page: routes are grouped by airport pair, because
   "several routes for one pair" is the whole point of the library and a flat table leaves it
   for the reader to notice. Within a group the ROUTE STRING leads - it is the content - with
   the note under it rather than in a column that is empty for most routes. */

/* The route library is a table since M24.8; the route text still wraps rather than scrolling. */
.route-text { font-size: 0.98rem; line-height: 1.45; word-break: break-word; }
.route-footnote { margin-top: 1rem; }

/* The add/edit panel opens in place above the library. Brand-bordered so it reads as the one
   thing now asking for input, without becoming a second page. */
.route-form {
    border-color: var(--vb-primary);
    box-shadow: 0 8px 24px color-mix(in srgb, var(--vb-primary) 18%, transparent);
    margin-bottom: 1.25rem;
}
.route-form-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}
.route-form-head h2 { margin: 0; }
.route-form-head .muted { font-size: 0.84rem; }
.route-input { font-family: var(--vb-mono); line-height: 1.5; }
/* The note and the submit share the last row: the note is the only field left, so a full-width
   row above a lone button wastes a band of the panel. */
.route-form-actions {
    display: flex;
    align-items: flex-end;
    gap: 0.9rem;
    flex-wrap: wrap;
}
.route-form-actions .field { margin-bottom: 0; }
.route-note-field { flex-grow: 1; min-width: 16rem; }
.route-form-actions .checkbox-field { margin-bottom: 0.4rem; }

/* Per-staff permission grants (M3F.2). The editor opens in place inside the staff
   table, so it sits on the raised surface rather than introducing a second card
   style. */
.permission-editor {
    margin: 0.75rem 0;
}

/* The preset shortcuts. A wrapping row of small ghost buttons, matching .card-actions
   rather than inventing a second button row. */
.preset-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0.75rem 0 1.1rem;
}

/* Fourteen areas read as a wall in one column, so they flow into as many columns as
   fit - the .card-grid rule with a wider minimum, because each option carries a
   sentence of description. */
.permission-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 0.9rem;
}

/* One option. Deliberately NOT .checkbox-field, whose label centres a single line
   beside its box: this one has a heading and a sentence, so the box aligns to the
   TOP or it floats in the middle of the text. */
.permission-option {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    text-transform: none;
    letter-spacing: normal;
    font-size: 0.95rem;
    line-height: 1.35;
}

.permission-option input[type="checkbox"] {
    width: 1.05rem;
    height: 1.05rem;
    margin-top: 0.15rem;
    accent-color: var(--vb-primary);
    flex-shrink: 0;
}

/* An option nobody may grant still reads as an option - shown disabled rather than
   hidden, so somebody can see what they cannot give away. */
.permission-option input[type="checkbox"]:disabled {
    cursor: not-allowed;
}

.permission-option input[type="checkbox"]:disabled + span {
    opacity: 0.6;
}

.permission-description {
    display: block;
    font-size: 0.85rem;
    margin-top: 0.15rem;
}

/* --------------------------------------------------------------------------
   The staff page's add form (M3F.11)

   ⚠️ Written here rather than borrowed. These three came from the Account Portal's
   invitations page, which M3F.11 consolidated onto this portal - and the two portals
   have SEPARATE stylesheets, so a class defined in one is undefined in the other
   (the M7.7 rule, and what StylesheetClassTests catches). Same metrics as their
   Account Portal namesakes on purpose: it is the same control, moved.
   -------------------------------------------------------------------------- */

.preset-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 0.75rem;
    margin-bottom: 1.1rem;
}

.preset-grid .radio-option { height: 100%; }

/* The heading above one group of option tiles. Quiet on purpose: it orients, it is not a
   section of the page. */
.option-group-title {
    font-size: 0.82rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--vb-text-muted);
    margin: 1.3rem 0 0.6rem;
}

.option-group-title:first-of-type { margin-top: 0.4rem; }

/* 🚨 STICKY, so what is chosen and the button that acts on it are never separated by a
   scroll. With the full list open the card is taller than a screen, and a button at the
   bottom of it makes choosing an area and adding the person two different journeys.

   ⚠️ It sits INSIDE the card and sticks to the bottom of the viewport, so it stops being
   sticky once the card ends - a bar hovering over the page after its own form had
   scrolled away would be chrome belonging to nothing. */
.action-bar {
    /* ⚠️ NOT sticky here, unlike the Account Portal page this came from. There it was the
       last thing on a dedicated page; on the staff page the card has the invitations table
       below it, and a bar pinned to the viewport would hover over unrelated content. The
       preset tiles mean most people never open the long list, so the button is rarely far. */
    position: static;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 1.4rem -1.4rem -1.4rem;
    padding: 0.9rem 1.4rem;
    border-top: 1px solid var(--vb-border);
    /* Opaque, matching the card: it is the card's own footer rather than a floating bar. */
    background: var(--vb-surface);
    border-radius: 0 0 var(--vb-radius) var(--vb-radius);
}

/* A modal holding a CHOICE rather than a confirmation (M3F.11: the permission editor).
   .modal caps at 30rem, which is right for "are you sure" and far too narrow for fourteen
   option tiles - at that width the grid collapses to one column and runs off the screen, which
   is exactly what it did as an inline card. Tall content scrolls INSIDE the card so the actions
   stay reachable rather than sitting below the fold. */
.modal-wide {
    max-width: 56rem;
    max-height: calc(100vh - 3rem);
    overflow-y: auto;
}

/* M4D.4: the utilisation table's bar column. The bar is decoration over figures that are already
   in the row - it exists so the SHAPE of a year reads at a glance, not to carry a number. The
   track is always drawn, including for a month with no flying: an absent bar reads as missing
   data, while an empty one reads as a quiet month, which is the distinction the panel is for. */
.util-bar-col { width: 40%; min-width: 6rem; }
.util-bar {
    height: 6px;
    background: var(--vb-surface-raised);
    border-radius: 3px;
    overflow: hidden;
}
.util-bar-fill { height: 100%; background: #8b6be0; border-radius: 3px; }

/* M4D.5, generalised at M11G.8: the portal's one bar chart, drawn by BarChart for utilisation
   and for streak participation alike. The SVG is STRETCHED to the card width (preserveAspectRatio
   none), which is what lets the bars fill the box at any width - and is exactly why the tick
   labels below are ordinary HTML positioned at a percentage rather than SVG <text>, which that
   same transform would squash. The M4C hub trend learned this the hard way. */
.bar-chart-card { padding: 1.1rem 1.2rem 0.9rem; }
.bar-chart-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    margin-bottom: 0.6rem;
    font-size: 0.82rem;
}
.bar-chart { display: block; width: 100%; height: 140px; }
.bar-chart-bar { fill: #8b6be0; }
/* The tick rides ON its own bar, so a label can never drift from the column it names. */
.bar-chart-axis {
    position: relative;
    height: 1.1rem;
    margin-top: 0.3rem;
}
.bar-chart-tick {
    position: absolute;
    transform: translateX(-50%);
    color: var(--vb-text-muted);
    font-size: 0.72rem;
    white-space: nowrap;
}
.bar-chart-range {
    margin: 0.35rem 0 0;
    font-size: 0.75rem;
}

/* M6I.8: a score line's evidence drill-down - compact links into the analysis timeline
   rows (whole-URL fragment hrefs; the rows carry matching ids). */
/* The reviewer's score lines grouped (M6J.8p): a caption row per group carrying the group's
   total, the points coloured only by sign, and the zero-point / not-evaluated lines behind a
   disclosure. table.data th paints a header background at (0,1,2); the caption outranks it. */
table.data tr.score-group-row th {
  background: transparent;
  padding-top: 10px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--vb-text-muted);
}
table.data tr.score-group-row th .mono { margin-left: 8px; letter-spacing: 0; text-transform: none; font-size: 0.8rem; }
.score-pos { color: var(--vb-success); }
.score-neg { color: var(--vb-danger); }
.score-other { margin-top: 12px; font-size: 0.9rem; }
.score-other summary { cursor: pointer; color: var(--vb-text-muted); }
.score-other summary strong { color: var(--vb-text); font-weight: 600; }
.score-other .table-wrap { margin-top: 8px; }
/* The statement's own arithmetic beside the headline tiles (the pilot page's points-summary). */
.score-arithmetic { margin: 10px 0 0; font-size: 0.86rem; font-variant-numeric: tabular-nums; }
.score-evidence { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.score-evidence a { font-size: 12.5px; color: var(--vb-text-muted); text-decoration: none; }
.score-evidence a:hover { color: var(--vb-text); text-decoration: underline; }

/* ---- The Flight Replay (M6J.3) ------------------------------------------------------ */
/* Mounted by the shared flight-replay-stage.js (byte-identical with the template's copy); the
   same class names under this stylesheet's own tokens. */
.replay-stage { outline: none; }
.replay-stage:focus-visible { box-shadow: 0 0 0 2px var(--vb-primary-soft); border-radius: var(--vb-radius); }
.replay-map-wrap { position: relative; }
.route-map.replay-map { height: 480px; margin-bottom: 0; }
/* M6J.8i: the altitude legend over the map, one toolbar, the readout in two tiers. */
.replay-legend { position: absolute; top: 10px; left: 10px; z-index: 2; display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; max-width: calc(100% - 64px); padding: 5px 10px; border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 12px; background: rgba(12, 12, 12, 0.78); font-size: 0.72rem; color: var(--vb-text-muted); pointer-events: none; }
.replay-legend-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.replay-legend-swatch { display: inline-block; width: 12px; height: 6px; border-radius: 3px; }
.replay-controls { display: flex; align-items: center; gap: 10px; margin-top: 10px; padding: 7px 10px; min-height: 44px; border: 1px solid var(--vb-border); border-radius: var(--vb-radius); background: var(--vb-surface-raised); flex-wrap: wrap; }
.replay-btn {
    background: var(--vb-surface);
    border: 1px solid var(--vb-border);
    border-radius: 999px;
    color: var(--vb-text);
    font-family: inherit;
    font-size: 0.84rem;
    font-weight: 600;
    padding: 6px 13px;
    cursor: pointer;
    min-width: 44px;
}
.replay-btn:hover { border-color: var(--vb-primary); }
.replay-play { font-size: 0.8rem; }
.replay-scrub { position: relative; flex: 1 1 220px; min-width: 140px; padding-bottom: 7px; }
.replay-scrubber { display: block; width: 100%; margin: 0; accent-color: var(--vb-primary); }
.replay-phase-ticks { position: absolute; left: 0; right: 0; bottom: 0; height: 6px; pointer-events: none; }
.replay-phase-tick { position: absolute; bottom: 0; width: 1px; height: 6px; background: var(--vb-text-muted); opacity: 0.7; }
.replay-clock { font-variant-numeric: tabular-nums; color: var(--vb-text-muted); min-width: 7.5ch; }
.replay-panel { margin-top: 12px; }
.replay-readout { display: grid; gap: 8px 14px; margin: 0; }
.replay-readout-primary { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.replay-readout-more { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--vb-border); }
.replay-readout-cell { min-width: 0; }
.replay-readout-cell dt { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--vb-text-muted); margin: 0; }
.replay-readout-cell dd { margin: 2px 0 0; font-size: 1rem; font-variant-numeric: tabular-nums; }
.replay-readout-primary .replay-readout-cell dd { font-size: 1.25rem; font-weight: 600; color: var(--vb-text); }
.replay-panel-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.replay-chips { display: flex; flex-wrap: wrap; gap: 6px; flex: 1 1 auto; }
.replay-chip { border: 1px solid var(--vb-border); border-radius: 999px; padding: 2px 9px; font-size: 0.74rem; color: var(--vb-text-muted); }
.replay-chip-on { color: var(--vb-text); border-color: var(--vb-primary); background: var(--vb-primary-soft); }
.replay-chip-warn { color: var(--vb-danger); border-color: var(--vb-danger); background: transparent; }
.replay-more-toggle { margin-left: auto; background: none; border: 1px solid var(--vb-border); border-radius: 999px; color: var(--vb-text-muted); font-family: inherit; font-size: 0.76rem; font-weight: 600; padding: 3px 11px; cursor: pointer; }
.replay-more-toggle:hover { border-color: var(--vb-primary); color: var(--vb-text); }
.replay-profile { margin-top: 12px; cursor: crosshair; touch-action: none; }
.debrief-chart.replay-profile { height: 150px; }
@media (max-width: 700px) {
    .route-map.replay-map { height: 60vh; min-height: 320px; }
    .replay-scrub { flex-basis: 100%; order: 10; }
    .replay-readout-primary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .replay-readout-primary .replay-readout-cell dd { font-size: 1.05rem; white-space: nowrap; }
}
@media (max-width: 420px) { .replay-readout-primary { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (pointer: coarse) { .replay-scrubber { height: 32px; } }
.replay-cursor { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: #ffffff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6); pointer-events: none; }
.replay-caption { min-height: 1.2em; }
.replay-notes { margin: 8px 0 0; font-size: 0.8rem; }
.replay-skip { margin: 4px 0 0; font-size: 0.8rem; }
/* A reviewer's timeline row a click can seek the replay to (M6J.3). */
.timeline-row.replay-seekable { cursor: pointer; }
.timeline-row.replay-seekable:hover { background: var(--vb-primary-soft); }
/* The timeline's weight (M6J.8o, the pilot page's M6J.8m rule): a coloured dot and a bold
   label for what a reviewer scans for (integrity, the shaping phases), a quieter row for the
   routine configuration and light changes, plain for the rest - and a seek glyph on hover so
   a row says it can be clicked. The row classes come from PirepDetailsPage.TimelineRowClass
   (timeline-row-low/-normal/-high and one per category); only the combinations below need a
   rule, the rest are the plain row. */
.timeline-dot { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: var(--vb-border-strong); align-self: center; }
.timeline-row-high .timeline-event { font-weight: 600; color: var(--vb-text); }
.timeline-row-high.timeline-row-integrity .timeline-dot { background: var(--vb-danger); }
.timeline-row-high.timeline-row-phases .timeline-dot { background: var(--vb-primary-bright); }
.timeline-row-low { color: var(--vb-text-muted); font-size: 0.85rem; }
.timeline-row-low .timeline-event { color: var(--vb-text-muted); }
.timeline-seek { flex: 0 0 auto; font-size: 10px; color: var(--vb-primary-bright); opacity: 0; transition: opacity 120ms; }
.timeline-row.replay-seekable:hover .timeline-seek, .timeline-row.timeline-row-focus .timeline-seek { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .timeline-seek { transition: none; } }

/* The flight story (M6J.8): the analysis card's opening paragraph, one sentence per line,
   each a seek target while the replay is mounted - the timeline rows' rule, in prose. */
.debrief-story { display: flex; flex-direction: column; gap: 4px; margin-bottom: 1rem; }
.debrief-story-line {
  margin: 0;
  padding: 0.3rem 0.5rem;
  border-radius: 6px;
  font-size: 0.95rem;
  line-height: 1.5;
}
.debrief-story-line.replay-seekable { cursor: pointer; }
.debrief-story-line.replay-seekable:hover { background: var(--vb-primary-soft); }

/* ---- The Landing Runway Analysis card (M6J.5, redesigned at M6J.8h) ------------------- */
/* Drawn by the shared runway-card.js over runway-diagram.js and flight-instruments.js
   (byte-identical with the template's copies); the same class names under this
   stylesheet's own tokens. The card is a CONTAINER: below 560px its bands stack and the
   stat rail becomes a 2x2 grid. */
.runway-card { container-type: inline-size; margin-top: 1rem; }
.runway-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.runway-card-heading { display: flex; flex-direction: column; gap: 4px; }
.runway-card-title { margin: 0; }
.runway-card-ident { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.runway-card-code { font-family: var(--vb-mono); font-size: 1rem; font-weight: 600; letter-spacing: 0.02em; color: var(--vb-text); }
.runway-card-name { color: var(--vb-text-muted); font-size: 0.85rem; }
.runway-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--vb-border-strong);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--vb-text-muted);
  white-space: nowrap;
}
.runway-badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.runway-badge-high { color: var(--vb-success); border-color: rgba(62, 194, 125, 0.45); }
.runway-badge-inferred { color: var(--vb-warning); border-color: rgba(242, 181, 107, 0.45); }
.runway-badge-unknown { color: var(--vb-text-muted); }
.runway-card-main { display: grid; grid-template-columns: minmax(0, 1fr) 220px; gap: 20px; margin-top: 14px; }
.runway-card-hero { min-width: 0; }
.runway-sentences { margin-top: 10px; }
.runway-sentence-lead { font-size: 1.05rem; font-weight: 600; line-height: 1.4; color: var(--vb-text); text-wrap: balance; }
.runway-sentence-sub { margin-top: 4px; font-size: 0.9rem; color: var(--vb-text-muted); }
.runway-flag {
  margin-top: 8px;
  padding: 6px 10px;
  border-left: 3px solid var(--vb-warning);
  background: rgba(242, 181, 107, 0.12);
  font-size: 0.85rem;
  color: var(--vb-text);
}
.runway-tertiary { margin: 10px 0 0; font-size: 0.76rem; line-height: 1.5; color: var(--vb-text-muted); }
.runway-rail { display: grid; grid-template-columns: 1fr; gap: 10px; align-content: start; }
.runway-stat { padding: 10px 12px; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); background: var(--vb-surface-raised); }
.runway-stat-label { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); }
.runway-stat-value {
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 2px;
  font-family: var(--vb-mono);
  font-size: 1.35rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--vb-text);
}
.runway-stat-unit { font-family: var(--vb-font); font-size: 0.72rem; font-weight: 500; color: var(--vb-text-muted); }
.runway-stat-pair { font-family: var(--vb-font); font-size: 0.85rem; font-weight: 500; color: var(--vb-text-muted); }
.runway-stat-sub { display: flex; align-items: center; gap: 4px; margin-top: 2px; font-size: 0.76rem; color: var(--vb-text-muted); }
.runway-stat-ok { color: var(--vb-success); font-weight: 700; }
.runway-card-second { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 20px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--vb-border); }
.runway-tile { min-width: 0; }
.runway-tile-cap { margin-bottom: 6px; font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); }
.runway-tile-body { max-width: 150px; }
.runway-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; align-content: start; font-size: 0.88rem; }
.runway-fact-key { color: var(--vb-text-muted); }
.runway-fact-value { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; color: var(--vb-text); }
.runway-fact-note { font-size: 0.8rem; color: var(--vb-text-muted); }
.runway-fact-absent { font-style: italic; color: var(--vb-text-muted); }
.runway-aoa-gauge { width: 100%; max-width: 260px; }
.runway-depart {
  display: flex;
  align-items: center;
  gap: 8px 14px;
  flex-wrap: wrap;
  margin-top: 16px;
  padding: 10px 12px;
  border: 1px solid var(--vb-border);
  border-radius: var(--vb-radius-sm);
  background: var(--vb-surface-raised);
  font-size: 0.85rem;
  color: var(--vb-text-muted);
}
.runway-depart-tag { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); }
.runway-depart-code { font-family: var(--vb-mono); font-weight: 600; color: var(--vb-text); }
.runway-depart-figs { flex: 1 1 auto; min-width: 0; }
.runway-depart-fig { font-family: var(--vb-mono); font-weight: 600; color: var(--vb-text); }
/* The diagram: the runway to scale along its length, labels as HTML rows above and below. */
.runway-diagram { position: relative; margin-top: 6px; }
.runway-diagram-svg { display: block; width: 100%; height: auto; overflow: visible; }
.runway-diagram-labels { position: relative; min-height: 1.35em; font-size: 0.74rem; color: var(--vb-text-muted); }
.runway-diagram-label { position: absolute; transform: translateX(-50%); white-space: nowrap; max-width: 48%; overflow: hidden; text-overflow: ellipsis; }
.runway-diagram-label-start { transform: none; max-width: 70%; }
.runway-diagram-label-strong { color: var(--vb-text); font-weight: 600; }
.runway-diagram-label-zone { color: var(--vb-success); }
.runway-diagram-tick-end { color: var(--vb-text); }
.runway-pavement { fill: rgba(255, 255, 255, 0.09); stroke: var(--vb-border-strong); stroke-width: 1.5; }
.runway-pavement-assumed { stroke: var(--vb-warning); }
.runway-displaced { stroke: rgba(255, 255, 255, 0.28); stroke-width: 2; }
.runway-zone { fill: rgba(62, 194, 125, 0.16); }
.runway-zone-edge { stroke: rgba(62, 194, 125, 0.6); stroke-width: 1; stroke-dasharray: 3 3; }
.runway-threshold { fill: var(--vb-text); }
.runway-threshold-keys { fill: rgba(255, 255, 255, 0.55); }
.runway-centreline { stroke: rgba(255, 255, 255, 0.4); stroke-width: 1.5; stroke-dasharray: 14 10; }
.runway-ident { fill: rgba(255, 255, 255, 0.7); font-family: var(--vb-mono); font-size: 16px; font-weight: 700; letter-spacing: 0.04em; }
.runway-rollout { fill: rgba(140, 90, 255, 0.26); }
.runway-end-stopped { fill: var(--vb-text); }
.runway-end-vacated { fill: none; stroke: var(--vb-text); stroke-width: 1.5; }
.runway-end-observed { fill: var(--vb-text-muted); }
.runway-leader { stroke: rgba(255, 255, 255, 0.35); stroke-width: 1; stroke-dasharray: 2 3; }
.runway-tick { stroke: var(--vb-text-muted); stroke-width: 1; }
.runway-contact { stroke: rgba(12, 12, 12, 0.9); stroke-width: 1.5; }
.runway-contact-touchdown { fill: #f2b56b; }
.runway-contact-liftoff { fill: #7bd88f; }
.runway-contact-halo { fill: #f2b56b; opacity: 0.55; transform-box: fill-box; transform-origin: center; }
/* The compact AOA gauge in the facts list: a scale, never a band. */
.aoa-gauge-svg { display: block; width: 100%; height: auto; overflow: visible; }
.aoa-gauge-track { fill: rgba(255, 255, 255, 0.1); }
.aoa-gauge-fill { fill: rgba(140, 90, 255, 0.55); }
.aoa-gauge-mark { stroke: var(--vb-text); stroke-width: 2; }
.aoa-gauge-tick-label { fill: var(--vb-text-muted); font-size: 8px; }
.aoa-gauge-value { fill: var(--vb-text); font-size: 9px; font-weight: 600; }
@container (max-width: 560px) {
  .runway-card-main, .runway-card-second { grid-template-columns: 1fr; }
  .runway-rail { grid-template-columns: 1fr 1fr; }
  .runway-diagram-tick-minor { display: none; }
  .runway-tile-body { max-width: 120px; }
}
/* The attitude tile (M6J.5), drawn by flight-instruments.js inside the card's second band. */
.instrument-svg { display: block; width: 100%; height: auto; }
.instrument-face { fill: rgba(255, 255, 255, 0.04); stroke: var(--vb-border); stroke-width: 1.5; }
.instrument-face-ring { fill: none; stroke: var(--vb-border); stroke-width: 1.5; }
.instrument-tick { stroke: var(--vb-text-muted); stroke-width: 1.5; }
.instrument-tick-label { fill: var(--vb-text-muted); font-size: 8px; }
.instrument-needle { stroke: var(--vb-text); stroke-width: 2.5; stroke-linecap: round; }
.instrument-hub { fill: var(--vb-text); }
.instrument-value { fill: var(--vb-text); font-size: 13px; font-weight: 600; }
.instrument-caption { fill: var(--vb-text-muted); font-size: 8px; letter-spacing: 0.06em; text-transform: uppercase; }
.attitude-sky { fill: #2f6fb0; }
.attitude-ground { fill: #7a5a2f; }
.attitude-horizon { stroke: #ffffff; stroke-width: 1.5; }
.attitude-pitch-line { stroke: rgba(255, 255, 255, 0.7); stroke-width: 1; }
.attitude-aircraft { fill: none; stroke: #f2b56b; stroke-width: 2.5; stroke-linejoin: round; }

/* ---- The approach-gate ladder (M6J.8s) ------------------------------------------------ */
/* The pilot page's M6J.8j ladder under this stylesheet's tokens: the analyzer's four
   altitudes as columns, a configured gate a button that seeks the replay, an unconfigured
   altitude a dashed placeholder so the ladder keeps its shape. Colour lives only on the
   verdict mark - never a column background. The row classes are computed in the page
   (GateClass), so every combination is ruled here by hand. */
.gate-ladder {
    margin: 0.5rem 0 1rem;
    padding: 12px 14px 14px;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius);
    background: var(--vb-surface-raised);
}
.gate-ladder-cap { margin: 0 0 10px; font-size: 0.78rem; color: var(--vb-text-muted); }
.gate-ladder-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.gate {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    min-width: 0;
    padding: 10px 12px 12px;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    background: var(--vb-surface);
    font-family: inherit;
    font-size: inherit;
    text-align: left;
    color: var(--vb-text);
}
button.gate { cursor: pointer; }
button.gate:hover, button.gate:focus-visible { border-color: var(--vb-primary-bright); }
.gate-unset { border-style: dashed; background: transparent; color: var(--vb-text-muted); }
.gate-selected { outline: 2px solid var(--vb-primary-bright); outline-offset: 1px; }
.gate-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; }
.gate-alt { font-family: var(--vb-mono); font-size: 1.05rem; font-weight: 600; }
.gate-unset .gate-alt { font-weight: 500; color: var(--vb-text-muted); }
.gate-mark { font-size: 1rem; font-weight: 700; color: var(--vb-text-muted); }
.gate-pass .gate-mark { color: var(--vb-success); }
.gate-fail .gate-mark { color: var(--vb-danger); }
.gate-notes { display: flex; flex-direction: column; gap: 1px; font-family: var(--vb-mono); font-size: 0.76rem; color: var(--vb-text-muted); }
.gate-unset .gate-notes { font-family: inherit; }
.gate-note { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.gate-hint { position: absolute; right: 10px; bottom: 8px; font-size: 0.7rem; color: var(--vb-primary-bright); opacity: 0; transition: opacity 120ms; }
button.gate:hover .gate-hint, button.gate:focus-visible .gate-hint { opacity: 1; }
.gate-ladder-verdict { margin: 10px 0 0; font-size: 0.82rem; }
@media (max-width: 700px) {
    .gate-ladder-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .gate-hint { display: none; }
}
@media (prefers-reduced-motion: reduce) { .gate-hint { transition: none; } }
/* What the shared card leaves unsaid, one muted line under it (M6J.8s). */
.runway-provenance { margin: 10px 0 0; font-size: 0.82rem; }
.runway-provenance-contact { display: block; margin-top: 4px; }

/* The weather record (M6J.8t): each station's raw METAR behind a disclosure - the evidence
   under the wind figures the shared runway card draws.
   ⚠️ The BOTTOM margin is load-bearing rather than taste: h3 carries `margin: 0 0 0.4rem`
   platform-wide, so the Terrain heading that follows this disclosure brings no top margin of
   its own and the two sat flush (2026-09-07, Myles: "weather obs needs a bit of spacing"). The
   block that wants the gap is the one that owes it. */
.weather-metars { margin: 12px 0 22px; font-size: 0.9rem; }
.weather-metars summary { cursor: pointer; color: var(--vb-text-muted); }
.weather-metars summary strong { color: var(--vb-text); font-weight: 600; }
.telemetry-sources { margin: 12px 0 22px; font-size: 0.9rem; }
.telemetry-sources summary { cursor: pointer; color: var(--vb-text-muted); }
.telemetry-sources summary strong { color: var(--vb-text); font-weight: 600; }
.telemetry-sources .table-wrap { margin-top: 8px; }
.telemetry-profile-part { margin-left: 6px; }
.weather-metar-head { margin: 10px 0 2px; font-size: 0.85rem; color: var(--vb-text-muted); }
.weather-metar { margin: 0; font-size: 0.85rem; color: var(--vb-text); overflow-wrap: anywhere; }

/* ---- The Flight analysis band (M6J.8v) --------------------------------------------------
   The pilot page's M6J.8l band on the reviewer's page: captioned columns of dense
   label/value lists, no boxes - the grid aligns the captions and the lists hang from them,
   so a one-line column beside a ten-row column is simply short. Ported under the admin
   tokens; the template's theme.css carries the same rules under its own. */
.analysis-band { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 22px 40px; align-items: start; margin-top: 18px; }
.analysis-col { min-width: 0; }
.analysis-col h4 { margin: 0 0 8px; font-size: 0.76rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); }
.analysis-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; font-size: 0.88rem; }
.analysis-facts dt { color: var(--vb-text-muted); }
.analysis-facts dd { margin: 0; color: var(--vb-text); min-width: 0; overflow-wrap: anywhere; }
.analysis-empty { margin: 0; font-size: 0.88rem; font-style: italic; color: var(--vb-text-muted); }
.telemetry-findings { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; font-size: 0.88rem; }
.telemetry-findings li { display: grid; gap: 2px; min-width: 0; }
.telemetry-finding { color: var(--vb-text-muted); }
.telemetry-finding-readings { color: var(--vb-text); overflow-wrap: anywhere; }
@media (max-width: 700px) { .analysis-facts { grid-template-columns: 1fr; gap: 2px 0; } .analysis-facts dd { margin-bottom: 6px; } }

/* ---- The terrain profile (M6J.6) ------------------------------------------------------- */
/* Drawn by the shared terrain-profile.js (byte-identical with the template, whose
   stylesheet carries the same rules under its own tokens): altitude over the DEM-derived
   terrain per section, the ticks as HTML at percentages, the source note beneath. */
.terrain-profile { margin-top: 8px; }
.terrain-chart { position: relative; height: 150px; margin-top: 10px; }
.terrain-chart-svg { display: block; width: 100%; height: 100%; }
.terrain-fill { fill: rgba(122, 90, 47, 0.55); stroke: #b08a55; stroke-width: 1.5; }
.terrain-corridor-fill { fill: rgba(176, 138, 85, 0.22); stroke: #b08a55; stroke-width: 1; stroke-dasharray: 4 3; }
.terrain-altitude { stroke: var(--vb-primary); stroke-width: 2; stroke-linejoin: round; }
.terrain-chart-ceiling, .terrain-chart-floor { position: absolute; left: 6px; font-size: 0.7rem; color: var(--vb-text-muted); pointer-events: none; }
.terrain-chart-ceiling { top: 4px; }
.terrain-chart-floor { bottom: 4px; }
.terrain-chart-ticks { position: relative; height: 1.4em; font-size: 0.72rem; color: var(--vb-text-muted); }
.terrain-chart-tick { position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap; }
.terrain-chart-tick:first-child { transform: none; }
.terrain-chart-tick-end { transform: translateX(-100%); }
.terrain-chart-caption { margin: 2px 0 0; font-size: 0.78rem; color: var(--vb-text-muted); }
.terrain-note { margin: 12px 0 0; font-size: 0.78rem; color: var(--vb-text-muted); }
.terrain-attribution { margin: 4px 0 0; font-size: 0.72rem; color: var(--vb-text-muted); }

/* M6J.8g - the terrain card's data-sources disclosure (the template's twin). */
.terrain-sources { margin-top: 8px; font-size: 0.78rem; color: var(--vb-text-muted); }
.terrain-sources summary { cursor: pointer; }
.terrain-sources .terrain-note { margin-top: 6px; }
/* M6J.8k - the segmented terrain card the shared module can draw (the template's twin). */
.terrain-segments { display: inline-flex; gap: 2px; margin-bottom: 4px; padding: 3px; border: 1px solid var(--vb-border); border-radius: 999px; background: var(--vb-surface-raised); }
.terrain-seg { border: 0; border-radius: 999px; min-height: 32px; padding: 5px 14px; background: transparent; color: var(--vb-text-muted); font-family: inherit; font-size: 0.82rem; font-weight: 600; cursor: pointer; }
.terrain-seg:hover { color: var(--vb-text); }
.terrain-seg-on { background: var(--vb-surface); color: var(--vb-text); box-shadow: 0 0 0 1px var(--vb-border-strong); }
.terrain-card-chart .terrain-chart { height: 220px; }
.terrain-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px 24px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--vb-border); }
.terrain-metric { min-width: 0; }
.terrain-metric-label { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); }
.terrain-metric-value { margin-top: 2px; font-family: var(--vb-mono); font-size: 1.25rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--vb-text); }
.terrain-metric-sub { margin-top: 2px; font-size: 0.76rem; color: var(--vb-text-muted); }
.terrain-details summary { font-weight: 600; color: var(--vb-text-muted); }
.terrain-detail-rows { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 10px 0 0; font-size: 0.8rem; }
.terrain-detail-rows dt { color: var(--vb-text-muted); }
.terrain-detail-rows dd { margin: 0; color: var(--vb-text); }
@media (max-width: 700px) {
    .terrain-segments { display: flex; width: 100%; box-sizing: border-box; }
    .terrain-seg { flex: 1 1 0; min-width: 0; min-height: 44px; padding: 5px 6px; font-size: 0.78rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .terrain-card-chart .terrain-chart { height: 200px; }
    .terrain-detail-rows { grid-template-columns: 1fr; }
    .terrain-chart-tick:nth-child(2), .terrain-chart-tick:nth-child(4) { display: none; }
}

/* The Fuel & weight card, no-plan shape (2026-09-07, Myles on the rendered card: the
   slash-separated values "makes the card harder to scan than it needs to be"). Three fuel
   stages and two weights are METRICS, so they take the treatment this page ALREADY uses on
   the terrain card a section above - uppercase muted label, strong tabular figure, auto-fit
   grid, hairline between groups - rather than a second metric language invented for one card.
   ⚠️ Deliberately NOT mono, unlike `.terrain-metric-value`: these sit among prose rows in a
   text card, and this is `.replay-readout-primary dd`'s styling, which is the closer sibling.
   The comparison shape keeps its table - three numbers per row against a header IS a table. */
.fuel-panel { margin: 14px 0 0; }
.fuel-panel > dl + dl { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--vb-border); }
.fuel-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 12px 24px; margin: 0; }
/* The weights carry longer labels, so they wrap at their own width instead of at a compromise
   that is too tight for one row and too loose for the other. */
.fuel-metrics-wide { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.fuel-metrics > div { min-width: 0; }
.fuel-metrics dt { font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); }
.fuel-metrics dd { margin: 2px 0 0; font-size: 1.25rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--vb-text); }
/* The quiet rows stay at the page's ordinary fact size: the burn is derived from the stages
   above it and the cargo is a typed figure, so neither should out-shout a measured stage. */
.fuel-row { margin: 0; font-size: 0.88rem; }
.fuel-row > div { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.fuel-row dt { color: var(--vb-text-muted); }
.fuel-row dd { margin: 0; font-variant-numeric: tabular-nums; color: var(--vb-text); }

/* --------------------------------------------------------------------------
   The PIREP section navigator (2026-09-07)
   A review table of contents for the reviewer's PIREP page, which now runs to a
   dozen sections. Admin only - the pilot's own report and the public flight page
   have nothing like it and are not getting one.
   -------------------------------------------------------------------------- */

/* THE OFFSET LIVES HERE, not in JavaScript. Every anchor target carries it, so a heading
   scrolled to by the rail, by a fragment on load, or by the browser's own find-in-page all
   clear the sticky topbar without a single subtracted pixel anywhere in the page's script. */
.pirep-section-anchor {
    /* M24.3: measured from the shell's top bar, so the two cannot disagree (88px, as before). */
    scroll-margin-top: calc(var(--vb-topbar-height) + 2rem);
}

/* The rail lives in the GUTTER beside the centred reading column, never inside it: the brief's
   one hard constraint is that it must not narrow the PIREP. .page is 1080px centred, so the
   gutter is only wide enough past ~1440px - below that the sticky Jump-to control takes over,
   which is why this whole block sits inside a media query and the default is "no rail". */
.pirep-sections {
    display: none;
}

.pirep-sections-jump {
    position: sticky;
    top: calc(var(--vb-topbar-height) + 0.5rem);
    z-index: 40;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0 0 1.25rem;
    padding: 0.5rem 0.7rem;
    background: color-mix(in srgb, var(--vb-bg) 82%, transparent);
    backdrop-filter: blur(10px);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm, 8px);
}

.pirep-sections-jump label {
    margin: 0;
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--vb-text-muted);
    white-space: nowrap;
}

/* 44px of touch target, and a native select so the keyboard, the screen reader and the
   platform's own picker all work without anything of ours in the way. */
.pirep-sections-jump select {
    flex: 1;
    min-height: 44px;
}

/* M24.3: THE RAIL IS A GUTTER OF THE CONTENT COLUMN, no longer fixed to the viewport. Its old
   position (half the viewport plus half a 1080px column) was wrong the moment a sidebar existed.
   The page keeps a 13rem gutter on its right and the rail floats into it, sticky under the top bar.
   A float rather than a grid on purpose: the docked decision bar (.review-bar) is a direct child of
   the page and docks with position: sticky, and a sticky grid item only sticks within its own row -
   so a grid would have undocked it. The rail takes no height of its own (its list overflows the
   zero-height float), so nothing in the report moves.
   Shown only while the report keeps at least the old 1080px beside it: from 1440px wide on the
   rail, 1600px with the sidebar expanded. Below that the sticky Jump-to control stands in. */
@media (min-width: 1440px) {
    .shell[data-sidebar="rail"] .shell-content .page:has(> .pirep-sections) { padding-right: calc(2rem + 13rem); }
    .shell[data-sidebar="rail"] .shell-content .page > .pirep-sections {
        display: block;
        float: right;
        clear: right;
        position: sticky;
        top: calc(var(--vb-topbar-height) + 1.5rem);
        width: 11rem;
        height: 0;
        margin-right: -13rem;
        overflow: visible;
        /* Above the page, below the docked decision bar (30) and the top bar (50). */
        z-index: 20;
    }
    .shell[data-sidebar="rail"] .shell-content .page > .pirep-sections-jump { display: none; }
}
@media (min-width: 1600px) {
    .shell:not([data-sidebar="rail"]) .shell-content .page:has(> .pirep-sections) { padding-right: calc(2rem + 13rem); }
    .shell:not([data-sidebar="rail"]) .shell-content .page > .pirep-sections {
        display: block;
        float: right;
        clear: right;
        position: sticky;
        top: calc(var(--vb-topbar-height) + 1.5rem);
        width: 11rem;
        height: 0;
        margin-right: -13rem;
        overflow: visible;
        z-index: 20;
    }
    .shell:not([data-sidebar="rail"]) .shell-content .page > .pirep-sections-jump { display: none; }
}

.pirep-sections-caption {
    margin: 0 0 0.6rem;
    /* In line with the labels, not the pills' edges. */
    padding-left: 0.7rem;
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--vb-text-muted);
}

/* The rail's rows are the Developer page's (Myles, 2026-09-28: two styles of the same "jump to a
   section" rail; he preferred the pill one) - one set of rules below, shared. */
.pirep-sections ol {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.pirep-sections li {
    margin: 0;
}

/* Reduced motion: the rail's own colour transitions are the only motion it owns, and the
   scroll behaviour is chosen in script from the same preference. */
@media (prefers-reduced-motion: reduce) {
    .pirep-sections a {
        transition: none;
    }
}

/* ---- M11H.1: pilot avatars and identities (product/25 sections 2.2 and 10) ----

   ⚠️ THE SAME VOCABULARY AS THE CUSTOMER TEMPLATE'S theme.css, WRITTEN AGAIN. The two are
   separate stylesheets by rule (M7.7) and neither may name a class the other lacks; what is
   shared is the JavaScript rule, not the CSS. The class NAMES match on purpose so a reader
   moving between the two surfaces meets one idea rather than two.

   ⚠️ THE BOX IS FIXED FROM THE FIRST PAINT. Razor renders the initials inside an empty circle
   and pilot-avatar.js draws the face on top after load - a circle that sized itself to its
   contents would move every row of a roster when the pictures landed. */
.pilot-avatar {
    position: relative;
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: 50%;
    background: var(--vb-primary-soft);
    color: var(--vb-primary-bright);
    font-weight: 600;
    line-height: 1;
    width: 32px;
    height: 32px;
    font-size: 0.72rem;
}

.pilot-avatar-xs { width: 24px; height: 24px; font-size: 0.6rem; }
.pilot-avatar-sm { width: 32px; height: 32px; font-size: 0.72rem; }
.pilot-avatar-md { width: 40px; height: 40px; font-size: 0.82rem; }
.pilot-avatar-lg { width: 96px; height: 96px; font-size: 1.7rem; }

/* The image sits OVER the initials rather than instead of them: that is the no-layout-shift
   half and the error fallback in one - removing a failed <img> reveals what was already
   underneath instead of leaving an empty hole. */
.pilot-avatar-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.pilot-identity {
    display: inline-flex;
    align-items: center;
    gap: 0.65rem;
    min-width: 0;
}

/* The pilot page's Profile card (M11H.1). */
.profile-moderation { display: flex; flex-wrap: wrap; gap: 1.25rem; align-items: flex-start; }
.profile-moderation-figure { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
.profile-moderation-detail { flex: 1; min-width: 220px; }
.profile-moderation-caption { font-size: 0.78rem; color: var(--vb-text-muted); }
/* M11H.2. A bio may be a short paragraph or two, so the line breaks the pilot typed survive
   here - white-space: pre-line renders them without letting a long unbroken run push the card
   sideways (overflow-wrap does that half). It is TEXT: Razor encodes it, and the entity
   normalised it on the way in. */
.profile-moderation-bio { display: block; white-space: pre-line; overflow-wrap: anywhere; }

/* --------------------------------------------------------------------------
   Chat moderation (M11I.6)
   -------------------------------------------------------------------------- */

/* The server's copy of what was said, as the moderator reads it. It WRAPS and it BREAKS: a
   reported message is whatever somebody typed, and one long unbroken run must not push the
   evidence column past the table (the M11I.4 conversation-list scar, where an inline span could
   not be clipped and every row pushed the column sideways). */
.chat-report-body {
  max-width: 28rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   Connection lost (Components/Shared/ReconnectModal.razor)
   --------------------------------------------------------------------------
   Blazor drives this dialog by putting ONE state class on #components-reconnect-modal; which
   wording shows is decided here and nowhere else, so the dialog is right with no script at all.

   ⚠️ THIS BLOCK IS BYTE-IDENTICAL IN BOTH PORTALS' app.css (PortalReconnectModalTests), with
   the component and js/reconnect-modal.js. Change all three pairs together.

   Hidden until Blazor says otherwise: with no state class the element is not on screen. Above
   every other layer (.modal-backdrop is 100) - a dialog open when the connection drops is as
   unusable as the page under it. */
.reconnect-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background: rgba(6, 7, 8, 0.62);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
}
.reconnect-backdrop.components-reconnect-show,
.reconnect-backdrop.components-reconnect-paused,
.reconnect-backdrop.components-reconnect-failed,
.reconnect-backdrop.components-reconnect-rejected,
.reconnect-backdrop.components-reconnect-resume-failed { display: flex; }

.reconnect-card {
    width: 100%;
    max-width: 25rem;
    padding: 1.9rem 1.75rem 1.6rem;
    text-align: center;
    background: var(--vb-surface);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
    outline: none;
}

/* One state's wording at a time. -retrying is ADDED to -show rather than replacing it, and a
   failed resume needs the same reload a rejected reconnect does. */
.reconnect-state { display: none; }
.components-reconnect-show .reconnect-state-retrying,
.components-reconnect-paused .reconnect-state-paused,
.components-reconnect-failed .reconnect-state-failed,
.components-reconnect-rejected .reconnect-state-rejected,
.components-reconnect-resume-failed .reconnect-state-rejected { display: block; }

.reconnect-title {
    margin: 0 0 0.5rem;
    font-size: 1.15rem;
    font-weight: 600;
    color: var(--vb-text);
}
.reconnect-text {
    margin: 0;
    font-size: 0.92rem;
    line-height: 1.55;
    color: var(--vb-text-muted);
}
/* The first attempts are back to back and need no counter; Blazor adds -retrying once it starts
   WAITING between them. The line keeps its space either way so the card does not jump. */
.reconnect-attempt {
    visibility: hidden;
    margin: 1rem 0 0;
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
    color: var(--vb-text-muted);
}
.components-reconnect-retrying .reconnect-attempt { visibility: visible; }

.reconnect-actions {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 1.4rem;
}
.reconnect-actions .btn:disabled { opacity: 0.55; cursor: default; }

/* The beacon: a dot calling out, two rings carrying the call. It pulses only while something
   is actually being tried; a settled state holds still and takes that state's colour. */
.reconnect-beacon {
    position: relative;
    width: 3.5rem;
    height: 3.5rem;
    margin: 0 auto 1.15rem;
    color: var(--vb-text-muted);
}
.reconnect-beacon i,
.reconnect-beacon span {
    position: absolute;
    inset: 0;
    margin: auto;
    border-radius: 50%;
}
.reconnect-beacon i {
    width: 0.8rem;
    height: 0.8rem;
    background: currentColor;
    box-shadow: 0 0 0 0.4rem color-mix(in srgb, currentColor 16%, transparent);
}
.reconnect-beacon span {
    border: 2px solid currentColor;
    opacity: 0;
}
/* At rest the first ring stays, drawn in: a lone dot reads as a stray pixel, not a signal. */
.reconnect-beacon span:first-child { opacity: 0.3; transform: scale(0.62); }
.components-reconnect-show .reconnect-beacon,
.reconnect-busy .reconnect-beacon { color: var(--vb-primary-bright); }
.components-reconnect-failed:not(.reconnect-busy) .reconnect-beacon { color: var(--vb-warning); }

@keyframes reconnect-ring {
    0% { transform: scale(0.3); opacity: 0.85; }
    100% { transform: scale(1); opacity: 0; }
}
@keyframes reconnect-arrive {
    from { opacity: 0; }
    to { opacity: 1; }
}
@media (prefers-reduced-motion: no-preference) {
    .components-reconnect-show .reconnect-beacon span,
    .reconnect-busy .reconnect-beacon span { animation: reconnect-ring 2s cubic-bezier(0.2, 0.6, 0.35, 1) infinite; }
    .components-reconnect-show .reconnect-beacon span + span,
    .reconnect-busy .reconnect-beacon span + span { animation-delay: 1s; }

    /* A connection that blinks for a moment is usually back before anyone could read this, so
       the FIRST state waits before fading in. The others are answers to a wait already had. */
    .reconnect-backdrop.components-reconnect-show { animation: reconnect-arrive 0.25s ease-out 0.4s both; }
}
/* end: connection lost */

/* --------------------------------------------------------------------------
   Cabin announcements (M16.5, product/31 section 11; the approved AdminList, AdminEditor and
   AdminStates boards). Layout and the few new controls only - cards, badges, buttons, fields,
   radio cards, alerts, modals and the storage meter are the portal's own.
   -------------------------------------------------------------------------- */

/* PageHeader's optional line above the title: the area ("ACARS"), or the way back. */
.page-header-eyebrow { font-size: 0.82rem; color: var(--vb-text-muted); margin-bottom: 0.15rem; }

/* The small uppercase heading the boards put over each block. */
.caps-title {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--vb-text-muted);
    margin: 0 0 0.6rem;
}

/* Page: the airline-wide switch beside the storage meter. */
.announce-top {
    display: grid;
    grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
    gap: 1.1rem;
    margin-bottom: 1.9rem;
    align-items: stretch;
}
.announce-top > .card { margin: 0; }
.announce-top .storage-meter { display: flex; flex-direction: column; }
.announce-switch-row { display: flex; align-items: center; gap: 0.9rem; }
.announce-switch-title { font-weight: 600; font-size: 1rem; }
.announce-switch-row .muted { font-size: 0.875rem; margin: 0; }
.announce-flight-types {
    border-top: 1px solid var(--vb-border);
    margin-top: 1rem;
    padding-top: 0.9rem;
}
.announce-subhead { font-weight: 600; font-size: 0.9rem; }
.announce-flight-types .muted { font-size: 0.85rem; margin: 0 0 0.6rem; }
.announce-usage { font-size: 0.85rem; margin-top: auto; padding-top: 0.75rem; }
.announce-usage strong { color: var(--vb-text); }
/* The Library's own line under the shared meter (M24.8), the announcements line's twin. */
.library-usage { font-size: 0.85rem; margin: 0.6rem 0 0; }
.library-usage strong { color: var(--vb-text); }

/* An on/off switch: a button with role="switch", its state in words beside it. */
.toggle-switch {
    width: 40px;
    height: 22px;
    flex: none;
    border-radius: 999px;
    border: none;
    padding: 0;
    position: relative;
    background: var(--vb-border-strong);
    cursor: pointer;
}
.toggle-switch::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--vb-text);
    transition: left 0.15s ease;
}
.toggle-switch-on { background: var(--vb-primary); }
.toggle-switch-on::after { left: 21px; }
.toggle-switch:focus-visible { outline: 2px solid var(--vb-primary-bright); outline-offset: 2px; }

/* Toggle chips: a set of choices, each on or off. */
.choice-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.choice-chip {
    border: 1px solid var(--vb-border-strong);
    background: transparent;
    color: var(--vb-text-muted);
    border-radius: 999px;
    padding: 0.3rem 0.75rem;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
}
.choice-chip:hover { color: var(--vb-text); }
.choice-chip-on {
    color: var(--vb-text);
    border-color: var(--vb-primary);
    background: color-mix(in srgb, var(--vb-primary) 20%, transparent);
}

/* Fleet type codes. */
.fleet-codes { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.6rem; }
.fleet-code {
    font-family: var(--vb-mono);
    font-weight: 700;
    font-size: 0.78rem;
    padding: 0.1rem 0.5rem;
    border-radius: 6px;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border);
    text-transform: uppercase;
}

/* The Airline Default card on the page. */
.announce-default {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 330px;
    gap: 1.75rem;
    align-items: center;
    margin-bottom: 1.9rem;
}
.announce-card-title { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.announce-card-title h2,
.announce-card-title h3 { margin: 0; }
.announce-default h3 { font-size: 1.2rem; }
.announce-default .muted { margin: 0.25rem 0 0; font-size: 0.9rem; }
.announce-count-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.announce-count { font-size: 1.1rem; }
.announce-dots { display: flex; gap: 3px; margin-top: 0.75rem; }
.announce-dot { flex: 1; height: 5px; border-radius: 3px; background: var(--vb-border); }
.announce-dot-on { background: var(--vb-primary-bright); }
.announce-default .announce-not-set-up { font-size: 0.78rem; margin-top: 0.4rem; }

/* Aircraft profile cards. */
.announce-profiles-head { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.announce-profiles-head .caps-title { margin: 0; }
.announce-profiles-head .muted { font-size: 0.85rem; margin: 0; }
.announce-profile-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
    margin-top: 0.75rem;
}
.announce-profile-card { display: flex; flex-direction: column; gap: 0.25rem; min-height: 168px; margin: 0; }
.announce-profile-card h3 { margin: 0; font-size: 1.05rem; }
.announce-profile-card .badge { margin-left: auto; }
.announce-profile-foot {
    margin-top: auto;
    padding-top: 0.9rem;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.9rem;
    font-size: 0.85rem;
}
.announce-profile-foot a { flex: none; font-weight: 600; }
.announce-profile-card-dashed {
    border-style: dashed;
    background: transparent;
    box-shadow: none;
    align-items: center;
    justify-content: center;
    text-align: center;
}
.announce-profile-card-dashed .fleet-codes { justify-content: center; }
.announce-profile-card-dashed a { font-weight: 600; font-size: 0.875rem; margin-top: 0.75rem; }

/* First visit (AdminStates frame 1). */
.announce-first-run { text-align: center; padding: 2.4rem 2.5rem; }
.announce-first-run .icon { width: 42px; height: 42px; color: var(--vb-primary-bright); }
.announce-first-run h2 { margin: 0.75rem 0 0.4rem; font-size: 1.3rem; }
.announce-first-run .muted { max-width: 46ch; margin: 0 auto 1.1rem; }

/* Editor: the profile card beside its aircraft types. */
.announce-editor-top {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 1.1rem;
    margin-bottom: 1.75rem;
    align-items: start;
}
.announce-editor-top > .card { margin: 0; }
.announce-status-options { display: grid; gap: 0.4rem; }
.announce-card-actions { display: flex; justify-content: flex-end; margin-top: 1rem; }
.fleet-row {
    display: grid;
    grid-template-columns: 70px minmax(0, 1fr) auto;
    gap: 0.75rem;
    align-items: center;
    padding: 0.5rem 0;
    border-top: 1px solid var(--vb-border);
    font-size: 0.9rem;
}
.fleet-row:first-of-type { border-top: none; }
.fleet-add { border-top: 1px solid var(--vb-border); padding-top: 0.75rem; margin-top: 0.15rem; }
.fleet-options { list-style: none; margin: 0; padding: 0; }
.fleet-option {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    width: 100%;
    padding: 0.45rem 0.75rem;
    border: none;
    background: transparent;
    color: var(--vb-text);
    font: inherit;
    font-size: 0.875rem;
    text-align: left;
    cursor: pointer;
}
.fleet-option:hover,
.fleet-option:focus-visible { background: color-mix(in srgb, var(--vb-primary) 20%, transparent); outline: none; }
.fleet-option:disabled { color: var(--vb-text-muted); cursor: not-allowed; background: transparent; opacity: 0.7; }
.fleet-option-where { margin-left: auto; font-size: 0.78rem; }

/* The moments: one card per group, one row per moment. */
.moment-list { padding: 0.4rem 1.4rem; margin-bottom: 1.5rem; }
.moment-list.is-dimmed { opacity: 0.45; }
.moment {
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr) auto;
    gap: 0.25rem 0.75rem;
    align-items: start;
    padding: 0.9rem 0;
    border-top: 1px solid var(--vb-border);
}
.moment:first-child { border-top: none; }
.moment-num {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--vb-text-muted);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 1px;
}
.moment-name { font-weight: 600; font-size: 0.97rem; display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; }
.moment-actions { display: flex; align-items: center; gap: 0.25rem; grid-row: 1 / span 3; grid-column: 3; }
.moment-when { font-size: 0.85rem; color: var(--vb-text-muted); grid-column: 2; margin: 0; }
.moment-facts { font-size: 0.85rem; grid-column: 2; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.25rem; }
.moment-fact {
    font-size: 0.78rem;
    padding: 0.05rem 0.5rem;
    border-radius: 6px;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border);
    color: var(--vb-text-muted);
    white-space: nowrap;
}
.moment > .notice-warning { grid-column: 2 / span 2; margin: 0.5rem 0 0; font-size: 0.85rem; }
.moment-edit {
    grid-column: 2 / span 2;
    margin-top: 0.75rem;
    padding: 1rem;
    border-radius: 10px;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border);
}
.moment-edit-head { display: flex; align-items: baseline; gap: 0.6rem; margin-bottom: 0.25rem; }
.moment-edit-head strong { font-size: 0.875rem; }
.moment-edit-head .muted { font-size: 0.8rem; }
.moment-edit-actions { display: flex; gap: 0.5rem; margin-top: 0.6rem; }
.moment-edit-choices {
    display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    gap: 1.4rem;
    margin-top: 1.1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--vb-border);
}
.moment-edit-choices .field { margin-bottom: 0; }
.moment-number { display: flex; align-items: center; gap: 0.5rem; }
.field .moment-number input { width: 84px; }

/* Round play buttons. */
.play-btn {
    width: 30px;
    height: 30px;
    flex: none;
    border-radius: 50%;
    border: 1px solid var(--vb-border-strong);
    background: var(--vb-surface-raised);
    color: var(--vb-text);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    cursor: pointer;
}
.play-btn:hover { border-color: var(--vb-primary); }
.play-btn .icon { width: 12px; height: 12px; }
.play-btn-sm { width: 26px; height: 26px; }
.play-btn-sm .icon { width: 10px; height: 10px; }

/* A recording in a moment's editor. */
.recording-row {
    display: grid;
    grid-template-columns: 18px 26px minmax(0, 1fr) 70px 70px auto;
    gap: 0.6rem;
    align-items: center;
    padding: 0.5rem 0;
    border-top: 1px solid var(--vb-border);
    font-size: 0.875rem;
}
.recording-row:first-of-type { border-top: none; }
.recording-row.is-dragging { opacity: 0.5; }
.recording-grip { color: var(--vb-border-strong); cursor: grab; letter-spacing: -2px; user-select: none; }
.recording-label { font-weight: 600; }
.recording-meta { color: var(--vb-text-muted); font-variant-numeric: tabular-nums; }
.recording-wave { height: 18px; display: flex; align-items: center; gap: 2px; }
.recording-wave i { display: block; width: 2px; border-radius: 1px; background: color-mix(in srgb, var(--vb-primary-bright) 45%, transparent); }

/* A segmented choice: one of a few, side by side. */
.segmented { display: inline-flex; border: 1px solid var(--vb-border-strong); border-radius: var(--vb-radius-sm); overflow: hidden; }
.segmented button {
    border: none;
    background: transparent;
    color: var(--vb-text-muted);
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    padding: 0.45rem 0.8rem;
    cursor: pointer;
}
.segmented button:hover { color: var(--vb-text); }
.segmented .segmented-on {
    background: color-mix(in srgb, var(--vb-primary) 22%, var(--vb-surface));
    color: var(--vb-text);
    box-shadow: inset 0 0 0 1px var(--vb-primary);
}

/* The upload dialog, and its per-file outcomes (AdminStates frame 3). */
.upload-dialog-head { display: flex; align-items: baseline; gap: 0.6rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.upload-dialog-head h2 { margin: 0; }
.upload-dialog-head .muted { font-size: 0.875rem; }
.upload-outcomes { margin-top: 0.5rem; }
.upload-outcome {
    display: grid;
    grid-template-columns: 20px minmax(0, 1fr) auto;
    gap: 0.15rem 0.6rem;
    padding: 0.6rem 0;
    border-top: 1px solid var(--vb-border);
    font-size: 0.875rem;
    align-items: start;
}
.upload-outcome:first-child { border-top: none; }
.upload-outcome-message { grid-column: 2 / span 2; font-size: 0.85rem; }
.upload-ok { color: var(--vb-success); }
.upload-bad { color: var(--vb-danger); }

/* Information counterpart of .alert-error / .alert-success (M16.5, AdminStates frame 2). */
.alert-info {
    background: color-mix(in srgb, var(--vb-primary) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--vb-primary) 45%, transparent);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.65rem 0.9rem;
    margin: 0.75rem 0 1rem;
    font-size: 0.9rem;
}

/* The dropzone's "choose files" reads as a link (AdminStates frame 3); the whole zone is the control. */
.dropzone-link { color: var(--vb-primary-bright); }

/* Cabin announcements at narrower widths: every grid stacks, a recording row folds its figures
   under its name, and the actions wrap under the moment instead of squeezing its sentence. */
@media (max-width: 960px) {
    .announce-top, .announce-editor-top, .announce-default, .moment-edit-choices { grid-template-columns: minmax(0, 1fr); }
    .announce-profile-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .announce-profile-grid { grid-template-columns: minmax(0, 1fr); }
    .moment { grid-template-columns: 26px minmax(0, 1fr); }
    .moment-actions { grid-row: auto; grid-column: 2; flex-wrap: wrap; }
    .moment-edit, .moment > .notice-warning { grid-column: 1 / span 2; }
    .recording-row { grid-template-columns: 18px 26px minmax(0, 1fr) auto; }
    .recording-row .recording-meta { display: none; }
}

/* A field whose value was refused, and the refusal under it (M16.10, AdminStates frame 4: the import dialog). */
.field input.input-invalid { border-color: var(--vb-danger); }
.field-error { margin: 0.4rem 0 0; font-size: 0.85rem; color: color-mix(in srgb, var(--vb-danger) 55%, #fff); }

/* A setting that only means anything while the setting ABOVE it is on (M18.1, AdminAircraftType
   part 2: the non-fleet minimum rank under "Allow non-fleet aircraft"). Indented and marked with
   the accent edge so the dependency is visible rather than implied by proximity.

   ⚠️ A SINGLE COLUMN, deliberately, although the board draws the field and its sentences side by
   side. The board draws Flight operations as full-width rows; the real page lays its switches out
   in `.option-grid`, THREE columns of about 320px, and a two-column box inside one of those
   overflows its neighbour and squeezes the sentences to one word a line. The design system's
   layout wins over the board's (product/33 section 0.2 item 4); the board's intent - the setting
   sits inside the toggle it depends on - is what is preserved. */
.nested-setting {
    margin: 0.75rem 0 0 0.75rem;
    padding: 0.85rem 1rem;
    border: 1px solid var(--vb-border);
    border-left: 3px solid var(--vb-primary-hover);
    border-radius: var(--vb-radius-sm);
    background: var(--vb-bg-soft);
}
.nested-setting > label {
    display: block;
    margin-bottom: 0.35rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--vb-text);
}
.nested-setting > select { width: 100%; }
.nested-setting > .hint { margin-bottom: 0; }
.nested-setting > .hint + .hint { margin-top: 0.5rem; }
/* ⚠️ `.option-grid .radio-option` is `height: 100%` so a row of switch cards lines up. A card
   that now has a nested setting BELOW it must take its own height instead: otherwise the label
   alone claims the whole grid row and the setting is laid out past the bottom of it, over
   whatever follows. Found by looking - the measurements said the box sat 240px below its own
   container - rather than by reading the markup. */
.option-grid .field:has(> .nested-setting) .radio-option { height: auto; }
@media (max-width: 640px) {
    /* The indent costs more than it explains once the card is the width of the screen. */
    .nested-setting { margin-left: 0; }
}

/* A quiet explanatory note beside a field - the neutral sibling of .alert-info, which is
   accent-tinted and reads as louder than a standing consequence deserves (M18.1,
   AdminAircraftType part 1: what a minimum rank on the first rung does to new pilots). */
.alert-note {
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text-muted);
    padding: 0.6rem 0.85rem;
    margin: 0.75rem 0 0;
    font-size: 0.9rem;
}

/* The column that EXPLAINS the field beside it, rather than holding a field of its own
   (M18.1, AdminAircraftType part 1). Its first line sits level with the control, not with the
   label above it, so the two columns read as one row. */
.field.field-aside { padding-top: 1.55rem; }
.field-aside > .hint { margin-top: 0; }
.field-aside > .hint + .hint { margin-top: 0.6rem; }
@media (max-width: 860px) {
    /* Stacked, the explanation follows its field and the offset is just a gap. */
    .field.field-aside { padding-top: 0; }
}

/* M18.5 (AdminRankDependency): one thing that requires a rank - what kind it is, what it is, and
   the way to it. A row rather than a table because the kinds differ (an aircraft type, a setting)
   and a table would invent columns neither of them has. */
.rank-ref {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: 0.75rem;
    align-items: baseline;
    padding: 0.7rem 0;
    border-top: 1px solid var(--vb-border);
}
.rank-ref:first-of-type { border-top: none; }
@media (max-width: 640px) {
    .rank-ref { grid-template-columns: auto minmax(0, 1fr); }
    .rank-ref > :last-child { grid-column: 2; justify-self: start; }
}

/* ---- M18.6: an attention-required state that is a WORD, not a colour (product/33 A5.1) ----
   The Admin Portal has no staff notification system, so a derived "Needs attention" surfaces
   through the mechanisms already here: a tile above the list, a filter option and a badge. */

/* A badge that leads with an icon. The icon is decoration - the word beside it carries the
   meaning, so a reader who cannot see colour or shape still gets the state. */
.badge-with-icon {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    margin-left: 0.4rem;
}
.badge-with-icon > svg { width: 0.8rem; height: 0.8rem; flex: none; }

/* The count tile as a BUTTON: it appears only while the count is above zero, and clicking it
   applies the filter it counts. Inherits .stat-tile and .card, and undoes what a button brings. */
.stat-tile-filter {
    border-color: rgba(227, 93, 106, 0.55);
    background: rgba(227, 93, 106, 0.07);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.stat-tile-filter:hover:not(:disabled) { border-color: rgba(227, 93, 106, 0.8); }
.stat-tile-filter:disabled { cursor: default; opacity: 0.7; }
.stat-tile-filter-head { display: flex; align-items: center; gap: 0.6rem; }
.stat-tile-filter .stat-tile-icon {
    width: 1.15rem;
    height: 1.15rem;
    flex: none;
    color: var(--vb-danger);
}
.stat-tile-filter .stat-label { color: var(--vb-danger); }
.stat-tile-action {
    margin-top: 0.15rem;
    color: var(--vb-primary-bright);
    font-size: 0.8rem;
    font-weight: 600;
}

/* The warning panel on an assignment that can no longer be started. It sits beside the actions
   that fix it, because a warning with nothing to do about it is just noise. */
.attention-panel {
    display: flex;
    gap: 0.85rem;
    align-items: flex-start;
    padding: 1rem 1.25rem;
    border: 1px solid rgba(227, 93, 106, 0.5);
    border-radius: var(--vb-radius);
    background: rgba(227, 93, 106, 0.08);
    margin-bottom: 1.25rem;
}
.attention-panel > svg {
    width: 1.25rem;
    height: 1.25rem;
    flex: none;
    margin-top: 0.15rem;
    color: var(--vb-danger);
}
.attention-panel-body { flex: 1; min-width: 0; }
.attention-panel-badges {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.35rem;
    flex-wrap: wrap;
}
.attention-panel-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.85rem;
    flex-wrap: wrap;
}

/* "Everything stopping this flight today" - every current blocker, categorised, so an admin can
   see that fixing the others alone would not make the flight startable. */
.blocker-list {
    margin: 0.85rem 0 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.blocker-list li {
    display: flex;
    gap: 0.6rem;
    align-items: flex-start;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--vb-border);
    border-radius: 0.5rem;
    background: var(--vb-bg-soft);
}
.blocker-list .badge { flex: none; }
.blocker-note { margin-top: 0.5rem; font-size: 0.85rem; }

/* M18.8 (product/33 A2): a warning that lives INSIDE a field's hint - the consequence of what
   is currently typed, said where it can still be changed, without the weight of an alert box. */
.hint-warning {
    display: block;
    margin-top: 0.3rem;
    color: var(--vb-warning);
    font-weight: 600;
}

/* ---- M18.11: the Aircraft images card (product/33 section 4.5; board AdminImages) ----
   A gallery of tiles, the Primary marked in WORDS as well as by its ring, and a picker that
   searches the File Library as you type. */

/* The images card is its own full-width section of the aircraft page (board AdminImages). */
/* The last section of the aircraft page, opening on the same 2.5rem as .section-title so it
   reads as a section rather than a card that drifted to the bottom. */
.aircraft-images-card { margin: 2.5rem 0 0; }
/* Operations sits in the page's "now" group with the summary and the location map: 1.5rem
   between each, rather than the shared row's 1rem, so the three read as one deliberate block. */
.aircraft-operations { margin-bottom: 1.5rem; }
.aircraft-images-head { display: flex; align-items: baseline; gap: 0.9rem; }
.aircraft-images-head h2 { flex: 1; margin-bottom: 0.25rem; }
.aircraft-images-head strong { font-variant-numeric: tabular-nums; }
/* THREE columns, as drawn, stepping down to two and then one as the page narrows. */
.image-tiles {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
    align-items: start;
    margin-top: 1rem;
}
.image-tile {
    border: 1px solid var(--vb-border);
    border-radius: 0.6rem;
    background: var(--vb-bg-soft);
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
/* The Primary is ringed AND says so - colour alone is never the answer. */
.image-tile-primary {
    border-color: var(--vb-primary-bright);
    box-shadow: 0 0 0 2px rgba(124, 77, 255, 0.35);
}
.image-tile-photo { position: relative; height: 11rem; background: var(--vb-bg); }
.image-tile-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.image-tile-mark { position: absolute; top: 0.6rem; left: 0.6rem; }
/* A SOLID mark, as the board draws it (Myles, 2026-09-21: "make the primary badge more visible").
   The translucent info badge it began as vanished over a light photograph; the accent fill and
   white text read on any picture. */
.image-tile-mark {
    background: var(--vb-primary);
    border-color: var(--vb-primary-bright);
    color: var(--vb-on-primary);
    font-weight: 700;
}
.image-tile-mark > svg { fill: currentColor; stroke: none; }
.image-tile-pos {
    position: absolute;
    top: 0.6rem;
    right: 0.6rem;
    padding: 0.05rem 0.5rem;
    border: 1px solid var(--vb-border-strong);
    border-radius: 0.4rem;
    background: rgba(12, 13, 15, 0.78);
    font-size: 0.72rem;
    font-weight: 600;
}
.image-tile-body {
    padding: 0.75rem 0.85rem 0.9rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}
.image-tile-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
/* The second row: the image's ROLE on the left, Detach on the right. */
.image-tile-actions-split { justify-content: space-between; }
.image-tile-detach { display: inline-flex; gap: 0.35rem; margin-left: auto; }
/* The star is the shape; the badge's own leading dot would be a second one. */
.image-tile-mark::before { content: none; }
.image-tile-mark.badge-with-icon { margin-left: 0; }
@media (max-width: 900px) { .image-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { .image-tiles { grid-template-columns: minmax(0, 1fr); } }

/* The empty slot: what is left, and what the Primary is for. */
.image-tile-add {
    border: 1px dashed var(--vb-border-strong);
    border-radius: 0.6rem;
    min-height: 11rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 0.4rem;
    padding: 1.1rem;
    color: var(--vb-text-muted);
    font-size: 0.85rem;
}

.image-panel {
    margin-top: 1.1rem;
    padding: 1rem;
    border: 1px solid var(--vb-border);
    border-radius: 0.6rem;
    background: var(--vb-bg-soft);
}
.image-panel-tabs {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin-bottom: 0.85rem;
    flex-wrap: wrap;
}
/* Close sits at the far end of the tabs row. */
.image-panel-close { margin-left: auto; }

/* The aircraft's image mode, under the card's intro. */
.aircraft-image-mode { margin-top: 0.75rem; }
.aircraft-image-confirm { margin-top: 1rem; }
.aircraft-image-confirm p { margin: 0 0 0.6rem; }
/* No image to show: the plane on the tile's own ground, as the public fleet map draws it. */
.image-tile-empty {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--vb-text-muted);
}
.image-tile-empty svg { width: 2.5rem; height: 2.5rem; opacity: 0.6; }
.image-tile-info p { margin: 0; }

/* A smaller button, for the dense controls inside an image tile. */
.btn-sm { padding: 0.3rem 0.6rem; font-size: 0.8rem; gap: 0.3rem; }

/* A pressed tab, so the open panel is obvious without a second heading. */
.btn-pressed { border-color: var(--vb-primary-bright); background: var(--vb-primary-soft); }

/* THE RESULTS ARE A DROPDOWN, directly under the search field (board AdminImages; Myles,
   2026-09-21: "too cramped"). The raised surface and shadow are .suggest-popout's, so it reads as
   the same kind of thing as every other predictive lookup - but it stays IN FLOW rather than
   overlaying, because in this panel the list IS the content and the hint belongs under it. */
.image-library {
    margin-top: 4px;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
    max-height: 26rem;
    overflow-y: auto;
}
.image-search-field { margin-bottom: 0; }
/* The Library image field (Components/Shared/LibraryImageField.razor; 2026-09-28, the approved
   PromoImagePicker mockup): one image for a tour, an event, a hub, an award or a rank, chosen with the
   aircraft card's panel above. The preview takes the shape the image is shown in - a 2:1 banner, or a
   square for a badge - so the reader sees the crop before saving. */
/* min-width: 0 so the chosen file's name can shorten with an ellipsis inside a grid column. */
.library-image-field { min-width: 0; }
.library-image-label { display: block; margin-bottom: 0.45rem; }
.library-image-pick { display: flex; gap: 1rem; align-items: center; padding: 0.8rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius); background: var(--vb-bg-soft); }
.library-image-thumb { flex: none; width: 16rem; aspect-ratio: 2 / 1; object-fit: cover; border-radius: var(--vb-radius-sm); background: var(--vb-bg); display: block; }
.library-image-empty { flex: none; width: 16rem; aspect-ratio: 2 / 1; border: 1px dashed var(--vb-border-strong); border-radius: var(--vb-radius-sm); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.35rem; color: var(--vb-text-muted); font-size: 0.85rem; text-align: center; }
.library-image-empty .icon { width: 22px; height: 22px; }
.library-image-thumb-square { width: 8rem; aspect-ratio: 1 / 1; }
.library-image-thumb-square.library-image-thumb { object-fit: contain; }
/* A logo made for one of the website's themes, previewed on that theme's header colours (the
   template's --surface-1 in each), whatever this portal's theme: the whole logo, never cropped. */
.library-image-thumb-on-dark,
.library-image-thumb-on-light { object-fit: contain; padding: 0.75rem 1rem; box-sizing: border-box; }
.library-image-thumb-on-dark { background: #141519; }
.library-image-thumb-on-light { background: #ffffff; box-shadow: inset 0 0 0 1px var(--vb-border); }
/* A thumbnail you can click for the File library's preview: looks like the image, zooms like a picture. */
.library-image-preview-button { flex: none; display: block; padding: 0; margin: 0; border: 0; background: none; line-height: 0; border-radius: var(--vb-radius-sm); cursor: zoom-in; }
.library-image-preview-button:hover img { filter: brightness(1.12); }
.library-image-preview-button:focus-visible { outline: 2px solid var(--vb-primary-bright); outline-offset: 2px; }
.library-image-body { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
.library-image-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.library-image-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.35rem; }
.library-image-close { margin-left: auto; }
.library-image-link-toggle { font-size: 0.85rem; margin-top: 0.5rem; }
.library-image-link { margin-top: 0.8rem; margin-bottom: 0; }
@media (max-width: 640px) {
    .library-image-pick { flex-direction: column; align-items: stretch; }
    .library-image-thumb, .library-image-empty, .library-image-pick > .library-image-preview-button { width: 100%; }
    .library-image-pick > .library-image-preview-button:has(.library-image-thumb-square) { width: auto; align-self: center; }
    .library-image-thumb-square { width: 8rem; align-self: center; }
}
.image-search { position: relative; }
.image-search-icon {
    position: absolute;
    left: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    width: 1rem;
    height: 1rem;
    color: var(--vb-text-muted);
    pointer-events: none;
}
.image-search input { padding-left: 2.3rem; }
.image-library-meta { font-size: 0.8rem; margin-top: 0.1rem; }
/* The shape is the check; the badge's own leading dot would be a second one. */
.image-attached { margin-left: 0; white-space: nowrap; }
.image-attached::before { content: none; }
/* The Library's drop zone, a little shorter: here it shares the panel with the storage meter. */
.image-dropzone { padding: 1.4rem 1rem; }
.image-library-row {
    display: grid;
    grid-template-columns: 4rem minmax(0, 1fr) auto;
    gap: 0.75rem;
    align-items: center;
    padding: 0.55rem 0.75rem;
    border-top: 1px solid var(--vb-border);
}
.image-library-row:first-child { border-top: none; }
.image-library-row img {
    width: 4rem;
    height: 2.6rem;
    object-fit: cover;
    border-radius: 0.3rem;
    background: var(--vb-bg);
}
/* An already-attached file stays VISIBLE and dimmed: hiding it would read as "you do not have
   that picture" rather than "you already used it". */
.image-library-row-off { opacity: 0.55; }
.image-library-row:not(.image-library-row-off):hover { background: var(--vb-primary-soft); }
/* On a phone the name needs the row: the badge or the Attach button drops under it rather than
   squeezing the name to nothing (a nowrap badge beside it left the name 8px wide). */
@media (max-width: 640px) {
    .image-library-row { grid-template-columns: 4rem minmax(0, 1fr); row-gap: 0.4rem; }
    .image-library-row > :last-child { grid-column: 2; justify-self: start; }
}
.image-library-name {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ==========================================================================
   M19.2 - Integrations: Discord (product/34 section 14; design/integrations/)
   New primitives, named for what they are and reused across the area. Everything takes the
   vaBase tokens EXCEPT the .discord-surface block below, which deliberately imitates Discord's own
   message surface so an administrator sees what members will see (product/34 0.2 rule 5).
   ========================================================================== */

/* A warning banner - the amber sibling of .alert-error / .alert-success (words + mark, never colour
   alone). First needed by the Discord area; general by name. */
.alert-warning {
    display: flex;
    gap: 0.6rem;
    align-items: flex-start;
    background: color-mix(in srgb, var(--vb-warning) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--vb-warning) 45%, transparent);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.7rem 1rem;
    margin: 0.75rem 0 1rem;
}
.alert-warning > span[aria-hidden="true"] { color: var(--vb-warning); font-weight: 700; }

/* A badge that carries its own mark (the boards' check, ! or cross) drops the generic status dot, so
   the mark is not said twice. */
.badge-marked::before { content: none; }

.btn-lg {
    padding: 0.7rem 1.4rem;
    font-size: 1rem;
}

/* (The Discord area's own sub-nav tab rules went at M24.6: those tabs are section tabs now.) */

/* The Discord mark: a rounded tile in Discord's own blurple. */
.discord-mark {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    background: #5865f2;
    color: #ffffff;
}
.discord-mark-lg { width: 56px; height: 56px; border-radius: 14px; }

.discord-unavailable p { margin: 0.25rem 0 0; }

/* First run: the Connect hero beside a sample post, then the three steps. */
.discord-connect {
    padding: 1.75rem;
    background: radial-gradient(ellipse at top left, var(--vb-primary-soft), transparent 55%), var(--vb-surface);
}
.discord-connect-hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 2rem;
    align-items: center;
}
.discord-connect-copy h2 { font-size: 1.5rem; margin: 1rem 0 0.4rem; }
.discord-connect-copy form { margin: 1.1rem 0 0.9rem; }
.discord-connect-note { font-size: 0.88rem; }
.discord-steps {
    list-style: none;
    margin: 1.75rem 0 0;
    padding: 1.25rem 0 0;
    border-top: 1px solid var(--vb-border);
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
}
.discord-step p { margin: 0.3rem 0 0; font-size: 0.9rem; }
.discord-step + .discord-step { border-left: 1px solid var(--vb-border); padding-left: 1rem; }
.discord-step-head { display: flex; align-items: center; gap: 0.6rem; }
.discord-step-num {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 1px solid var(--vb-border-strong);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--vb-text-muted);
    flex: none;
}
.discord-step-current .discord-step-num {
    background: var(--vb-primary);
    border-color: transparent;
    color: var(--vb-on-primary);
}

/* Back from Discord: name the connection, pick the first channel. */
.discord-welcome-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 2rem;
    margin-top: 1rem;
}
.discord-server-summary { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.5rem; }

/* The predictive channel field (DiscordChannelField): an input with a leading #, and the matches as a
   raised list below it - each saying whether vaBase can post there, in words. */
.discord-channel-input {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    background: var(--vb-bg);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    padding: 0 0.7rem;
}
.discord-channel-input input { border: none; background: transparent; flex: 1; min-width: 0; padding-left: 0; }
.discord-channel-input input:focus { outline: none; }
.discord-channel-input:focus-within { border-color: var(--vb-primary-bright); }
.discord-channel-results {
    margin-top: 0.5rem;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    padding: 0.35rem;
}
.discord-channel-results ul { list-style: none; margin: 0; padding: 0; }
.discord-channel-results > p { margin: 0.5rem; }
.discord-channel-option {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.55rem 0.6rem;
    border-radius: var(--vb-radius-sm);
    background: transparent;
    border: none;
    color: inherit;
    font: inherit;
    text-align: left;
}
button.discord-channel-option { cursor: pointer; }
button.discord-channel-option:hover,
button.discord-channel-option:focus-visible { background: var(--vb-primary-soft); outline: none; }
.discord-channel-option-blocked { color: var(--vb-text-muted); }
.discord-channel-name { flex: 1; min-width: 0; }
.discord-channel-parent { text-transform: uppercase; font-size: 0.78rem; letter-spacing: 0.04em; }
.discord-channel-selected { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }

/* Connections: one card per server. */
.discord-connection-head { display: flex; align-items: flex-start; gap: 0.9rem; }
.discord-connection-head h3 { margin: 0; }
.discord-connection-head > h3 { flex: 1; min-width: 0; }
.discord-connection-head + p { margin-top: 0.6rem; }
.discord-connection-title { flex: 1; min-width: 0; }
.discord-connection-name { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.discord-connection-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.discord-connection-actions form,
.discord-connection .card-actions form,
.page-header-actions form { margin: 0; }
.discord-connection .alert-warning { margin: 1rem 0 0; align-items: center; }
.discord-alert-body { flex: 1; min-width: 0; }
.discord-connection-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
    gap: 2rem;
    margin-top: 1.25rem;
}
.discord-section-label {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--vb-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 0.6rem;
}
.discord-permission-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.45rem; }
.discord-permission-list li { display: flex; align-items: center; gap: 0.6rem; }
.discord-channels-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
.discord-channels-head .discord-section-label { flex: 1; margin: 0; }
.discord-channel-table { width: 100%; border-collapse: collapse; margin-top: 0.6rem; font-size: 0.92rem; }
.discord-channel-table td { padding: 0.6rem 0.5rem; border-bottom: 1px solid var(--vb-border); }
.discord-channel-table td:last-child { text-align: right; }

/* ---- Discord's own message surface (deliberately NOT vaBase tokens) ------------------------------
   Discord's dark-theme greys, embed bar, link buttons and APP tag, so a preview reads as the message
   members will see. M19.3's DiscordMessagePreview renders into these, and adds the light theme. */
.discord-surface {
    background: #313338;
    color: #dbdee1;
    border-radius: 10px;
    padding: 1rem 1.1rem;
    font-family: "gg sans", "Segoe UI", system-ui, sans-serif;
    font-size: 0.95rem;
    line-height: 1.4;
}
.discord-channel { font-size: 0.8rem; color: #949ba4; font-weight: 600; margin-bottom: 0.6rem; }
.discord-message { display: flex; gap: 0.9rem; }
.discord-message-body { flex: 1; min-width: 0; }
.discord-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: #5e00ea;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #ffffff;
    font-weight: 800;
}
.discord-name { font-weight: 600; color: #f2f3f5; }
.discord-app-tag {
    background: #5865f2;
    color: #ffffff;
    font-size: 0.66rem;
    font-weight: 700;
    border-radius: 4px;
    padding: 0 0.3rem;
    margin-left: 0.3rem;
    vertical-align: 1px;
}
.discord-time { color: #949ba4; font-size: 0.75rem; margin-left: 0.45rem; }
.discord-embed {
    margin-top: 0.4rem;
    background: #2b2d31;
    border-left: 4px solid #5e00ea;
    border-radius: 4px;
    padding: 0.6rem 0.9rem 0.75rem 0.75rem;
    max-width: 470px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0 0.9rem;
}
.discord-author { font-size: 0.84rem; font-weight: 600; color: #f2f3f5; display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem; }
.discord-author i { width: 22px; height: 22px; border-radius: 50%; background: #7c4dff; display: inline-block; }
.discord-title { font-weight: 700; color: #00a8fc; margin-bottom: 0.25rem; }
.discord-description { font-size: 0.88rem; }
/* M20.4: Discord keeps a message's line breaks - /online's rows, a template's own lines - so the preview does too. */
.discord-description, .discord-content, .discord-field { white-space: pre-line; }
.discord-thumb {
    width: 72px;
    height: 72px;
    border-radius: 6px;
    background: linear-gradient(135deg, #3a3c43, #24262b);
    grid-column: 2;
    grid-row: 1 / span 2;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #6d6f78;
    font-size: 0.7rem;
    text-align: center;
}
.discord-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem 0.6rem; margin-top: 0.5rem; grid-column: 1 / -1; }
.discord-field { font-size: 0.84rem; }
.discord-field b { display: block; font-size: 0.8rem; color: #f2f3f5; }
.discord-field-full { grid-column: 1 / -1; }
.discord-progress { height: 6px; border-radius: 999px; background: #1e1f22; overflow: hidden; margin-top: 0.25rem; }
.discord-progress > span { display: block; height: 100%; background: #7c4dff; border-radius: 999px; }
.discord-progress-sample > span { width: 31%; }
.discord-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
.discord-button {
    background: #4e5058;
    color: #ffffff;
    border-radius: 4px;
    padding: 0.35rem 0.9rem;
    font-size: 0.88rem;
    font-weight: 500;
}
.discord-button::after { content: "\2197"; font-size: 0.75rem; opacity: 0.8; margin-left: 0.35rem; }

/* Phone width (390): everything stacks; the Discord surface keeps its own look. */
@media (max-width: 720px) {
    .discord-connect-hero,
    .discord-welcome-grid,
    .discord-connection-body,
    .discord-steps { grid-template-columns: minmax(0, 1fr); }
    .discord-step + .discord-step { border-left: none; padding-left: 0; }
    .discord-connection-head { flex-wrap: wrap; }
    .discord-connection-actions { width: 100%; }
    .discord-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- M19.3: DiscordMessagePreview (product/34 section 14) ----------------------------------------
   The same Discord surface the first-run sample introduced, now drawn from a RenderedDiscordMessage: the
   accent bar takes the message's own colour (--discord-accent, set per message), pictures are real
   images, and Discord's LIGHT theme is the second set of greys. Still deliberately not vaBase tokens. */
.discord-embed { border-left-color: var(--discord-accent, #5e00ea); }
.discord-content { margin-top: 0.15rem; }
.discord-author img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
img.discord-thumb { object-fit: cover; display: block; }
.discord-image {
    grid-column: 1 / -1;
    margin-top: 0.6rem;
    max-width: 100%;
    border-radius: 6px;
    display: block;
}
.discord-footer { grid-column: 1 / -1; margin-top: 0.55rem; font-size: 0.75rem; color: #949ba4; }
.discord-title-plain { color: #f2f3f5; }

.discord-surface-light { background: #ffffff; color: #313338; border: 1px solid #e3e5e8; }
.discord-surface-light .discord-name,
.discord-surface-light .discord-author,
.discord-surface-light .discord-field b,
.discord-surface-light .discord-title-plain { color: #060607; }
.discord-surface-light .discord-embed { background: #f2f3f5; }
.discord-surface-light .discord-button { background: #e3e5e8; color: #313338; }
.discord-surface-light .discord-progress { background: #e3e5e8; }

/* M20.4: the Commands page (the Commands and CommandsStates boards). */
.discord-commands-intro { margin: 1rem 0; }
.discord-commands-intro h2 { margin: 0 0 0.25rem; font-size: 1.15rem; }
.discord-commands-intro p { margin: 0; max-width: 90ch; }
.discord-commands-note { margin: 0.5rem 0 0; font-size: 0.88rem; }
.discord-commands-start { display: grid; gap: 0.5rem; margin-bottom: 1rem; }
.discord-commands-start p { margin: 0; }
.discord-commands-switch-row { display: flex; align-items: center; gap: 0.75rem; }
.discord-commands-table td:first-child { min-width: 12rem; }
.discord-commands-table .toggle-switch { margin-top: 0.15rem; }
.discord-commands-alert { margin-top: 0.75rem; }
.discord-commands-body { margin-top: 1.25rem; }
.discord-command-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.75rem; }
.discord-command-card { margin: 0; padding: 0.85rem 1rem; display: grid; gap: 0.3rem; align-content: start; font-size: 0.92rem; }
.discord-command-name { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-weight: 700; color: var(--vb-primary-bright); }
.discord-command-who { font-size: 0.8rem; color: var(--vb-text-muted); }
.discord-commands-health { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.45rem; font-size: 0.9rem; }
@media (max-width: 900px) {
    .discord-command-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .discord-command-grid { grid-template-columns: minmax(0, 1fr); }
    .discord-commands-table td:first-child { min-width: 0; }
}

/* M20.8: the Overview's bot line under the Servers card (the AdminV15 board, frame 1). */
.discord-bot-line { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; border-top: 1px solid var(--vb-border); margin-top: 0.9rem; padding-top: 0.65rem; font-size: 0.85rem; }

/* M20.3: a mention, as Discord draws one - the boards' .dc-mention, in both themes. */
.discord-mention { background: rgba(88, 101, 242, 0.3); color: #c9cdfb; border-radius: 3px; padding: 0 2px; font-weight: 500; }
.discord-surface-light .discord-mention { background: rgba(88, 101, 242, 0.15); color: #505cdc; }
.discord-mention-hint { margin: 0.4rem 0 0; font-size: 0.8rem; }

/* The gallery of every default message (M19.3's review page, the DiscordMessages board). */
.discord-gallery { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem; }
.discord-gallery-item h3 { font-size: 0.95rem; margin: 0 0 0.5rem; }
.discord-gallery-group { margin: 2rem 0 0.75rem; }
@media (max-width: 900px) { .discord-gallery { grid-template-columns: minmax(0, 1fr); } }

/* --------------------------------------------------------------------------
   M19.4 - Discord automations: the list, the guided editor, the condition rows
   (the Automations, AutomationEditor and AutomationStates boards)
   -------------------------------------------------------------------------- */

/* A trigger's icon: the list's rows and the editor's chosen trigger. */
.automation-icon {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border);
    color: var(--vb-primary-bright);
}

.automation-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    margin: -0.4rem 0 1rem;
}
.automation-toolbar .choice-chips { flex: 1; min-width: 0; }
.automation-search { width: 16rem; max-width: 100%; }

.automation-list { display: grid; gap: 0.75rem; }
.automation-row {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 1rem 1.25rem;
    margin: 0;
}
.automation-row-off { opacity: 0.72; }
.automation-row-attention { border-color: color-mix(in srgb, var(--vb-warning) 50%, transparent); }
.automation-row-main { flex: 1; min-width: 0; }
.automation-row-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.automation-row-title h3 { margin: 0; font-size: 1.05rem; }
.automation-row-when { margin-top: 0.2rem; }
.automation-row-where { margin-top: 0.2rem; font-size: 0.88rem; }
.automation-channel { color: var(--vb-text); }
.automation-channel-problem { color: var(--vb-warning); }
.automation-row-last { text-align: right; font-size: 0.82rem; display: grid; }
.automation-row-last strong { color: var(--vb-text); display: inline-flex; align-items: center; justify-content: flex-end; gap: 0.4rem; }
.automation-row > .toggle-switch { margin-top: 0.5rem; }

/* The ⋯ menu: a small raised list under its button. */
.automation-menu-why { display: block; font-size: 0.78rem; margin-top: 0.1rem; }
/* The open menu floats over the rows below it. Every .card lifts on hover with a transform, which makes the hovered
   row its own stacking context - so without this the NEXT row's switch painted through the menu. The row that owns
   the open menu is raised above its neighbours, and the menu (itself a .card) does not lift. */

.automation-empty { text-align: center; padding: 2rem; }
.automation-empty strong { font-size: 1.05rem; }
.automation-empty p { max-width: 52ch; margin: 0.3rem auto 0.9rem; }

/* The editor: the step rail, the steps, the sticky preview. */
/* The board's 168 / 330 columns are drawn on a 1,160px page; the portal's content is 1,080px, so both
   side columns give a little back to the steps, where a condition row needs the room. */
.automation-editor {
    display: grid;
    grid-template-columns: 150px minmax(0, 1fr) 300px;
    gap: 1.25rem;
    align-items: start;
}
.automation-steps { position: sticky; top: 1rem; display: grid; gap: 0.15rem; }
.automation-step {
    display: flex;
    gap: 0.75rem;
    align-items: flex-start;
    padding: 0.6rem 0.75rem;
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    text-decoration: none;
}
.automation-step:hover { background: var(--vb-surface); }
.automation-step-num {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 1px solid var(--vb-border-strong);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.78rem;
    font-weight: 700;
    flex: none;
    color: var(--vb-text-muted);
}
.automation-step-done .automation-step-num {
    background: color-mix(in srgb, var(--vb-success) 18%, transparent);
    border-color: color-mix(in srgb, var(--vb-success) 50%, transparent);
    color: var(--vb-success);
}
.automation-step-detail { display: block; font-size: 0.78rem; font-weight: 400; }

.automation-editor-steps { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; min-width: 0; }
.automation-editor-steps > .card { margin: 0; }
.automation-step-current { border-color: var(--vb-primary); }
.automation-step-head { display: flex; align-items: center; gap: 0.75rem; }
.automation-step-head > h2 { flex: 1; min-width: 0; margin: 0; }
.automation-editor-steps h2.discord-section-label { margin-bottom: 0.6rem; }
.automation-step-note { text-transform: none; letter-spacing: 0; font-weight: 400; }

.automation-trigger { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.6rem; }
.automation-trigger-copy { flex: 1; min-width: 0; }
.automation-editor .alert-warning { margin: 0.9rem 0 0; }
.automation-trigger-picker {
    border-top: 1px solid var(--vb-border);
    margin-top: 0.9rem;
    padding-top: 0.9rem;
}
.automation-choice-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
    margin-top: 0.75rem;
}
.automation-choice {
    display: grid;
    gap: 0.15rem;
    text-align: left;
    font: inherit;
    font-size: 0.9rem;
    color: var(--vb-text);
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    padding: 0.65rem 0.8rem;
    cursor: pointer;
}
.automation-choice:hover { border-color: var(--vb-border-strong); }
.automation-choice-on { border-color: var(--vb-primary); }
.automation-choice-title { display: flex; align-items: center; gap: 0.5rem; }
.automation-choice-title strong { flex: 1; min-width: 0; }
.automation-message-list { display: grid; gap: 0.5rem; }

.automation-summary { display: flex; gap: 0.75rem; align-items: flex-start; }
.automation-destination { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.automation-destination .field { margin: 0; }
.automation-destination select { width: 100%; }
.automation-switch-on { display: flex; align-items: flex-start; gap: 1rem; }
.automation-switch-on > div { flex: 1; min-width: 0; }
.automation-switch-on p { margin: 0; }
.automation-preview { position: sticky; top: 1rem; }
.automation-preview .field { margin-top: 0.9rem; }
.automation-preview select { width: 100%; }

/* One condition: [field] [how to compare] [value] [remove]. */
.condition-rows { display: grid; gap: 0.5rem; }
.condition-row { display: flex; align-items: flex-start; gap: 0.5rem; }
.condition-row .condition-field { width: 10rem; }
.condition-row .condition-op { width: 8.75rem; }
.condition-value { flex: 1; min-width: 6.5rem; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.condition-value .field { margin: 0; flex: 1; min-width: 0; }
/* A shared predictive field brings its own label; the row's first box already names the value. */
.condition-value .field > label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
.condition-value .suggest-wrap { flex: 1; min-width: 10rem; }
.condition-input { display: inline-flex; align-items: center; gap: 0.4rem; }
.condition-row .condition-input input { width: 5.5rem; }
.condition-switch { display: inline-flex; align-items: center; gap: 0.5rem; }
.condition-add { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.6rem 0 0; }

/* M21.1 - conditions v2 (the Conditions board): "Match [all | any] of these" over a rule or a group, the
   AND / OR between rows, and a group - an indented card one level deep, outlined in danger while it cannot
   be saved, its reason inside it. */
.condition-match { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; font-size: 0.9rem; }
.condition-match-select { width: 6.9rem; min-height: 2rem; padding-top: 0.25rem; padding-bottom: 0.25rem; }
.condition-joiner { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.06em; color: var(--vb-primary-bright); margin: 0 0 0 0.25rem; }
.condition-group {
    border: 1px solid var(--vb-border-strong);
    border-radius: 10px;
    padding: 0.75rem 0.875rem;
    margin: 0.125rem 0 0.125rem 1.375rem;
    background: color-mix(in srgb, var(--vb-bg) 50%, transparent);
}
.condition-group-invalid { border-color: color-mix(in srgb, var(--vb-danger) 60%, transparent); }
.condition-group-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.625rem; margin-bottom: 0.5rem; font-size: 0.85rem; }
.condition-group-head > span { display: inline-flex; align-items: center; gap: 0.5rem; white-space: nowrap; }
.condition-group-problem { margin: 0.5rem 0 0; }
.condition-group .condition-add { margin-top: 0.5rem; }
/* The delivery log's rule, each clause marked true or not (frame 6). */
.condition-met { color: var(--vb-success); }
.condition-unmet { color: var(--vb-danger); }

@media (max-width: 1100px) {
    .automation-editor { grid-template-columns: minmax(0, 1fr); }
    .automation-steps { display: none; }
    .automation-preview { position: static; }
}
@media (max-width: 720px) {
    .automation-row { flex-wrap: wrap; }
    .automation-row-main { flex-basis: calc(100% - 56px); }
    .automation-row-last { text-align: left; flex: 1; }
    .automation-choice-grid,
    .automation-destination { grid-template-columns: minmax(0, 1fr); }
    .condition-row { flex-wrap: wrap; }
    .condition-row .condition-field,
    .condition-row .condition-op { width: 100%; }
    /* Phone width (MobileV2 frame 5): each condition a card of stacked boxes, a group flush with the rule,
       every control a 44 px target. */
    .condition-row {
        border: 1px solid var(--vb-border);
        border-radius: var(--vb-radius);
        background: var(--vb-surface);
        padding: 0.625rem 0.75rem;
    }
    .condition-row select,
    .condition-row input,
    .condition-match-select { min-height: 2.75rem; }
    .condition-group { margin-left: 0; }
    .automation-search { width: 100%; }
}

/* M21.3 - webhooks (the Webhooks, WebhookEndpointEditor and WebhookEndpoint boards). */
/* The endpoints list: name and masked host, what it sends, its state, its last delivery, Open. */
.webhook-endpoint-list { padding: 0; overflow: hidden; }
.webhook-endpoint-row {
    display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) 150px 190px 90px;
    gap: 0.75rem;
    align-items: center;
    padding: 0.875rem 1.25rem;
    border-bottom: 1px solid var(--vb-border);
}
.webhook-endpoint-row:last-child { border-bottom: none; }
.webhook-endpoint-head {
    padding-top: 0.625rem;
    padding-bottom: 0.625rem;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--vb-text-muted);
}
.webhook-endpoint-host { font-size: 0.8rem; overflow-wrap: anywhere; }
.webhook-endpoint-sends,
.webhook-endpoint-last { font-size: 0.85rem; }
.webhook-endpoint-foot { font-size: 0.82rem; margin: 0.5rem 0 0; }
/* No endpoints yet: the promise, the three uses, the way in. */
.webhook-empty { padding: 2.1rem 1.9rem; text-align: center; }
.webhook-empty h2 { font-size: 1.125rem; margin: 0; }
.webhook-empty > p.muted { max-width: 60ch; margin: 0.4rem auto 0; }
.webhook-empty-uses { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.875rem; margin-top: 1.4rem; text-align: left; }
.webhook-empty-uses > .card { margin: 0; padding: 0.875rem 1rem; background: var(--vb-surface-raised); }
.webhook-empty-uses p { margin: 0.25rem 0 0; font-size: 0.85rem; }
.webhook-empty-actions { margin: 1.4rem 0 0; }
.webhook-empty-note { font-size: 0.82rem; margin: 0.875rem 0 0; }
/* New endpoint: the step rail beside one step at a time (the automation editor's rail). */
.webhook-editor { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 1.6rem; align-items: start; }
.webhook-editor-steps { display: grid; gap: 1rem; min-width: 0; }
.webhook-editor-steps > .card { margin: 0; }
.automation-step-on { background: var(--vb-primary-soft); }
.automation-step-on .automation-step-num { background: var(--vb-primary); border-color: transparent; color: var(--vb-on-primary); }
.webhook-field-refusal { margin: -0.25rem 0 0.9rem; }
/* M21.4 - the editor's events (a checklist by category, searched as typed) and each event's conditions. */
.webhook-event-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin-bottom: 0.9rem; }
.webhook-event-toolbar .choice-chips { flex: 1 1 26rem; min-width: 0; }
.webhook-event-search { width: 14rem; max-width: 100%; }
.webhook-event-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.65rem 1.4rem; }
.webhook-event { display: flex; gap: 0.625rem; align-items: flex-start; font-size: 0.9rem; cursor: pointer; }
.webhook-event input[type="checkbox"] { margin-top: 0.2rem; flex: none; }
.webhook-event-copy { display: grid; gap: 0.1rem; min-width: 0; }
.webhook-event-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.webhook-event-key { font-size: 0.75rem; }
.webhook-event-about { font-size: 0.82rem; }
.webhook-event-count { font-size: 0.82rem; margin: 0.75rem 0 0; }
.webhook-event-none { margin: 0 0 0.5rem; }
.webhook-event-conditions { padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--vb-border); }
.webhook-event-conditions:last-of-type { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
.webhook-event-conditions-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.625rem; margin-bottom: 0.625rem; }
.webhook-event-conditions-head .muted { font-size: 0.85rem; }
.webhook-event-conditions .automation-summary { margin-top: 0.75rem; }
.webhook-sends-edit { font-weight: 600; margin-left: 0.5rem; }
@media (max-width: 700px) {
    .webhook-event-grid { grid-template-columns: minmax(0, 1fr); }
    .webhook-event-search { width: 100%; }
}
/* A secret shown once: the value in a box of its own, and what to do with it. */
.token-box {
    font-family: "Cascadia Mono", Consolas, monospace;
    font-size: 0.85rem;
    background: var(--vb-bg);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    padding: 0.625rem 0.75rem;
    margin-top: 0.75rem;
    word-break: break-all;
    color: var(--vb-text);
}
.token-box-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.625rem; margin-top: 0.625rem; font-size: 0.85rem; }
.webhook-secret-note { font-size: 0.82rem; margin: 0.75rem 0 0; }
.webhook-secret-shown { margin-bottom: 1rem; }
/* One endpoint: its facts beside the signature hint. */
.webhook-detail { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 1.4rem; align-items: start; }
.webhook-detail > .card { margin: 0; }
.webhook-verify { background: var(--vb-surface-raised); }
.webhook-verify p { margin: 0.35rem 0 0; font-size: 0.85rem; }
.webhook-notice { display: flex; gap: 0.75rem; align-items: flex-start; }
.webhook-switch { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; }
.webhook-overlap { grid-template-columns: minmax(0, 1fr); margin: 1rem 0; }
/* M21.5 - a sending endpoint: its two columns, the figures, the recent deliveries, the test and every state in words. */
.webhook-detail-main,
.webhook-detail-side { display: grid; gap: 1rem; min-width: 0; align-content: start; }
.webhook-detail-main > .card,
.webhook-detail-side > .card { margin: 0; }
.webhook-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.webhook-tiles { margin: 0; }
.webhook-tiles .stat-tile { gap: 0.1rem; margin: 0; padding: 0.9rem 1.1rem; }
.webhook-recent .delivery-log-table { min-width: 0; }
.webhook-recent .delivery-log-foot { padding: 0 0.6rem; }
.webhook-recent-label { margin: 0.5rem 0 -0.25rem; }
.webhook-recent { padding: 0.25rem 0.5rem 0.75rem; }
.webhook-recent-none { margin: 0; padding: 1rem 1.2rem; }
.webhook-verify .btn { margin-top: 0.6rem; }
.webhook-test-result { display: flex; gap: 0.75rem; align-items: flex-start; margin: 0.75rem 0 0; font-size: 0.875rem; }
.webhook-state-line { margin: 0 0 1rem; font-size: 0.9rem; }
.webhook-notice { margin: 0 0 1rem; }
.webhook-notice-detail { font-size: 0.85rem; margin-top: 0.15rem; }
.webhook-body { font-family: "Cascadia Mono", Consolas, monospace; font-size: 0.75rem; background: var(--vb-bg); border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); padding: 0.75rem 0.875rem; margin: 0.4rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 22rem; overflow-y: auto; }
.webhook-drawer-facts { grid-template-columns: 110px minmax(0, 1fr); font-size: 0.82rem; margin-bottom: 0.5rem; }
.webhook-drawer-actions { flex-wrap: wrap; }
.webhook-drawer-note { font-size: 0.8rem; margin: 0.6rem 0 0; }
.webhook-retry-replay { margin-top: 1rem; }
.webhook-retry-replay .key-value-list { grid-template-columns: 90px minmax(0, 1fr); }
.webhook-resume-option { display: flex; gap: 0.6rem; align-items: flex-start; margin: 0.75rem 0 0; font-size: 0.9rem; }
.webhook-resume-option input { margin-top: 0.2rem; flex: none; }
.webhook-first-test { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin-top: 0.75rem; }
@media (max-width: 720px) {
    .webhook-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* A list of facts, label beside value (the WebhookEndpoint board's detail card). */
.key-value-list { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 0.5rem 1rem; margin: 0; font-size: 0.875rem; }
.key-value-list dt { color: var(--vb-text-muted); }
.key-value-list dd { margin: 0; overflow-wrap: anywhere; }
.key-value-list .btn-link { margin-left: 0.5rem; }
.page-header-title-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
@media (max-width: 960px) {
    .webhook-detail { grid-template-columns: minmax(0, 1fr); }
    .webhook-editor { grid-template-columns: minmax(0, 1fr); }
    .webhook-editor > .automation-steps { display: none; }
}
/* Phone width (MobileV2 frame 1): each endpoint a card of its own, nothing sideways. */
@media (max-width: 720px) {
    .webhook-endpoint-head { display: none; }
    .webhook-endpoint-row { grid-template-columns: minmax(0, 1fr) auto; gap: 0.35rem 0.75rem; }
    .webhook-endpoint-row > :nth-child(2),
    .webhook-endpoint-row > :nth-child(4) { grid-column: 1 / -1; }
    .webhook-empty-uses { grid-template-columns: minmax(0, 1fr); }
    .key-value-list { grid-template-columns: minmax(0, 1fr); gap: 0.15rem; }
    .key-value-list dd { margin-bottom: 0.6rem; }
}

/* M22.1 - who a server is for (the DiscordAudience board): the classification on the card, and the owner's dialog. */
.discord-audience { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin: 0.9rem 0 0; font-size: 0.9rem; }
.discord-audience-words { flex: 1 1 20rem; min-width: 0; }
.discord-audience-owner { font-size: 0.85rem; }
.discord-audience-dialog { max-width: 40rem; max-height: calc(100vh - 3rem); overflow-y: auto; }
.discord-audience-options { grid-template-columns: minmax(0, 1fr); margin: 0.9rem 0; }
.discord-audience-warning { margin: 0 0 0.9rem; }
.discord-audience-warning-detail { font-size: 0.85rem; margin-top: 0.15rem; }
.discord-audience-confirm { display: flex; gap: 0.6rem; align-items: flex-start; font-size: 0.9rem; margin: 0 0 0.9rem; }
.discord-audience-confirm input { margin-top: 0.2rem; flex: none; }
.discord-audience-matrix { margin: 0 0 0.5rem; font-size: 0.85rem; }
.discord-audience-matrix summary { cursor: pointer; font-weight: 600; }
.discord-audience-matrix table { width: 100%; border-collapse: collapse; margin-top: 0.6rem; }
.discord-audience-matrix th { text-align: left; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--vb-text-muted); padding: 0.4rem; border-bottom: 1px solid var(--vb-border); }
.discord-audience-matrix td { padding: 0.45rem 0.4rem; border-bottom: 1px solid var(--vb-border); vertical-align: top; }
/* M21.6 - the phone sweep (MobileV2 frames 1 and 2): what only a phone shows, what it hides, 44 px targets, and the
   endpoint page in the frame's order - the test, then the recent deliveries as cards, then the details. */
.webhook-phone-only { display: none; }
.webhook-endpoint-name { min-width: 0; }
.webhook-endpoint-status { display: flex; align-items: center; }
.webhook-recent-cards { padding: 0 0.25rem; }
.webhook-endpoint-link { color: var(--vb-text); font-weight: 650; }
.webhook-endpoint-link:hover { text-decoration: underline; }
.webhook-recent-card { border-top: 1px solid var(--vb-border); padding: 0.75rem 0.25rem; }
.webhook-recent-card:first-child { border-top: none; }
.webhook-recent-card-head { display: flex; align-items: center; gap: 0.5rem; justify-content: space-between; font-size: 0.9rem; }
.webhook-recent-card-head a { color: var(--vb-text); }
.webhook-recent-card-facts { font-size: 0.8rem; margin-top: 0.15rem; }
.webhook-recent-card .btn-link { margin-top: 0.35rem; min-height: 44px; }
@media (max-width: 720px) {
    .webhook-phone-only { display: block; }
    a.btn.webhook-phone-only { display: flex; justify-content: center; }
    .webhook-desktop-only,
    .webhook-recent .delivery-log-table.webhook-desktop-only { display: none; }
    /* The list: each endpoint a card - name and state, "host · N events", the last delivery; the card opens it. */
    .webhook-endpoint-row { position: relative; grid-template-columns: minmax(0, 1fr) auto; }
    .webhook-endpoint-row .webhook-endpoint-host,
    .webhook-endpoint-row .webhook-endpoint-sends,
    .webhook-endpoint-row .webhook-endpoint-last,
    .webhook-endpoint-row .webhook-endpoint-open { display: none; }
    .webhook-endpoint-row > .webhook-endpoint-facts,
    .webhook-endpoint-resume { grid-column: 1 / -1; }
    .webhook-endpoint-facts { font-size: 0.82rem; }
    .webhook-endpoint-link::after { content: ""; position: absolute; inset: 0; }
    .webhook-endpoint-resume { position: relative; z-index: 1; min-height: 44px; align-items: center; margin-top: 0.35rem; }
    .webhook-list-new { display: none; }
    .webhook-list-new-foot { min-height: 44px; align-items: center; margin-top: 0.75rem; }
    /* The endpoint page, in the frame's order. */
    .webhook-detail { display: flex; flex-direction: column; align-items: stretch; gap: 1rem; }
    .webhook-detail-main,
    .webhook-detail-side { display: contents; }
    .webhook-test-card { order: 1; background: transparent; border: none; padding: 0; }
    .webhook-test-card > .discord-section-label,
    .webhook-test-card > p { display: none; }
    .webhook-test-card .btn { width: 100%; min-height: 44px; justify-content: center; margin-top: 0; }
    .webhook-recent-label { order: 2; margin: 0.25rem 0 -0.5rem; }
    .webhook-recent { order: 3; }
    .webhook-facts-card { order: 4; }
    .webhook-tiles { order: 5; }
    .webhook-verify-card { order: 6; }
    .webhook-header-test { display: none; }
    .webhook-switch,
    .webhook-switch ~ .menu > .btn { min-height: 44px; }
}
/* M19.5 - the editor's "Try it" step: the test button beside what it does, and where the test got to. */
.automation-test { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
.automation-test .muted { flex: 1; min-width: 14rem; font-size: 0.9rem; }
.automation-test-result { display: flex; gap: 0.75rem; align-items: flex-start; margin: 0.9rem 0 0; }

/* M19.6 - the Live Flight Card in the editor: its options, and the switch inside the completion warning. */
.automation-inline-switch { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.6rem; }
.automation-excellent { display: flex; align-items: flex-start; gap: 0.75rem; }
.automation-excellent > div { flex: 1; min-width: 0; }
.automation-excellent-threshold { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.4rem; }
.automation-excellent-threshold input { width: 5rem; }

/* --------------------------------------------------------------------------
   M19.7 - the Integrations hub, the Discord Overview, the Delivery Log and onboarding
   (the Hub, DiscordOverview, DeliveryLog, Onboarding and Mobile boards)
   -------------------------------------------------------------------------- */

.integration-section-label {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--vb-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 1.75rem 0 0.75rem;
}
.integration-featured {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 20rem;
    gap: 1.5rem;
    margin: 0;
}
.integration-featured-main p { margin: 0.9rem 0; }
.integration-card-head { display: flex; align-items: center; gap: 0.75rem; }
.integration-card-head h3 { margin: 0; flex: 1; min-width: 0; font-size: 1rem; }
.integration-card-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.integration-card-title h2 { margin: 0; font-size: 1.3rem; }
.integration-capabilities { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.integration-capabilities span {
    border: 1px solid var(--vb-border-strong);
    border-radius: 999px;
    padding: 0.15rem 0.65rem;
    font-size: 0.8rem;
    color: var(--vb-text-muted);
}
.integration-featured-health { border-left: 1px solid var(--vb-border); padding-left: 1.5rem; }
.integration-health-row { display: flex; align-items: center; gap: 0.6rem; margin: 0.45rem 0; }
.integration-figures { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.75rem; margin-top: 1rem; }
.integration-figures .muted { font-size: 0.8rem; }
.integration-figures strong { font-size: 1.25rem; }
.integration-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
.integration-card { margin: 0; display: flex; flex-direction: column; }
.integration-card p { margin: 0.75rem 0; color: var(--vb-text-muted); font-size: 0.92rem; }
/* M21.5 (the HubV2 board): a developer card's one action, and the first-run card, dashed. */
.integration-card-action { margin-top: auto; padding-top: 0.75rem; }
.integration-card-first-run { border-style: dashed; background: transparent; }
.integration-card-head-wrap { flex-wrap: wrap; row-gap: 0.4rem; }
.integration-card-head-wrap h3 { flex: 1 1 auto; min-width: auto; }
.integration-card-icon {
    width: 36px;
    height: 36px;
    border-radius: 9px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border);
    color: var(--vb-primary-bright);
}
.integration-card-facts { font-size: 0.85rem; color: var(--vb-text-muted); margin-top: auto; }
.integration-card-links { margin-top: 0.75rem; display: flex; gap: 0.4rem; flex-wrap: wrap; }

/* The Discord Overview. */
.discord-overview-alert { display: flex; align-items: center; gap: 0.75rem; margin: 0 0 1rem; }
.discord-overview-alert form { margin: 0; }
.discord-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.9rem; margin-bottom: 1.25rem; }
.discord-tile { margin: 0; padding: 1rem 1.1rem; }
.discord-tile-figure { font-size: 1.75rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }
.discord-tile-figure .muted { font-size: 1rem; font-weight: 400; }
.discord-overview { display: grid; grid-template-columns: minmax(0, 1fr) 21rem; gap: 1.25rem; align-items: start; }
.discord-overview-main, .discord-overview-side { display: grid; gap: 1rem; min-width: 0; }
.discord-overview .card { margin: 0; }
.discord-overview-head { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.5rem; }
.discord-overview-head .discord-section-label { flex: 1; margin: 0; }
.discord-overview-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.discord-overview-table th {
    text-align: left;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--vb-text-muted);
    font-weight: 600;
    padding: 0.4rem 0.5rem;
    border-bottom: 1px solid var(--vb-border);
}
.discord-overview-table td { padding: 0.55rem 0.5rem; border-bottom: 1px solid var(--vb-border); vertical-align: top; }
.discord-overview-table tr:last-child td { border-bottom: none; }
.discord-overview-server { display: flex; align-items: center; gap: 0.75rem; margin: 0.6rem 0; }
.discord-overview-server > div { flex: 1; min-width: 0; }
.discord-overview-mode { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; margin-top: 0.5rem; }
.discord-overview-side p { margin: 0.4rem 0 0; }

/* The Delivery Log and its panel. */
.delivery-log { margin: 0; padding: 0.5rem 1rem 1rem; overflow-x: auto; }
.delivery-log-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; min-width: 46rem; }
.delivery-log-table th {
    text-align: left;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--vb-text-muted);
    font-weight: 600;
    padding: 0.55rem 0.6rem;
    border-bottom: 1px solid var(--vb-border);
}
.delivery-log-table td { padding: 0.65rem 0.6rem; border-bottom: 1px solid var(--vb-border); vertical-align: top; }
.delivery-log-open td { background: color-mix(in srgb, var(--vb-primary) 10%, transparent); }
.delivery-log-link { background: none; border: none; padding: 0; color: var(--vb-text); font: inherit; cursor: pointer; text-align: left; }
.delivery-log-link:hover strong { text-decoration: underline; }
.delivery-log-detail { font-size: 0.82rem; margin-top: 0.2rem; }
.delivery-log-foot { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-top: 0.75rem; }
.delivery-drawer-backdrop { justify-content: flex-end; align-items: stretch; }
.delivery-drawer {
    width: min(36rem, 100%);
    max-height: 100vh;
    overflow-y: auto;
    margin: 0;
    border-radius: var(--vb-radius) 0 0 var(--vb-radius);
    display: grid;
    gap: 0.75rem;
    align-content: start;
}
.delivery-drawer-head { display: flex; align-items: center; gap: 0.6rem; }
.delivery-drawer-head h3 { flex: 1; min-width: 0; margin: 0; }
.delivery-drawer-alert { display: flex; gap: 0.75rem; align-items: flex-start; margin: 0; }
.delivery-drawer-steps { font-size: 0.88rem; border-collapse: collapse; }
.delivery-drawer-steps td { padding: 0.3rem 0.75rem 0.3rem 0; vertical-align: top; }
.delivery-drawer .card-actions { margin: 0; }
.delivery-drawer-technical summary { cursor: pointer; color: var(--vb-text-muted); font-size: 0.88rem; }
.delivery-drawer-technical .mono { font-size: 0.8rem; margin-top: 0.5rem; overflow-wrap: anywhere; }
.delivery-drawer-technical pre { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.5rem 0 0; }

/* The Drawer (Drawer.razor): the delivery logs' drawer under a shared name. A record beside its
   list, never for editing: the backdrop is the modal's, the card slides in from the right at the
   same width the delivery logs settled on, and the head carries the title, the state pill and
   Close. The delivery logs keep their own class names until they are moved onto this one. */
.drawer-backdrop { justify-content: flex-end; align-items: stretch; }
.drawer {
    width: min(36rem, 100%);
    max-height: 100vh;
    overflow-y: auto;
    margin: 0;
    border-radius: var(--vb-radius) 0 0 var(--vb-radius);
    display: grid;
    gap: 0.75rem;
    align-content: start;
}
.drawer-head { display: flex; align-items: center; gap: 0.6rem; }
.drawer-head h3 { flex: 1; min-width: 0; margin: 0; }
.drawer-label {
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--vb-text-muted);
    margin-top: 0.35rem;
}
.drawer .key-value-list { margin: 0; }
.drawer-actions { margin: 0.25rem 0 0; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.drawer-actions .btn-danger { margin-left: auto; }
@media (max-width: 640px) {
    .drawer { width: 100%; border-radius: 0; }
    .drawer-actions .btn-danger { margin-left: 0; }
}

/* The progress line inside a table cell (the dashboard's .progress-line, sized for a row). */
table.data .progress-line { margin-top: 0.3rem; max-width: 9rem; }

/* The board's flight cell: the route stays on one line under the number; the source badge
   beneath it. The Airport filter is a predictive field the width of a code and a name. */
.board-route { white-space: nowrap; }
.board-source { margin-top: 0.25rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.filter-bar .filter-airport { margin: 0; width: 11rem; }
.filter-bar .filter-airport input { margin: 0; }
/* A filter bar showing a predictive list sits above the table after it. Every card lifts on hover with a
   transform, which makes the hovered bar its own stacking context, so without this the table painted over
   the list the moment the pointer reached it and a click on Select landed in the table. */
.filter-bar:has(.suggest-popout) { position: relative; z-index: 5; }

/* The move modal's facts: the aircraft and where vaBase has it, as two lines above the field. */
.move-facts { display: grid; gap: 0.35rem; margin-bottom: 0.25rem; }
.move-facts-id { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: baseline; font-size: 1.05rem; }
.move-facts-id .mono { font-weight: 600; }
.move-facts-at { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: baseline; }
.move-alt { margin: 0.25rem 0 0; }

/* Onboarding - "How should flights appear?" */
.onboarding-lede { max-width: 70ch; margin-top: -0.5rem; }
.onboarding-done { display: flex; gap: 0.75rem; align-items: flex-start; }
.onboarding-channel { max-width: 32rem; margin: 1rem 0; }
.onboarding-modes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
.onboarding-mode {
    margin: 0;
    display: grid;
    gap: 0.75rem;
    align-content: start;
    text-align: left;
    font: inherit;
    color: var(--vb-text);
    cursor: pointer;
}
.onboarding-mode-on { border-color: var(--vb-primary); box-shadow: 0 0 0 1px var(--vb-primary); }
.onboarding-mode-title { display: flex; align-items: center; gap: 0.6rem; }
.onboarding-radio {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid var(--vb-border-strong);
    flex: none;
}
.onboarding-mode-on .onboarding-radio { border-color: var(--vb-primary); background: radial-gradient(var(--vb-primary) 45%, transparent 50%); }
.onboarding-sample { display: block; padding: 0.6rem; font-size: 0.85rem; }
.onboarding-sample .discord-embed { display: block; margin: 0; }
.onboarding-sample .discord-title, .onboarding-sample .discord-description { display: block; }
.onboarding-sample-custom { border: 1px dashed var(--vb-border-strong); border-radius: var(--vb-radius-sm); color: var(--vb-text-muted); }
.onboarding-creates { display: grid; gap: 0.25rem; font-size: 0.9rem; }
.onboarding-note { font-size: 0.85rem; }
.onboarding-community { margin: 0; }
.onboarding-community .field { margin: 0; }
.onboarding-community .field > label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
.onboarding-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin-top: 1.25rem; }
.onboarding-actions p { flex: 1; min-width: 16rem; margin: 0; }

@media (max-width: 1000px) {
    .integration-featured, .discord-overview, .onboarding-modes { grid-template-columns: minmax(0, 1fr); }
    .integration-featured-health { border-left: none; padding-left: 0; border-top: 1px solid var(--vb-border); padding-top: 1rem; }
    .integration-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .discord-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .integration-grid { grid-template-columns: minmax(0, 1fr); }
    .discord-overview-alert { flex-wrap: wrap; }
    .delivery-drawer { border-radius: 0; }
}

/* M19.7 - at phone width the Overview's and the log's tables become cards (the Mobile board: "tables become
   cards ... nothing scrolls sideways"). */
@media (max-width: 640px) {
    .discord-overview-table, .discord-overview-table tbody, .discord-overview-table tr, .discord-overview-table td,
    .delivery-log-table, .delivery-log-table tbody, .delivery-log-table tr, .delivery-log-table td {
        display: block;
        width: 100%;
    }
    .discord-overview-table thead, .delivery-log-table thead { display: none; }
    .delivery-log { overflow-x: visible; }
    .delivery-log-table { min-width: 0; }
    .discord-overview-table tr, .delivery-log-table tr { border-bottom: 1px solid var(--vb-border); padding: 0.6rem 0; }
    .discord-overview-table td, .delivery-log-table td { border: none; padding: 0.1rem 0; }
    .discord-overview-table td, .delivery-log-table td { overflow-wrap: anywhere; }
}

/* --------------------------------------------------------------------------
   M19.8 - Discord templates: the library and the message designer
   (the Templates and Designer boards)
   -------------------------------------------------------------------------- */

/* Shared helpers the designer introduced: a label that is not a <label> (a group's heading), a button that
   reads as a link, and text only a screen reader hears. */
.field-label { display: block; color: var(--vb-text); font-size: 0.88rem; font-weight: 550; margin-bottom: 0.35rem; }
.btn-link {
    border: none;
    background: none;
    padding: 0;
    font: inherit;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--vb-primary-bright);
    cursor: pointer;
}
.btn-link:hover { text-decoration: underline; }
.btn-link:disabled { color: var(--vb-text-muted); cursor: default; text-decoration: none; }
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ReorderList's own rows, for a consumer with no row style of its own (the cabin announcements pass theirs). */
.reorder-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.45rem 0; border-top: 1px solid var(--vb-border); }
.reorder-row:first-child { border-top: none; }
.reorder-row.is-dragging { opacity: 0.5; }
.reorder-grip { color: var(--vb-border-strong); cursor: grab; user-select: none; flex: none; }

/* ColourField: the native picker, its hex, and the swatches (the airline's colour first). */
.colour-field { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.colour-field .colour-field-picker { flex: none; width: 36px; height: 36px; padding: 2px; border-radius: var(--vb-radius-sm); border: 1px solid var(--vb-border-strong); background: var(--vb-surface); cursor: pointer; }
.colour-field .colour-field-hex { flex: none; width: 7.5rem; }
.colour-field-swatches { display: flex; gap: 0.35rem; }
.colour-swatch {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 2px solid var(--vb-surface);
    background: var(--swatch, #5e00ea);
    box-shadow: 0 0 0 1px var(--vb-border-strong);
    cursor: pointer;
    padding: 0;
}
.colour-swatch[aria-checked="true"] { box-shadow: 0 0 0 2px var(--vb-primary-bright); }
.colour-swatch-brand { position: relative; }
.colour-swatch-brand::after { content: "\2605"; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #ffffff; font-size: 0.7rem; }

/* The library. */
.template-alert > span[aria-hidden="true"] { color: var(--vb-danger); font-weight: 700; }
.template-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-bottom: 1.75rem; }
.template-grid-defaults { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.template-tile { display: flex; flex-direction: column; gap: 0.4rem; margin: 0; min-width: 0; }
.template-tile-link { color: var(--vb-text); text-decoration: none; }
.template-tile-link:hover { border-color: var(--vb-border-strong); }
.template-tile-thumb { display: block; text-decoration: none; }
.template-tile-name { display: flex; align-items: flex-start; gap: 0.5rem; }
.template-tile-name strong { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.template-tile-meta { font-size: 0.82rem; }
.template-tile-improved {
    display: flex;
    gap: 0.5rem;
    font-size: 0.84rem;
    background: color-mix(in srgb, var(--vb-primary-bright) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--vb-primary-bright) 35%, transparent);
    border-radius: var(--vb-radius-sm);
    padding: 0.5rem 0.65rem;
}
.template-tile-improved > span[aria-hidden="true"] { color: var(--vb-primary-bright); font-weight: 700; }
.template-tile-actions { display: flex; align-items: center; gap: 0.4rem; margin-top: auto; }
.template-tile-start {
    justify-content: center;
    border: 1px dashed var(--vb-border-strong);
    border-radius: var(--vb-radius);
    padding: 1.1rem;
    font-size: 0.9rem;
}
.template-defaults-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1rem; margin-bottom: 0.75rem; }
.template-defaults-head .integration-section-label { flex: 1 1 24rem; margin: 0; }
.template-defaults-note { text-transform: none; letter-spacing: 0; font-weight: 400; }

/* A message drawn small: Discord's own greys, the message's own accent. */
.discord-mini { background: #313338; border-radius: 8px; padding: 0.6rem; }
.discord-mini-embed {
    background: #2b2d31;
    border-left: 3px solid var(--discord-accent, #5e00ea);
    border-radius: 4px;
    padding: 0.45rem 0.6rem;
    min-height: 3.2rem;
}
.discord-mini-title { color: #f2f3f5; font-weight: 700; font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.discord-mini-text {
    color: #dbdee1;
    font-size: 0.74rem;
    margin-top: 0.15rem;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.discord-mini-text strong { color: #f2f3f5; }

/* The designer: the message in parts, the message as Discord shows it. */
.template-designer { display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: 1.25rem; align-items: start; }
.template-editor { border: none; margin: 0; padding: 0; min-width: 0; display: grid; gap: 1rem; }
.template-editor:disabled input, .template-editor:disabled textarea, .template-editor:disabled select { opacity: 0.85; }
.template-section { margin: 0; }
.template-section > .field:last-child,
.template-section > .template-section-grid:last-child > .field:last-child { margin-bottom: 0; }
@media (min-width: 761px) {
    .template-section > .template-section-grid:last-child > .field { margin-bottom: 0; }
}
.template-section textarea { resize: vertical; }
.template-section-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1rem; }
.template-section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 0.35rem; }
.template-section-head label, .template-section-head .field-label { margin: 0; }
.template-section h2.discord-section-label { margin: 0 0 0.75rem; }
.template-count { color: var(--vb-text-muted); font-size: 0.8rem; font-variant-numeric: tabular-nums; }
.template-address { margin-top: 0.4rem; width: 100%; }
.template-rows { display: grid; }
.template-field-row, .template-button-row { gap: 0.4rem; }
.template-field-inputs { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 0.4rem; flex: 1; min-width: 0; }
.template-field-inputs input, .template-button-row > input { min-width: 0; width: 100%; }
.template-button-row > input { flex: 0.8; }
.template-button-link { flex: 1.2; min-width: 0; display: grid; }
.template-button-link select { width: 100%; }
.template-inline { white-space: nowrap; min-width: 6.75rem; }
.template-mentions { margin: 0; font-size: 0.88rem; }
/* M20.3: the Mentions section's switch beside its words (the AdminV15 board's state 3). */
.template-mention { display: flex; align-items: flex-start; gap: 0.75rem; }
.template-mention .template-mentions { margin-top: 0.2rem; }
/* M20.3: "Also mention a role" in the automation editor's step 3. */
.automation-role { margin-top: 1rem; }
.template-readonly-note { margin-bottom: 1rem; }
.template-improved-banner { margin-bottom: 1rem; }
.template-card-note { margin: 0.5rem 0 1rem; }
.template-card-tabs { margin-bottom: 1rem; }

.template-preview { position: sticky; top: 1rem; display: grid; gap: 0.9rem; min-width: 0; }
.template-preview-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.template-preview-head .field-label { flex: 1 1 100%; margin: 0; }
.template-preview .field { margin: 0; }
.template-preview select { width: 100%; }
.template-preview-desktop { min-width: 0; }
.template-preview-phone { width: 100%; max-width: 360px; margin: 0 auto; }
.template-preview-phone .discord-surface { padding: 0.75rem; font-size: 0.9rem; }
.template-preview-phone .discord-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.template-characters-line { display: flex; justify-content: space-between; font-size: 0.88rem; }
.template-characters-bar { width: 100%; height: 6px; accent-color: var(--vb-primary-bright); }
.template-characters .hint { margin-top: 0.2rem; }
.template-problem { margin: 0; font-size: 0.88rem; }
.template-problem > span[aria-hidden="true"] { font-weight: 700; }
.alert-error.template-problem > span[aria-hidden="true"] { color: var(--vb-danger); }
.template-problem-where { display: block; font-size: 0.78rem; margin-top: 0.15rem; }

/* The value picker, open under the body (the Designer board). */
.variable-picker { border: 1px solid var(--vb-border-strong); border-radius: var(--vb-radius-sm); margin-top: 0.75rem; padding: 0.6rem; }
.variable-picker-search { width: 100%; }
.variable-picker-body { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: 0.6rem; margin-top: 0.6rem; }
.variable-picker-groups { display: grid; align-content: start; gap: 0.15rem; }
.variable-picker-groups button {
    text-align: left;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.86rem;
    color: var(--vb-text-muted);
    padding: 0.35rem 0.5rem;
    border-radius: var(--vb-radius-sm);
    cursor: pointer;
}
.variable-picker-groups .variable-picker-group-on { background: var(--vb-surface-raised); color: var(--vb-text); font-weight: 600; }
.variable-picker-body > .variable-picker-table:only-child { grid-column: 1 / -1; }
.variable-picker-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.variable-picker-table td { padding: 0.3rem 0.4rem; border-bottom: 1px solid var(--vb-border); vertical-align: top; }
.variable-picker-pick { display: grid; text-align: left; border: none; background: none; font: inherit; color: var(--vb-text); cursor: pointer; padding: 0; }
.variable-picker-pick:hover strong { color: var(--vb-primary-bright); }
.variable-picker-token { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 0.78rem; color: var(--vb-text-muted); }
.variable-picker-value { text-align: right; color: var(--vb-text-muted); white-space: nowrap; }
.variable-picker .hint { margin-bottom: 0; }

@media (max-width: 1100px) {
    .template-designer { grid-template-columns: minmax(0, 1fr); }
    .template-preview { position: static; }
    .template-grid-defaults { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
    .template-grid, .template-grid-defaults { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .template-section-grid { grid-template-columns: minmax(0, 1fr); }
    .template-field-inputs { grid-template-columns: minmax(0, 1fr); }
    .template-field-row, .template-button-row { flex-wrap: wrap; }
    .template-field-inputs, .template-button-row > input, .template-button-link { flex: 1 1 calc(100% - 2rem); }
    .variable-picker-body { grid-template-columns: minmax(0, 1fr); }
    .variable-picker-groups { display: flex; flex-wrap: wrap; }
}
@media (max-width: 480px) {
    .template-grid, .template-grid-defaults { grid-template-columns: minmax(0, 1fr); }
}

/* M19.8 - the automation editor's way into the designer. */
.automation-designer-links { display: flex; flex-wrap: wrap; gap: 0.4rem 1.25rem; margin: 0.4rem 0 0; font-size: 0.88rem; }
/* The designer's test dialog is narrow: its server and channel stack, so the channel list has room. */
.template-test .automation-destination { grid-template-columns: minmax(0, 1fr); }

/* M19.9 - the predictive timezone field (TimeZoneField): the chosen zone with Change, or an input and its
   matches as a raised list below it (DiscordChannelField's shape). */
.timezone-selected { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; min-height: 2.4rem; }
.timezone-results {
    margin-top: 0.5rem;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    padding: 0.35rem;
    max-height: 18rem;
    overflow-y: auto;
}
.timezone-results ul { list-style: none; margin: 0; padding: 0; }
.timezone-results > p { margin: 0.5rem; }
.timezone-option {
    display: block;
    width: 100%;
    padding: 0.5rem 0.6rem;
    border-radius: var(--vb-radius-sm);
    background: transparent;
    border: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.timezone-option:hover, .timezone-option:focus-visible { background: var(--vb-primary-soft); outline: none; }

/* M19.9 - a summary's schedule in the automation editor (the AutomationStates board, frame 4). */
.automation-schedule { display: grid; gap: 0.9rem; margin-top: 0.9rem; }
.automation-schedule > .segmented { justify-self: start; }
.automation-schedule .field { margin: 0; }
.automation-schedule-when { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; }
.automation-schedule-when .field { flex: 0 1 12rem; }
.automation-schedule-when select, .automation-schedule-when input { width: 100%; }
.automation-schedule .alert-info { margin: 0; }
.automation-lead { margin: 0.9rem 0 0; max-width: 22rem; }
.automation-lead select { width: 100%; }
.discord-overview-next { display: grid; gap: 0.75rem; }
.discord-overview-next .muted { font-size: 0.85rem; }

/* M20.1 - the pilot's Discord fact on Pilot details: the date and, for ManagePilots, the one action
   beside it (the AdminV15 board, frame 2). Wraps on a narrow screen rather than pushing the button
   off the row. */
.pilot-discord-fact { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }

/* M20.1 - the Discord Overview's Pilots card: how many pilots have linked, and nothing about WHO
   (decision E11). The figure reads like the tiles above it. */
.discord-pilots-count { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem; }
/* M20.9: the Pilots card's count beside its Commands link, and the airline's Rich Presence switch (AdminV15 frame 1). */
.discord-pilots-row { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.5rem 1rem; }
.discord-pilots-row > a { font-weight: 600; font-size: 0.9rem; }
.discord-presence-switch-row { display: flex; align-items: flex-start; gap: 0.75rem; margin-top: 0.75rem; }
.discord-presence-switch-row .toggle-switch { flex: none; margin-top: 0.15rem; }
.discord-presence-switch-row .muted { font-size: 0.85rem; }
.discord-pilots-count strong { font-size: 1.05rem; }

/* M22.2 - Role sync (the RoleSyncFirstRun, RoleSync, RoleSyncMapping and RoleSyncStates boards). The role pill carries the
   Discord role's own colour as data (--role-colour); everything else is the stylesheet's tokens. */
.role-pill { display: inline-flex; align-items: center; gap: 0.45rem; border: 1px solid var(--vb-border-strong); border-radius: 999px; padding: 0.2rem 0.7rem 0.2rem 0.45rem; font-size: 0.85rem; font-weight: 600; background: var(--vb-surface-raised); white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.role-pill > i { width: 11px; height: 11px; border-radius: 50%; flex: none; background: var(--role-colour, var(--vb-text-muted)); }
.role-pill-off { opacity: 0.55; text-decoration: line-through; }
.role-sync-hero { padding: 1.8rem 2rem; background: radial-gradient(900px 300px at 12% 0%, color-mix(in srgb, #5865f2 22%, transparent), transparent 70%), var(--vb-surface); }
.role-sync-hero h2 { font-size: 1.35rem; margin: 0; }
.role-sync-lede { margin: 0.4rem 0 0; max-width: 64ch; }
.role-sync-promises { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.85rem; margin-top: 1.25rem; }
.role-sync-promise { background: var(--vb-surface-raised); padding: 0.85rem 1rem; }
.role-sync-promise .muted { font-size: 0.85rem; margin-top: 0.25rem; }
.role-sync-start { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin-top: 1.35rem; }
.role-sync-permission-note { font-size: 0.85rem; margin-top: 0.75rem; }
.role-sync-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.9rem; margin-bottom: 1rem; }
.role-sync-toolbar-grow { flex: 1; }
.role-sync-picker { width: 20rem; max-width: 100%; margin: 0; }
.role-sync-picker-name { font-size: 0.95rem; }
.role-sync-other { font-size: 0.85rem; }
.role-sync-switch-row { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
.role-sync-card { padding: 0.9rem 1.25rem; margin-bottom: 1.35rem; }
.role-sync-card-flush { padding: 0; overflow: hidden; }
.role-sync-card-flush > .role-sync-strip { padding: 0.9rem 1.25rem; border-bottom: 1px solid var(--vb-border); }
.role-sync-line { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.role-sync-text { margin: 0.6rem 0 0; font-size: 0.92rem; }
.role-sync-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 0.75rem; }
.role-sync-inline-form { display: inline-flex; margin: 0; }
.role-sync-strip { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.1rem; }
.role-sync-figure { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.88rem; }
.role-sync-when { font-size: 0.85rem; }
.role-sync-check-note { margin: 0.6rem 0 0; }
.role-sync-card-flush > .role-sync-check-note { padding: 0 1.25rem; }
.role-sync-empty { padding: 1.75rem 1.25rem; text-align: center; }
.role-sync-empty .muted { max-width: 56ch; margin: 0.4rem auto 0; }
.role-sync-empty .btn { margin-top: 1rem; }
.role-sync-empty-title { font-size: 1.05rem; font-weight: 600; }
.role-sync-alert { display: flex; gap: 0.7rem; align-items: flex-start; }
.role-sync-alert-detail { font-size: 0.85rem; margin-top: 0.15rem; }
.role-sync-card-flush > .role-sync-not-linked { margin: 0 1.25rem 1.25rem; }
.role-sync-section-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.75rem; margin-bottom: 0.5rem; }
.role-sync-section-head .discord-section-label { flex: 1; margin: 0; }
.role-sync-section-head > .muted { font-size: 0.82rem; }
.role-sync-rows { padding: 0; margin-bottom: 1.35rem; }
.role-sync-row { display: grid; grid-template-columns: minmax(0, 14rem) 1.75rem minmax(0, 1fr) minmax(0, 12rem) 44px 44px; align-items: center; gap: 0.6rem; padding: 0.75rem 0.9rem; border-bottom: 1px solid var(--vb-border); }
.role-sync-rows > .role-sync-row:last-child { border-bottom: none; }
.role-sync-source .muted { font-size: 0.82rem; }
.role-sync-arrow { color: var(--vb-text-muted); font-size: 1rem; }
.role-sync-row-alert { display: flex; gap: 0.7rem; align-items: flex-start; margin: 0 0.9rem 0.9rem; }
.role-sync-none { margin: 0; padding: 0.9rem; }
.role-sync-know { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin-top: 1.6rem; }
.role-sync-know .muted { font-size: 0.85rem; margin: 0.25rem 0 0; }
.role-sync-dialog { max-width: 34rem; max-height: calc(100vh - 3rem); overflow-y: auto; }
.role-sync-dialog h2 { font-size: 1.15rem; margin: 0 0 0.5rem; }
.role-sync-mapping-dialog { max-width: 36rem; }
.role-sync-mapping-dialog .field { margin-top: 0.9rem; }
.role-sync-check { display: flex; gap: 0.55rem; align-items: flex-start; font-size: 0.9rem; margin: 0.4rem 0 0; }
.role-sync-check input { margin-top: 0.2rem; }
.role-sync-manage-existing { display: flex; gap: 0.75rem; align-items: flex-start; background: var(--vb-surface-raised); padding: 0.75rem 0.9rem; margin: 0.9rem 0; }
.role-sync-manage-existing .toggle-switch { margin-top: 0.15rem; }
.role-sync-manage-existing .muted { font-size: 0.85rem; margin-top: 0.2rem; }
.role-sync-change-note { font-size: 0.85rem; }
.role-sync-options { grid-template-columns: minmax(0, 1fr); margin: 0.9rem 0; }
.discord-role-field { position: relative; }
.discord-role-list { margin-top: 0.35rem; padding: 0.35rem; background: var(--vb-bg); max-height: 16rem; overflow-y: auto; }
.discord-role-choice { display: flex; width: 100%; align-items: center; gap: 0.6rem; padding: 0.45rem 0.6rem; border: none; border-radius: var(--vb-radius-sm); background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.discord-role-choice:hover, .discord-role-choice-selected { background: var(--vb-primary-soft); }
.discord-role-choice[aria-disabled="true"] { cursor: not-allowed; }
.discord-role-choice[aria-disabled="true"]:hover { background: transparent; }
.discord-role-choice-unavailable { opacity: 0.6; }
.discord-role-choice-why { flex: 1; min-width: 0; font-size: 0.82rem; }
.discord-role-empty { padding: 0.5rem 0.6rem; font-size: 0.85rem; }
@media (max-width: 760px) {
    .role-sync-promises, .role-sync-know { grid-template-columns: minmax(0, 1fr); }
    .role-sync-row { grid-template-columns: minmax(0, 1fr) 44px 44px; grid-template-areas: "source switch menu" "role role role" "state state state"; }
    .role-sync-row > .role-sync-source { grid-area: source; }
    .role-sync-row > .role-sync-arrow { display: none; }
    .role-sync-row > .role-pill, .role-sync-row > .muted { grid-area: role; justify-self: start; }
    .role-sync-row > span:nth-of-type(3), .role-sync-row > span:empty { grid-area: state; }
    .role-sync-row > .toggle-switch { grid-area: switch; }
    .role-sync-row > .menu, .role-sync-row > .btn { grid-area: menu; }
    .role-sync-row .toggle-switch, .role-sync-row .menu > .btn, .role-sync-row > .btn { min-height: 44px; }
    .role-sync-picker { width: 100%; }
}
/* M22.3 - the last check (RoleSyncStates frame 8): the summary tiles, the pilots needing attention and the sync log. */
.role-sync-last { padding: 0; margin-top: 1.6rem; overflow: hidden; }
.role-sync-last-head { padding: 1rem 1.25rem; border-bottom: 1px solid var(--vb-border); }
.role-sync-last-head h3 { font-size: 1.05rem; margin: 0; }
.role-sync-tiles { margin-top: 0.9rem; }
.role-sync-attention-value { color: var(--vb-warning); }
.role-sync-last-note { font-size: 0.85rem; margin: 0.6rem 0 0; }
.role-sync-last-part { padding: 0.9rem 1.25rem; border-bottom: 1px solid var(--vb-border); }
.role-sync-last-part:last-child { border-bottom: none; }
.role-sync-last-part .choice-chips { margin: 0.25rem 0 0.75rem; }
.role-sync-log td { vertical-align: top; }
@media (max-width: 760px) {
    .role-sync-last-part { overflow-x: auto; }
}

/* M23.1 - Integrations → Developer (the Developer, DeveloperCredentials and DeveloperCredential boards): a side navigation beside
   the page's sections, the credentials list, the create dialog's scope picker, the token shown once and one credential's page. */
.developer-lede { margin: -0.5rem 0 1.25rem; max-width: 70ch; }
.developer-layout { display: grid; grid-template-columns: 12rem minmax(0, 1fr); gap: 1.5rem; align-items: start; }
.developer-nav { position: sticky; top: calc(var(--vb-topbar-height) + 1.5rem); display: flex; flex-direction: column; gap: 0.15rem; }
/* One section rail, two pages: the Developer page and the PIREP page (M24.8). */
.developer-nav a, .pirep-sections a { display: block; padding: 0.45rem 0.7rem; border-radius: var(--vb-radius-sm); color: var(--vb-text-muted); font-weight: 600; font-size: 0.9rem; text-decoration: none; }
.developer-nav a:hover, .pirep-sections a:hover { color: var(--vb-text); background: var(--vb-surface); }
/* The section the reader is in (vaBaseSectionNav sets is-active and aria-current="location"): the brand purple, readable on dark. */
.developer-nav a.is-active, .pirep-sections a.is-active { color: var(--vb-primary-bright); background: color-mix(in srgb, var(--vb-primary) 16%, transparent); }
.developer-nav a:focus-visible, .pirep-sections a:focus-visible { outline: 2px solid var(--vb-primary-bright); outline-offset: 2px; }
.developer-main { min-width: 0; display: flex; flex-direction: column; gap: 1.25rem; }
/* 5rem clears the sticky topbar (about 61px) with room to spare - the nav sits there, a section lands there, and the section
   navigator reads the same scroll-margin-top as its reading line, so a clicked link is always the lit one. */
.developer-section { scroll-margin-top: 5rem; }
.developer-section-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.6rem; }
.developer-section-head h2 { font-size: 1.15rem; margin: 0; }
.developer-list { padding: 0; overflow-x: auto; }
.developer-table td { vertical-align: middle; }
.developer-table .tag-new { margin-left: 0.45rem; }
.tag-new { display: inline-block; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em; color: var(--vb-bg); background: var(--vb-primary-bright); border-radius: 4px; padding: 0.05rem 0.4rem; vertical-align: 1px; }
.developer-warning { margin: 0.9rem 0 0; }
.developer-empty { padding: 1.75rem 1.5rem; }
.developer-empty h2 { font-size: 1.15rem; margin: 0 0 0.4rem; }
.developer-empty .btn { margin-top: 0.9rem; }
.developer-empty-note { font-size: 0.85rem; margin: 0.75rem 0 0; }
.developer-token .token-box { margin-top: 0.9rem; }
.developer-token-facts { margin-top: 1rem; }
.developer-dialog h2 { font-size: 1.15rem; margin: 0 0 0.75rem; }
.developer-scopes { border: none; margin: 0 0 0.9rem; padding: 0; }
.developer-scopes legend { font-weight: 550; font-size: 0.9rem; margin-bottom: 0.4rem; }
.developer-scope { display: flex; gap: 0.6rem; align-items: flex-start; padding: 0.5rem 0.2rem; border-bottom: 1px solid var(--vb-border); }
.developer-scope input { margin-top: 0.25rem; }
.developer-scope > span { display: flex; flex-wrap: wrap; gap: 0.1rem 0.6rem; align-items: baseline; }
.developer-scope .muted { font-size: 0.85rem; }
.developer-options { grid-template-columns: minmax(0, 1fr); margin: 0.9rem 0; }
.developer-expired { margin: 0 0 1rem; }
.developer-credential { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 1.25rem; align-items: start; }
.developer-usage .key-value-list { grid-template-columns: 8rem minmax(0, 1fr); }
.developer-usage-note { font-size: 0.85rem; margin: 0.9rem 0 0; }
@media (max-width: 760px) {
    .developer-layout, .developer-credential { grid-template-columns: minmax(0, 1fr); }
    .developer-nav { position: static; flex-direction: row; flex-wrap: wrap; }
    .developer-nav a { min-height: 44px; display: inline-flex; align-items: center; }
}
/* M23.1 - MobileV2 frame 3: the credentials as cards at phone width. */
.developer-cards { display: none; }
.developer-card { display: flex; flex-direction: column; gap: 0.2rem; padding: 0.85rem 1rem; color: inherit; text-decoration: none; min-height: 44px; }
.developer-card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.developer-card .muted { font-size: 0.85rem; }
@media (max-width: 760px) {
    .developer-list, .developer-head-create { display: none; }
    .developer-cards { display: flex; flex-direction: column; gap: 0.6rem; }
    .developer-card-create { min-height: 44px; }
}
/* M23.3 - the Developer page's guide: the sections, the generated reference and the code samples. */
.developer-section h2 { font-size: 1.15rem; margin: 0 0 0.6rem; }
.developer-section p { font-size: 0.92rem; }
.developer-code { background: var(--vb-bg); border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); padding: 0.75rem 0.9rem; font-family: var(--vb-mono); font-size: 0.82rem; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.6rem 0; }
.developer-reference-note { font-size: 0.85rem; margin: 0.75rem 0 0; }
.developer-route { border-top: 1px solid var(--vb-border); padding: 0.6rem 0; }
.developer-route > summary { cursor: pointer; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; font-size: 0.9rem; }
.developer-route > summary .muted { font-size: 0.85rem; }
.developer-route > p { font-size: 0.85rem; margin: 0.5rem 0; }
.developer-shape { margin-top: 0.6rem; overflow-x: auto; }
.developer-shape-name { font-size: 0.8rem; color: var(--vb-text-muted); margin-bottom: 0.25rem; }
.developer-fields { font-size: 0.85rem; }
.developer-fields td { vertical-align: top; }
/* A clicked link moves focus to its section (tabindex -1), which should not draw a ring round the whole card. */
.developer-section:focus:not(:focus-visible) { outline: none; }
@media (max-width: 760px) {
    /* A phone reads a field as its name and type on one line, and what it means under them. */
    .developer-fields thead { display: none; }
    .developer-fields tr { display: grid; grid-template-columns: auto 1fr; column-gap: 0.75rem; padding: 0.45rem 0; border-top: 1px solid var(--vb-border); }
    .developer-fields td { border: 0; padding: 0; }
    .developer-fields td:nth-child(3) { grid-column: 1 / -1; }
    .developer-fields td:nth-child(2) { color: var(--vb-text-muted); }
}
.developer-reference-table { overflow-x: auto; }
@media (max-width: 760px) {
    .developer-fields td { overflow-wrap: anywhere; }
    /* The resource table the same way: the resource and its scope, then its routes. */
    .developer-reference-table thead { display: none; }
    .developer-reference-table tr { display: grid; grid-template-columns: auto 1fr; column-gap: 0.75rem; padding: 0.55rem 0.75rem; border-top: 1px solid var(--vb-border); }
    .developer-reference-table td { border: 0; padding: 0; overflow-wrap: anywhere; }
    .developer-reference-table td:nth-child(3) { grid-column: 1 / -1; }
}

/* ==========================================================================
   M24 - the Admin Portal UX: tokens, the application shell and the primitives (M24.1)

   Merged from design/admin-shell/proposal.css, the approved boards' stylesheet (product/37
   section 4, decision G13). DARK at M24.1: every class below is new, so nothing that renders
   today changes; the shell (M24.2), the width (M24.3) and the page sweep (M24.6) are what
   put them on screen.

   ⚠️ THREE THINGS FROM proposal.css ARE DELIBERATELY NOT HERE YET (product/37 "Corrections
   during implementation", M24.1):
   - --vb-content-width stays 1080px above. 1440px is M24.3's whole job (G2), and it moves the
     PIREP rail with it.
   - The rules that RESTYLE classes today's pages already use (body, .page-header and its
     title, .filter-bar, the table header and hover row, .row-actions, .pager, .btn-sm and
     .btn-small, .btn .icon, .btn-danger, .badge-marked, a.stat-tile, .card table.data, .modal,
     .badge's nowrap, the phone .btn target). Each is a visible change to every page that uses
     the class, so each lands with the slice that lights that surface.
   - The Concept A block: ShellA was the comparison board, not a design to ship (G1).

   The Discord-named classes a generic one here supersedes keep their own rules until the
   sweep moves their markup; StylesheetClassTests.RetiringAliases names each pair.
   ========================================================================== */

:root {
    /* Spacing scale (doc 13 --space-1..8, 4px base). */
    --vb-space-1: 0.25rem;
    --vb-space-2: 0.5rem;
    --vb-space-3: 0.75rem;
    --vb-space-4: 1rem;
    --vb-space-5: 1.5rem;
    --vb-space-6: 2rem;
    --vb-space-7: 3rem;
    --vb-space-8: 4rem;

    /* Type scale (doc 13 --text-xs..2xl). Body stays 16px. */
    --vb-text-xs: 0.75rem;
    --vb-text-sm: 0.85rem;
    --vb-text-base: 0.95rem;
    --vb-text-md: 1.05rem;
    --vb-text-lg: 1.25rem;
    --vb-text-xl: 1.6rem;
    --vb-text-2xl: 2rem;

    /* Shape and depth additions. */
    --vb-radius-lg: 16px;
    --vb-shadow-2: 0 18px 60px rgba(0, 0, 0, 0.55);

    /* Semantic tints, so the 226 colour literals can go. */
    --vb-success-soft: rgba(62, 194, 125, 0.12);
    --vb-success-line: rgba(62, 194, 125, 0.4);
    --vb-warning-soft: rgba(217, 161, 60, 0.12);
    --vb-warning-line: rgba(217, 161, 60, 0.4);
    --vb-danger-soft: rgba(227, 93, 106, 0.12);
    --vb-danger-line: rgba(227, 93, 106, 0.4);
    /* product/43 S2: a STATE colour, so it does not follow the airline's accent (a red brand would
       draw "Setting up" as an error). The literal is the value it always resolved to. */
    --vb-info: #a883ff;

    /* The shell. (--vb-content-width goes from 1080px to 1440px at M24.3, G2.) */
    --vb-topbar-height: 56px;
    --vb-sidebar-width: 248px;
    --vb-sidebar-rail: 64px;

    /* Breakpoints are documented here and used literally in media queries below
       (custom properties cannot be used inside @media): phone 640, tablet 960, wide 1440. The sidebar starts on
       the rail below 1400 (decided by admin-shell.js with matchMedia, remembered per browser, so a board at
       1280 can still draw the expanded state). */
}

/* --------------------------------------------------------------------------
   Application shell (Concept B)
   -------------------------------------------------------------------------- */

.shell {
    display: grid;
    grid-template-columns: var(--vb-sidebar-width) minmax(0, 1fr);
    min-height: 100vh;
}
.shell[data-sidebar="rail"] { grid-template-columns: var(--vb-sidebar-rail) minmax(0, 1fr); }

.shell-sidebar {
    position: sticky;
    top: 0;
    height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--vb-bg-soft);
    border-right: 1px solid var(--vb-border);
    min-width: 0;
}

/* The airline switcher at the top of the sidebar. A <button> when the account has several
   airlines, a <div> with no chevron when it has one. */
.airline-switcher {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    width: 100%;
    padding: 0.85rem 0.9rem;
    border: none;
    border-bottom: 1px solid var(--vb-border);
    background: transparent;
    color: var(--vb-text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    min-height: 64px;
}
.airline-switcher:hover { background: var(--vb-surface); }
.airline-switcher[aria-expanded="true"] { background: var(--vb-surface); }
div.airline-switcher { cursor: default; }
div.airline-switcher:hover { background: transparent; }
.airline-logo {
    width: 36px;
    height: 36px;
    flex: none;
    border-radius: 9px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.85rem;
    letter-spacing: 0.02em;
    background: var(--vb-primary-soft);
    border: 1px solid color-mix(in srgb, var(--vb-primary) 45%, var(--vb-border));
    color: var(--vb-primary-bright);
    overflow: hidden;
}
/* product/43 S3: a favicon fits the square whole - contain, never cropped. */
.airline-logo-img { width: 100%; height: 100%; object-fit: contain; }
.airline-switcher-text { flex: 1; min-width: 0; }
.airline-switcher-name { font-weight: 650; font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.airline-switcher-state { font-size: 0.78rem; color: var(--vb-text-muted); display: flex; align-items: center; gap: 0.35rem; white-space: nowrap; }
.airline-switcher-state .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.airline-switcher-state.is-active { color: var(--vb-success); }
.airline-switcher-state.is-pending { color: var(--vb-info); }
.airline-switcher-state.is-suspended { color: var(--vb-danger); }
.airline-switcher-chevron { flex: none; color: var(--vb-text-muted); }

/* The open switcher: a popover under the button. */
.switcher-popover {
    position: absolute;
    left: 0.6rem;
    right: 0.6rem;
    top: calc(64px + 0.4rem);
    z-index: 60;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius);
    box-shadow: var(--vb-shadow-2);
    padding: 0.4rem;
}
.switcher-popover-label { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); padding: 0.45rem 0.7rem 0.3rem; }
.switcher-row {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.5rem 0.6rem;
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    text-decoration: none;
}
.switcher-row:hover { background: var(--vb-surface); color: var(--vb-text); }
.switcher-row[aria-current="true"] { background: var(--vb-primary-soft); }
.switcher-row .airline-logo { width: 30px; height: 30px; border-radius: 7px; font-size: 0.75rem; }
.switcher-row-text { flex: 1; min-width: 0; }
.switcher-row-name { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.switcher-row-meta { font-size: 0.75rem; color: var(--vb-text-muted); display: flex; gap: 0.4rem; align-items: center; }
.switcher-row-tick { color: var(--vb-primary-bright); flex: none; }
.switcher-row-warn { color: var(--vb-warning); flex: none; }
.switcher-footer { border-top: 1px solid var(--vb-border); margin-top: 0.35rem; padding-top: 0.35rem; display: grid; }
.switcher-footer a { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.7rem; border-radius: var(--vb-radius-sm); color: var(--vb-text-muted); font-weight: 600; font-size: 0.88rem; }
.switcher-footer a:hover { background: var(--vb-surface); color: var(--vb-text); }
.switcher-footer .icon { width: 16px; height: 16px; }

/* The navigation. */
.sidebar-nav { flex: 1; overflow-y: auto; padding: 0.6rem 0.6rem 1rem; scrollbar-width: thin; }
.nav-group { margin-top: 0.65rem; }
.nav-group:first-child { margin-top: 0.1rem; }
.nav-group-label {
    font-size: 0.7rem;
    font-weight: 650;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--vb-text-muted);
    padding: 0 0.7rem;
    margin: 0 0 0.3rem;
}
/* Density: 26 destinations and six labels must fit a 1080p display without scrolling, so a row is
   30px and a group's label sits close to its rows. The nav scrolls on its own when it must. */
.nav-item {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.3rem 0.7rem;
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text-muted);
    font-size: 0.9rem;
    font-weight: 550;
    text-decoration: none;
    line-height: 1.3;
    min-height: 30px;
}
.nav-item:hover { color: var(--vb-text); background: var(--vb-surface); }
.nav-item[aria-current="page"] { color: var(--vb-text); background: var(--vb-primary-soft); }
.nav-item[aria-current="page"] .icon { color: var(--vb-primary-bright); }
.nav-item .icon { width: 18px; height: 18px; flex: none; }
.nav-item-label { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nav-count {
    font-size: 0.72rem;
    font-weight: 700;
    min-width: 1.35rem;
    height: 1.25rem;
    padding: 0 0.4rem;
    border-radius: 999px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--vb-primary);
    color: var(--vb-on-primary);
    flex: none;
}
.nav-count-muted { background: var(--vb-border-strong); color: var(--vb-text); }
.sidebar-foot { border-top: 1px solid var(--vb-border); padding: 0.5rem 0.6rem; display: grid; gap: 0.1rem; }
.sidebar-foot .nav-item { font-size: 0.85rem; }

/* The icon rail. Labels hide; the switcher keeps its logo; a title attribute carries the name. */
.shell[data-sidebar="rail"] .nav-item-label,
.shell[data-sidebar="rail"] .nav-group-label,
.shell[data-sidebar="rail"] .airline-switcher-text,
.shell[data-sidebar="rail"] .airline-switcher-chevron,
.shell[data-sidebar="rail"] .nav-count { display: none; }
.shell[data-sidebar="rail"] .nav-item { justify-content: center; padding: 0.5rem; position: relative; }
.shell[data-sidebar="rail"] .nav-group { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--vb-border); }
.shell[data-sidebar="rail"] .nav-group:first-child { border-top: none; }
.shell[data-sidebar="rail"] .airline-switcher { justify-content: center; padding: 0.85rem 0; }
.shell[data-sidebar="rail"] .nav-item[data-count]::after {
    content: "";
    position: absolute;
    top: 6px;
    right: 8px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--vb-primary-bright);
}

/* The main column. */
.shell-main { min-width: 0; display: flex; flex-direction: column; }
.shell-topbar {
    position: sticky;
    top: 0;
    z-index: 50;
    height: var(--vb-topbar-height);
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0 1.5rem;
    background: color-mix(in srgb, var(--vb-bg) 82%, transparent);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--vb-border);
}
.topbar-menu { display: none; }
.topbar-collapse { margin-left: -0.5rem; }   /* desktop: collapse or expand the sidebar */
.topbar-spacer { flex: 1; }          /* reserved: a future global search / command palette (G10) */
.topbar-icon-btn {
    width: 34px;
    height: 34px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    border-radius: var(--vb-radius-sm);
    background: transparent;
    color: var(--vb-text-muted);
    cursor: pointer;
}
.topbar-icon-btn:hover { color: var(--vb-text); background: var(--vb-surface); }
.topbar-icon-btn .icon { width: 18px; height: 18px; }

/* Breadcrumbs: every crumb but the last is a link; the last names the page. */
.breadcrumbs { min-width: 0; }
.breadcrumbs ol { list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: 0.35rem; font-size: 0.9rem; white-space: nowrap; overflow: hidden; }
.breadcrumbs li { display: flex; align-items: center; gap: 0.35rem; min-width: 0; }
.breadcrumbs a { color: var(--vb-text-muted); font-weight: 550; }
.breadcrumbs a:hover { color: var(--vb-text); }
.breadcrumbs li[aria-current="page"] { color: var(--vb-text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.breadcrumbs .crumb-sep { color: var(--vb-border-strong); flex: none; }
.breadcrumbs .crumb-sep .icon { width: 14px; height: 14px; }

/* The user menu. */
.user-menu { position: relative; }
.user-menu-btn {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.25rem 0.5rem 0.25rem 0.25rem;
    border: 1px solid transparent;
    border-radius: 999px;
    background: transparent;
    color: var(--vb-text);
    font: inherit;
    font-size: 0.9rem;
    font-weight: 550;
    cursor: pointer;
}
.user-menu-btn:hover, .user-menu-btn[aria-expanded="true"] { background: var(--vb-surface); border-color: var(--vb-border); }
.user-menu-btn .airline-switcher-chevron .icon { width: 16px; height: 16px; }
.avatar-initials {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    font-weight: 700;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    color: var(--vb-text);
    flex: none;
}
.user-menu-list {
    position: absolute;
    right: 0;
    top: calc(100% + 0.4rem);
    z-index: 60;
    min-width: 15rem;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius);
    box-shadow: var(--vb-shadow-2);
    padding: 0.4rem;
}
.user-menu-head { padding: 0.55rem 0.7rem 0.6rem; border-bottom: 1px solid var(--vb-border); margin-bottom: 0.35rem; }
.user-menu-head strong { display: block; font-size: 0.92rem; }
.user-menu-head span { display: block; font-size: 0.78rem; color: var(--vb-text-muted); overflow: hidden; text-overflow: ellipsis; }
.user-menu-list a, .user-menu-list button {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.5rem 0.7rem;
    border: none;
    border-radius: var(--vb-radius-sm);
    background: transparent;
    color: var(--vb-text);
    font: inherit;
    font-size: 0.9rem;
    text-align: left;
    cursor: pointer;
    text-decoration: none;
}
.user-menu-list a:hover, .user-menu-list button:hover { background: var(--vb-surface); color: var(--vb-text); }
.user-menu-list .icon { width: 16px; height: 16px; color: var(--vb-text-muted); }
.user-menu-list .menu-sep { border-top: 1px solid var(--vb-border); margin: 0.35rem 0; }
.user-menu-list .menu-ext { margin-left: auto; color: var(--vb-text-muted); }
.user-menu-list .menu-ext .icon { width: 13px; height: 13px; }

/* Billing state, in the shell rather than on each page. Same wording as BillingPresentation. */
.shell-banner {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.55rem 1.5rem;
    font-size: 0.88rem;
    border-bottom: 1px solid var(--vb-warning-line);
    background: var(--vb-warning-soft);
}
.shell-banner .icon { width: 18px; height: 18px; flex: none; color: var(--vb-warning); }
.shell-banner-danger { border-bottom-color: var(--vb-danger-line); background: var(--vb-danger-soft); }
.shell-banner-danger .icon { color: var(--vb-danger); }
/* product/82: a free period with time to spare is news, not a warning. */
.shell-banner-calm { border-bottom-color: var(--vb-border); background: var(--vb-surface); }
.shell-banner-calm .icon { color: var(--vb-info); }
.shell-banner .btn { margin-left: auto; flex: none; white-space: nowrap; }

/* Content column: fluid up to the content width, left-anchored beside the sidebar. */
.shell-content { flex: 1; min-width: 0; }
.shell-content > .page,
.shell-content .page {
    max-width: var(--vb-content-width);
    margin: 0 auto;
    padding: 1.6rem 2rem 3rem;
}
.page-narrow { max-width: 64rem; }

/* Phone and tablet: the sidebar becomes a drawer behind the top bar's menu button. */
@media (max-width: 960px) {
    .shell { grid-template-columns: minmax(0, 1fr); }
    .shell-sidebar {
        position: fixed;
        inset: 0 auto 0 0;
        width: min(300px, 86vw);
        z-index: 200;
        transform: translateX(-100%);
        transition: transform 0.18s ease;
        box-shadow: var(--vb-shadow-2);
    }
    .shell[data-drawer="open"] .shell-sidebar { transform: none; }
    .shell-scrim { position: fixed; inset: 0; z-index: 150; background: rgba(6, 7, 8, 0.6); }
    .topbar-menu { display: inline-flex; }
    .topbar-collapse { display: none; }
    .shell-content .page { padding: 1.1rem 1rem 2.5rem; }
    .breadcrumbs li:not(:last-child):not(:nth-last-child(2)) { display: none; }
}

/* --------------------------------------------------------------------------
   Page anatomy
   -------------------------------------------------------------------------- */

.page-header-actions .btn-icon { padding: 0.5rem 0.6rem; }
.page-count { color: var(--vb-text-muted); font-weight: 500; font-size: 1.1rem; margin-left: 0.15rem; }

/* Section tabs: the Integrations sub-nav promoted to the standard (M24.1). The Discord and Webhooks
   tabs moved onto it at M24.6; `.filter-tabs` goes as its last pages move. */
.section-tabs {
    display: flex;
    gap: 0.25rem;
    box-shadow: inset 0 -1px 0 var(--vb-border);
    margin: 0 0 1.25rem;
    overflow-x: auto;
    scrollbar-width: none;
}
.section-tabs a, .section-tabs button {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.55rem 0.9rem;
    font: inherit;
    font-weight: 600;
    font-size: 0.92rem;
    color: var(--vb-text-muted);
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    white-space: nowrap;
    cursor: pointer;
    text-decoration: none;
}
.section-tabs a:hover, .section-tabs button:hover { color: var(--vb-text); }
.section-tabs [aria-current="page"], .section-tabs [aria-selected="true"] { color: var(--vb-text); border-bottom-color: var(--vb-primary-bright); }
.tab-count { font-size: 0.72rem; font-weight: 700; padding: 0.05rem 0.45rem; border-radius: 999px; background: var(--vb-surface-raised); border: 1px solid var(--vb-border-strong); color: var(--vb-text-muted); }
[aria-current="page"] .tab-count, [aria-selected="true"] .tab-count { color: var(--vb-text); }

/* Filter bar: one predictive search, chips and selects, the result count at the end. No Search button. */
.filter-search { position: relative; flex: 1 1 260px; min-width: 200px; }
.filter-search .icon { position: absolute; left: 0.7rem; top: 50%; width: 16px; height: 16px; transform: translateY(-50%); color: var(--vb-text-muted); pointer-events: none; }
.filter-search input {
    width: 100%;
    padding: 0.5rem 0.8rem 0.5rem 2.2rem;
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    font: inherit;
    font-size: 0.92rem;
}
.filter-select {
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.45rem 2rem 0.45rem 0.7rem;
    font: inherit;
    font-size: 0.88rem;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23979aa1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.7rem center;
}
.filter-count { margin-left: auto; font-size: 0.85rem; color: var(--vb-text-muted); white-space: nowrap; }
.filter-clear { font-size: 0.85rem; }

/* Data table: one header style (small caps), sticky under the top bar, one row-action style. */
table.data.data-sticky thead th { position: sticky; top: var(--vb-topbar-height); z-index: 5; }
/* A sticky header sticks to the PAGE, so its wrapper must not be a scroll container: a sticky
   table is one that fits its column (the width change makes that true for the wide lists). */
.table-wrap:has(.data-sticky) { overflow: visible; }
/* ...and neither may the table: app.css rounds the table with overflow: hidden, which makes the
   table its own scroll container and pins the header 56px low. The corners move to the cells. */
table.data.data-sticky { overflow: visible; }
table.data.data-sticky thead th:first-child { border-top-left-radius: var(--vb-radius); }
table.data.data-sticky thead th:last-child { border-top-right-radius: var(--vb-radius); }
table.data.data-sticky tbody tr:last-child td:first-child { border-bottom-left-radius: var(--vb-radius); }
table.data.data-sticky tbody tr:last-child td:last-child { border-bottom-right-radius: var(--vb-radius); }
table.data td .num { font-family: var(--vb-mono); font-variant-numeric: tabular-nums; }
table.data th.is-sorted { color: var(--vb-text); }
table.data th .sort-mark { font-size: 0.7em; margin-left: 0.3rem; }
.row-primary { font-weight: 600; }
.row-sub { display: block; font-size: 0.8rem; color: var(--vb-text-muted); }
/* A status pill with its detail beneath (the streak milestone's "since" date): a little air between them. */
.badge + .row-sub { margin-top: 0.3rem; }
.cell-tight { width: 1%; white-space: nowrap; }

/* Pager: the same component everywhere, with the range in words. */
.pager-range { font-size: 0.85rem; color: var(--vb-text-muted); }
.pager-buttons { display: flex; gap: 0.4rem; }
.pager-buttons .btn { padding: 0.35rem 0.8rem; font-size: 0.85rem; }

/* Buttons: icon buttons and the modal's final destructive press. */
.btn-icon { padding: 0.5rem 0.6rem; }
.btn-icon .icon { width: 18px; height: 18px; }
.btn-danger-solid { background: var(--vb-danger); border-color: var(--vb-danger); color: #fff; font-weight: 600; }
.btn-danger-solid:hover { background: #ee6f7b; border-color: #ee6f7b; color: #fff; }

/* Status pill: the one badge family, with an optional mark. Lifecycle Inactive is muted. */
.badge-lg { font-size: 0.85rem; padding: 0.25rem 0.85rem; }

/* Stat tile: one tile, clickable when it links, with an optional delta and sparkline. */
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.9rem; margin-bottom: 1.25rem; }
.kpi-grid .stat-tile { padding: 0.9rem 1.1rem; }
.stat-tile-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.stat-tile-head .icon { width: 16px; height: 16px; color: var(--vb-text-muted); }
.stat-live .stat-value { color: var(--vb-success); }
.stat-delta { font-size: 0.78rem; font-weight: 600; display: inline-flex; align-items: center; gap: 0.2rem; }
.stat-delta-up { color: var(--vb-success); }
.stat-delta-down { color: var(--vb-danger); }
.stat-delta-flat { color: var(--vb-text-muted); }

/* VA Pulse: a compact row of figures with one sparkline each. */
.pulse-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; }
.pulse-item { padding: 0.35rem 1rem; border-left: 1px solid var(--vb-border); }
.pulse-item:first-child { border-left: none; padding-left: 0; }
.pulse-label { font-size: 0.78rem; font-weight: 600; color: var(--vb-text-muted); letter-spacing: 0.02em; }
.pulse-value { display: flex; align-items: baseline; gap: 0.5rem; margin-top: 0.15rem; }
.pulse-value strong { font-size: 1.35rem; font-weight: 650; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.pulse-spark { display: block; width: 100%; height: 28px; margin-top: 0.35rem; }
.pulse-spark path { fill: none; stroke: var(--vb-primary-bright); stroke-width: 1.6; stroke-linejoin: round; }
.pulse-spark .spark-fill { fill: var(--vb-primary-soft); stroke: none; }
.pulse-foot { font-size: 0.78rem; color: var(--vb-text-muted); margin-top: 0.75rem; }

/* Needs your attention: a prioritised list, 5 to 7 rows, one action each. */
.attention-list { list-style: none; margin: 0; padding: 0; }
.attention-item { display: flex; align-items: center; gap: 0.8rem; padding: 0.65rem 0; border-top: 1px solid var(--vb-border); }
.attention-item:first-child { border-top: none; padding-top: 0.2rem; }
.attention-mark {
    width: 28px;
    height: 28px;
    flex: none;
    border-radius: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--vb-warning-soft);
    color: var(--vb-warning);
}
.attention-mark .icon { width: 15px; height: 15px; }
.attention-mark-danger { background: var(--vb-danger-soft); color: var(--vb-danger); }
.attention-mark-info { background: var(--vb-primary-soft); color: var(--vb-primary-bright); }
.attention-text { flex: 1; min-width: 0; font-size: 0.92rem; }
.attention-text .row-sub { margin-top: 0.05rem; }
.attention-item .btn { flex: none; }
.attention-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 0.6rem; font-size: 0.85rem; color: var(--vb-text-muted); }
.attention-clear { display: flex; align-items: center; gap: 1rem; padding: 0.6rem 0 0.2rem; }
.attention-clear .empty-state-icon { margin: 0; width: 40px; height: 40px; background: var(--vb-success-soft); border-color: var(--vb-success-line); color: var(--vb-success); }
.attention-clear strong { display: block; font-size: 0.95rem; }
.attention-clear .muted { font-size: 0.88rem; }

/* Card header: title, optional count, an action on the right. */
.card-head { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.75rem; }
.card-head h2, .card-head h3 { margin: 0; font-size: 1rem; flex: 1; min-width: 0; }
.card-head .btn-link, .card-head a { font-size: 0.85rem; font-weight: 600; }
.card-tight { padding: 1rem 1.2rem; }
.card-list { list-style: none; margin: 0; padding: 0; }
.card-list li { display: flex; align-items: center; gap: 0.7rem; padding: 0.5rem 0; border-top: 1px solid var(--vb-border); font-size: 0.9rem; }
.card-list li:first-child { border-top: none; padding-top: 0; }
.card-list .grow { flex: 1; min-width: 0; }
.card-list .when { color: var(--vb-text-muted); font-size: 0.8rem; white-space: nowrap; }
.card-list .mono { font-size: 0.85rem; }

/* Dashboard layout. */
.dash-grid { display: grid; grid-template-columns: minmax(0, 1fr) 20rem; gap: 1.25rem; align-items: start; }
.dash-main, .dash-side { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem; min-width: 0; }
/* A card in a stacked column never widens its column: tables inside it scroll or wrap. */
.dash-main > *, .dash-side > *, .detail-main > *, .detail-side > *, .settings-main > * { min-width: 0; }
.dash-grid .card { margin: 0; }
.dash-grid .card:hover { transform: none; }
@media (max-width: 1100px) { .dash-grid { grid-template-columns: minmax(0, 1fr); } }

/* Getting started (PendingSetup). */
.setup-card { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1.5rem; align-items: start; }
.setup-ring { width: 84px; height: 84px; position: relative; }
.setup-ring svg { width: 84px; height: 84px; transform: rotate(-90deg); }
.setup-ring circle { fill: none; stroke-width: 7; }
.setup-ring .ring-track { stroke: var(--vb-border); }
.setup-ring .ring-fill { stroke: var(--vb-primary-bright); stroke-linecap: round; }
.setup-ring-label { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.95rem; }
.setup-list { list-style: none; margin: 0.75rem 0 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.25rem 1.5rem; }
.setup-list li { display: flex; align-items: center; gap: 0.6rem; padding: 0.35rem 0; font-size: 0.9rem; }
.setup-list .icon { width: 16px; height: 16px; flex: none; }
.setup-list .done { color: var(--vb-text-muted); }
.setup-list .done .icon { color: var(--vb-success); }
.setup-list .todo .icon { color: var(--vb-border-strong); }
.setup-list a { margin-left: auto; font-size: 0.85rem; font-weight: 600; }
.setup-actions { display: flex; align-items: center; gap: 0.8rem; margin-top: 1rem; }
.setup-row { display: flex; align-items: center; gap: 0.8rem; }
.setup-row .grow { flex: 1; }
.setup-bar { height: 6px; border-radius: 999px; background: var(--vb-border); overflow: hidden; width: 160px; }
.setup-bar > span { display: block; height: 100%; background: var(--vb-primary-bright); }

/* Switch row: a boolean setting as a switch with its name and one sentence. */
.switch-row { display: flex; align-items: flex-start; gap: 0.9rem; padding: 0.8rem 0; border-top: 1px solid var(--vb-border); }
.switch-row:first-of-type { border-top: none; padding-top: 0; }
.switch-row .toggle-switch { margin-top: 0.15rem; }
.switch-row-text { flex: 1; min-width: 0; }
.switch-row-text strong { display: block; font-size: 0.92rem; font-weight: 600; }
.switch-row-text .muted { font-size: 0.85rem; }
.switch-row-child { margin-left: 3.4rem; border-top: none; padding-top: 0; }

/* Forms: the standard is header Cancel + Save; fields keep their width. */
.form-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1.25rem; }
.form-actions-inline { display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 1.25rem; }
.form-actions-inline .btn { width: auto; }
.field-help { font-size: 0.8rem; color: var(--vb-text-muted); margin-top: 0.3rem; }
.field-error-text { font-size: 0.8rem; color: var(--vb-danger); margin-top: 0.3rem; display: flex; align-items: center; gap: 0.3rem; }
.field-error-text .icon { width: 14px; height: 14px; }
.field input.is-invalid { border-color: var(--vb-danger); box-shadow: 0 0 0 3px var(--vb-danger-soft); }

/* Settings category page: a rail of categories beside the page. */
.settings-layout { display: grid; grid-template-columns: 14rem minmax(0, 1fr); gap: 1.75rem; align-items: start; }
.settings-rail { position: sticky; top: calc(var(--vb-topbar-height) + 1rem); display: grid; gap: 0.1rem; }
.settings-rail a { display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 0.7rem; border-radius: var(--vb-radius-sm); color: var(--vb-text-muted); font-weight: 600; font-size: 0.9rem; }
.settings-rail a:hover { color: var(--vb-text); background: var(--vb-surface); }
.settings-rail a[aria-current="page"] { color: var(--vb-text); background: var(--vb-primary-soft); }
.settings-rail .icon { width: 16px; height: 16px; }
.settings-main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem; min-width: 0; }
.settings-main .card { margin: 0; }
.settings-main .card:hover { transform: none; }
.settings-section-head { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 0.9rem; }
.settings-section-head h2 { font-size: 1.05rem; margin: 0; }
.settings-section-head p { margin: 0.15rem 0 0; font-size: 0.88rem; color: var(--vb-text-muted); }
.settings-section-head .badge { margin-left: auto; }
.settings-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
.settings-card { display: flex; flex-direction: column; gap: 0.35rem; color: inherit; text-decoration: none; padding: 1.1rem 1.2rem; }
.settings-card:hover { border-color: var(--vb-primary); color: inherit; }
.settings-card-head { display: flex; align-items: center; gap: 0.6rem; font-weight: 650; }
.settings-card-head .integration-card-icon { width: 32px; height: 32px; border-radius: 8px; }
.settings-card p { margin: 0; font-size: 0.85rem; color: var(--vb-text-muted); }
.settings-card-facts { font-size: 0.8rem; color: var(--vb-text-muted); margin-top: auto; padding-top: 0.5rem; }
.settings-gate { font-size: 0.78rem; color: var(--vb-text-muted); display: inline-flex; align-items: center; gap: 0.3rem; }
.settings-gate .icon { width: 13px; height: 13px; }

/* Detail page: two columns and a sticky side. */
.detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) 21rem; gap: 1.25rem; align-items: start; }
.detail-main, .detail-side { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem; min-width: 0; }
.detail-grid .card { margin: 0; }
.detail-grid .card:hover { transform: none; }
/* A card holding a modal or a floating ⋯ menu never lifts (2026-09-29): a transformed element becomes the
   containing block of every position: fixed descendant, so the lift dragged the File library preview
   (opened from the Library image field) to the card's own box, off-screen and under the top bar, and
   would misplace a row menu floated inside a card the same way. */
.card:has(.modal-backdrop, .menu-list-floating) { transform: none; }
.facts { display: grid; grid-template-columns: 9rem minmax(0, 1fr); gap: 0.45rem 1rem; margin: 0; font-size: 0.9rem; }
.facts dt { color: var(--vb-text-muted); }
.facts dd { margin: 0; }

/* Modal: one component; the destructive variant is named. */
.modal-danger h2 { display: flex; align-items: center; gap: 0.5rem; }
.modal-danger h2 .icon { width: 18px; height: 18px; color: var(--vb-danger); }
.modal-inline { position: relative; inset: auto; z-index: auto; padding: 0; background: transparent; display: block; }
.modal-inline .modal { margin: 0; }

/* Drawer: the delivery drawer generalised (`.delivery-drawer` stays as an alias). */
.drawer { width: min(36rem, 100%); max-height: 100vh; overflow-y: auto; margin: 0; border-radius: var(--vb-radius) 0 0 var(--vb-radius); }
.drawer-head { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.75rem; }
.drawer-head h3 { flex: 1; margin: 0; }

/* Menu: the ⋯ list - the shared Menu component (M24.8). */
.menu { position: relative; display: inline-block; }
.menu-list {
    position: absolute;
    right: 0;
    top: calc(100% + 0.3rem);
    z-index: 40;
    min-width: 13rem;
    padding: 0.35rem;
    margin: 0;
    display: grid;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius);
    box-shadow: var(--vb-shadow);
}
/* display: grid above beats the browser's own [hidden] rule, so a closed menu says so itself
   (HiddenAttributeDisplayTests). */
.menu-list[hidden] { display: none; }
.menu-list a, .menu-list button {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    text-align: left;
    background: transparent;
    border: none;
    color: var(--vb-text);
    padding: 0.45rem 0.65rem;
    border-radius: var(--vb-radius-sm);
    font: inherit;
    font-size: 0.9rem;
    cursor: pointer;
    text-decoration: none;
}
.menu-list a:hover, .menu-list button:hover { background: var(--vb-surface); color: var(--vb-text); }
.menu-list .icon { width: 15px; height: 15px; color: var(--vb-text-muted); }
.menu-list .menu-danger { color: var(--vb-danger); }
.menu-list .menu-danger .icon { color: var(--vb-danger); }
.menu-list .menu-sep { border-top: 1px solid var(--vb-border); margin: 0.3rem 0; }
/* A disabled item reads as unavailable - greyed, no hover, the not-allowed cursor - so an action that
   cannot run now (a flight in the air cannot be cancelled) never looks like one that can. A
   destructive item loses its red with it. */
.menu-list button:disabled,
.menu-list button:disabled:hover { background: transparent; color: var(--vb-text-muted); opacity: 0.6; cursor: not-allowed; }
.menu-list button:disabled .icon { color: var(--vb-text-muted); }

/* Entity lifecycle (product/69): the additions to app.css, written to be merged as they stand.

   The dependency list inside the shared LifecycleDialog: one row per thing that references the
   record, each with its count, what it is, one sentence on what the action means for it, and an
   optional link to the list where it can be resolved. A row either BLOCKS the action (a red mark)
   or is INFORMATION (a muted mark); the words beside the mark say which, so the colour is never
   the only signal (product/37 section 0 rule 6). The list sits between the dialog's sentence and
   its actions, inside the 30rem modal, and never scrolls sideways. */
.lifecycle-deps {
    list-style: none;
    margin: 0.9rem 0 0;
    padding: 0;
    display: grid;
    gap: 0.35rem;
}
.lifecycle-dep {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.7rem;
    padding: 0.5rem 0.65rem;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    background: var(--vb-surface);
}
.lifecycle-dep-count {
    font-family: var(--vb-mono);
    font-size: 0.95rem;
    font-weight: 600;
    min-width: 2.4rem;
    text-align: right;
}
.lifecycle-dep-text { display: grid; gap: 0.05rem; min-width: 0; }
.lifecycle-dep-text .muted { font-size: 0.82rem; }
.lifecycle-dep-blocks .lifecycle-dep-count { color: var(--vb-danger); }
.lifecycle-dep-info .lifecycle-dep-count { color: var(--vb-text-muted); }
/* The resolution sentence under the list ("Move or retire these first."). */
.lifecycle-resolve { margin: 0.9rem 0 0; }

/* The banner on an inactive record's detail page: the same muted notice the page already uses
   for warnings, with the one action that undoes the state beside the words. */
.lifecycle-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}
.lifecycle-banner p { margin: 0; }
/* A section a dependency row links to (the rank page's requirements): it clears the sticky top bar
   when the address's fragment brings it into view. */
.lifecycle-anchor { scroll-margin-top: calc(var(--vb-topbar-height) + 1rem); }
@media (max-width: 640px) {
    .lifecycle-dep { grid-template-columns: auto minmax(0, 1fr); }
    .lifecycle-dep > .btn { grid-column: 2; justify-self: start; }
}

/* Lookup results: one container for every predictive field (`.suggest-popout`, `.timezone-results`,
   `.discord-channel-results` and `.route-suggestions` become aliases). */
.lookup { position: relative; }
.lookup-results {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + 0.3rem);
    z-index: 40;
    max-height: 18rem;
    overflow-y: auto;
    padding: 0.3rem;
    margin: 0;
    list-style: none;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    box-shadow: var(--vb-shadow);
}
.lookup-results li { display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 0.6rem; border-radius: 6px; font-size: 0.9rem; cursor: pointer; }
.lookup-results li:hover, .lookup-results li[aria-selected="true"] { background: var(--vb-surface); }
.lookup-results .code { font-family: var(--vb-mono); font-weight: 700; text-transform: uppercase; min-width: 3.5rem; }
.lookup-results .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lookup-results .muted { font-size: 0.8rem; }
.lookup-status { padding: 0.55rem 0.7rem; font-size: 0.85rem; color: var(--vb-text-muted); }
.lookup-inline { position: static; box-shadow: none; }

/* Page problem: airline not available, permission required, not found. One component. */
.page-problem { max-width: 28rem; margin: 3.5rem auto 0; text-align: center; }
.page-problem .empty-state-icon { width: 52px; height: 52px; margin-bottom: 1.1rem; }
.page-problem h1 { font-size: 1.35rem; margin: 0 0 0.4rem; }
.page-problem p { color: var(--vb-text-muted); margin: 0 0 1.25rem; }
.page-problem-actions { display: flex; justify-content: center; gap: 0.6rem; }

/* Skeleton: shimmer blocks in place of "Loading…". */
.skeleton {
    display: block;
    border-radius: 6px;
    /* The shimmer's light band, toned down (Myles, 2026-09-28): 60% of the border colour into the raised surface. */
    background: linear-gradient(90deg, var(--vb-surface-raised) 25%, color-mix(in srgb, var(--vb-border) 60%, var(--vb-surface-raised)) 50%, var(--vb-surface-raised) 75%);
    background-size: 200% 100%;
    animation: vb-shimmer 1.4s ease-in-out infinite;
    height: 0.9rem;
}
.skeleton-text { width: 60%; }
.skeleton-title { height: 1.3rem; width: 40%; margin-bottom: 0.7rem; }
.skeleton-row { display: flex; gap: 1rem; padding: 0.7rem 0; border-top: 1px solid var(--vb-border); }
.skeleton-row .skeleton { flex: 1; }
.skeleton-tile { height: 4.5rem; border-radius: var(--vb-radius); }
@keyframes vb-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }

/* Toast: saved and copied confirmations, bottom right, one at a time. */
.toast-stack { position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: 300; display: grid; gap: 0.5rem; }
.toast {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 16rem;
    max-width: 24rem;
    padding: 0.7rem 0.9rem;
    background: var(--vb-surface-raised);
    border: 1px solid var(--vb-border-strong);
    border-left: 3px solid var(--vb-success);
    border-radius: var(--vb-radius-sm);
    box-shadow: var(--vb-shadow-2);
    font-size: 0.9rem;
}
.toast .icon { width: 18px; height: 18px; color: var(--vb-success); flex: none; }
.toast-text { flex: 1; }
.toast-text .muted { display: block; font-size: 0.8rem; }
.toast .btn-link { padding: 0; font-size: 0.85rem; }
.toast-close { margin-left: 0.25rem; }
.toast-close .icon { width: 14px; height: 14px; color: var(--vb-text-muted); }
.toast-error { border-left-color: var(--vb-danger); }
.toast-error .icon { color: var(--vb-danger); }
.toast-inline { position: static; }

/* Live-flight row used by the dashboard and the list boards. */
.flight-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.75rem; align-items: center; }
.flight-row .code { font-family: var(--vb-mono); font-weight: 700; }
.flight-phase { font-size: 0.78rem; color: var(--vb-text-muted); }
.progress-line { height: 4px; border-radius: 999px; background: var(--vb-border); overflow: hidden; margin-top: 0.35rem; }
.progress-line > span { display: block; height: 100%; background: var(--vb-success); }

/* Health strip for integrations on the dashboard. */
.health-strip { display: grid; gap: 0.45rem; }
.health-strip-row { display: flex; align-items: center; gap: 0.6rem; font-size: 0.88rem; }
.health-strip-row .grow { flex: 1; min-width: 0; }

/* Findings while drawing the boards. (The .badge nowrap, the .page-header column and the phone
   .btn target are held with the other restyles - see the head of this block.) */
.settings-section-head > .badge, .settings-section-head > .settings-gate, .settings-section-head > .btn { flex: none; margin-left: auto; }
.switch-row .field { margin-bottom: 0; }
@media (max-width: 640px) {
    /* Phone: every control keeps a 44px target; attention buttons drop under their text; the pulse is 2 x 2. */
    .topbar-icon-btn, .user-menu-btn, .nav-item, .switcher-row { min-height: 44px; }
    .topbar-icon-btn { min-width: 44px; }
    .attention-item { flex-wrap: wrap; }
    .attention-text { flex: 1 1 calc(100% - 28px - 0.8rem); }
    .attention-item .btn { margin-left: calc(28px + 0.8rem); }
    .pulse-row { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 0.75rem; }
    .pulse-item:nth-child(3) { border-left: none; padding-left: 0; }
    .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .setup-list { grid-template-columns: minmax(0, 1fr); }
}

/* The primitives' own rules (M24.1): what the components need that the boards drew with inline
   board-only styles. */
.skeleton-block { display: block; }
.skeleton-block > .kpi-grid { margin-bottom: 1rem; }
.skeleton-short { flex: 0 0 4rem; }
.stat-value-suffix { color: var(--vb-text-muted); font-size: 1rem; font-weight: 400; }
/* admin-shell.js hides a toast with the hidden attribute; .toast's own display would otherwise win. */
.toast[hidden] { display: none; }

/* The shell's own rules (M24.2): what the real shell needs that the boards drew with board-only
   placement or scaffolding (board.css's phone sheet). */
.switcher-footer-ext { margin-left: auto; color: var(--vb-text-muted); display: inline-flex; }
.switcher-footer-ext .icon { width: 13px; height: 13px; }
.user-menu-list form { margin: 0; }
/* On the rail the sidebar is 64px wide, so the switcher's list opens beside it, anchored to the rail's
   edge (the AirlineSwitcher board, frame 4). */
.shell[data-sidebar="rail"] .switcher-popover { left: calc(100% + 0.4rem); right: auto; top: 0.4rem; width: 19rem; }
@media (max-width: 960px) {
    /* The switcher as a sheet from the bottom, where a thumb is (the Mobile board, frame c). */
    .switcher-popover,
    .shell[data-sidebar="rail"] .switcher-popover {
        position: fixed;
        left: 0;
        right: 0;
        top: auto;
        bottom: 0;
        width: auto;
        z-index: 250;
        border-radius: var(--vb-radius-lg) var(--vb-radius-lg) 0 0;
        padding: 0.5rem 0.5rem 1rem;
        max-height: 80vh;
        overflow-y: auto;
    }
    .switcher-popover::before {
        content: "";
        display: block;
        width: 40px;
        height: 4px;
        border-radius: 999px;
        background: var(--vb-border-strong);
        margin: 0.25rem auto 0.6rem;
    }
    .switcher-popover .switcher-row { padding: 0.7rem 0.6rem; }
    /* The drawer is the sidebar unchanged: its labels show even if the rail was chosen on a desktop. */
    .shell[data-sidebar="rail"] .shell-sidebar .nav-item-label,
    .shell[data-sidebar="rail"] .shell-sidebar .nav-group-label,
    .shell[data-sidebar="rail"] .shell-sidebar .airline-switcher-text,
    .shell[data-sidebar="rail"] .shell-sidebar .airline-switcher-chevron { display: revert; }
    .shell[data-sidebar="rail"] .nav-count { display: inline-flex; }
    .shell[data-sidebar="rail"] .nav-item { justify-content: flex-start; padding: 0.3rem 0.7rem; }
    .shell[data-sidebar="rail"] .nav-item[data-count]::after { content: none; }
    .shell[data-sidebar="rail"] .nav-group { border-top: none; padding-top: 0; }
    .shell[data-sidebar="rail"] .airline-switcher { justify-content: flex-start; padding: 0.85rem 0.9rem; }
}
@media (max-width: 640px) {
    /* The account button is the initials alone on a phone; its name is in its label. */
    .user-menu-name, .user-menu-btn .airline-switcher-chevron { display: none; }
    .user-menu-btn { min-width: 44px; justify-content: center; padding: 0.25rem; }
}

/* The menu button opens the drawer, which exists only below 960px: above it the button is hidden.
   (proposal.css says so with .topbar-menu { display: none }, but .topbar-icon-btn's later display
   overrode it on the boards - product/37 M24.2 corrections.) */
.shell-topbar .topbar-menu { display: none; }
@media (max-width: 960px) {
    .shell-topbar .topbar-menu { display: inline-flex; }
}

/* A sticky sidebar is its own stacking context, and the sticky top bar (z-index 50) painted over the
   switcher's open list where the two meet. The sidebar sits above the top bar; they never overlap
   otherwise. (Only above 960px: below it the drawer's own z-index must win over the scrim.) */
@media (min-width: 961px) {
    .shell-sidebar { z-index: 60; }
}

@media (max-width: 960px) {
    /* One column whatever width was chosen on a desktop: the rail's grid rule is more specific than
       the drawer's, and without this the page was squeezed into the 64px rail column on a phone. */
    .shell[data-sidebar="rail"] { grid-template-columns: minmax(0, 1fr); }
}

/* M24.4 - the dashboard lights these held proposal.css rules (product/37 M24.1 correction 1). No
   existing page uses an anchor stat tile or .stat-tile-attention (the Assignments count tile became
   .stat-tile-filter, M24.4 correction 1), and the table rule is scoped to the dashboard's cards. */
a.stat-tile { color: inherit; text-decoration: none; }
a.stat-tile:hover { border-color: var(--vb-primary); color: inherit; }
.stat-tile-attention .stat-value { color: var(--vb-warning); }
.dash-grid .card table.data { box-shadow: none; border: none; border-radius: 0; }
.dash-grid .card table.data td.mono, .dash-grid .card table.data td.num { white-space: nowrap; }

/* M24.4 - the dashboard's own rules: what the Dashboard and DashboardSetup boards set with inline
   board-only spacing, as named rules. */
.skeleton-lede { width: min(34rem, 100%); margin-top: 0.5rem; }
.card-head-tight { margin-bottom: 0.2rem; }
.card-head-note { font-size: 0.85rem; }
.setup-intro { margin: 0; }
/* The eight steps run down two columns, in the order an airline needs them. */
.setup-list-columns { grid-auto-flow: column; grid-template-rows: repeat(4, auto); }
.setup-hint { text-align: left; }
.setup-actions { margin-bottom: 0; }
.kpi-grid-after-setup { margin-top: 1.25rem; }
.card-empty { margin: 0; }
.card-empty-sm { font-size: 0.88rem; }
.pulse-row-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.health-strip-figure { font-size: 0.8rem; }
.activity-line { font-size: 0.85rem; color: var(--vb-text); }
@media (max-width: 640px) {
    .setup-list-columns { grid-auto-flow: row; grid-template-rows: none; }
}

/* A pilot number beside the name in a table cell: spaced by the stylesheet, not by whitespace Razor trims. */
.pilot-number-inline { margin-left: 0.35rem; }
@media (max-width: 640px) {
    /* The Getting started card stacks on a phone: the ring above the list, the list one column. */
    .setup-card { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
    .setup-actions { flex-wrap: wrap; }
}

/* M24.5 - the Settings hub and category pages' own rules. */
.settings-hidden-note { margin-top: 1rem; font-size: 0.88rem; }
.settings-crosslink { margin: 0.75rem 0 0; }
.settings-note { margin: 0.75rem 0 0; }
.settings-preset-toggle { align-self: center; }
.setup-row-count { font-size: 0.88rem; }
.setup-row-link { margin-left: auto; font-weight: 600; font-size: 0.88rem; }
.setup-review { margin-top: 0.75rem; }
.setup-review > summary { cursor: pointer; font-weight: 600; font-size: 0.88rem; color: var(--vb-primary-bright); width: max-content; }
@media (max-width: 960px) {
    /* The category rail sits above the cards on a tablet or phone rather than beside them. */
    .settings-layout { grid-template-columns: minmax(0, 1fr); }
    .settings-rail { position: static; grid-auto-flow: column; overflow-x: auto; }
    /* One line per category in the strip: a wrapped label doubles its height and splits the name. */
    .settings-rail { justify-content: start; }
    .settings-rail a { white-space: nowrap; }
    .settings-cards { grid-template-columns: minmax(0, 1fr); }
}
@media (min-width: 961px) and (max-width: 1280px) {
    .settings-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* M24.6 - the page sweep (product/37 section 9). An inactive, retired or hidden row stays in its list,
   muted; its status pill still names the state, so the muting is never the only signal. */
.row-inactive td, .row-inactive td a:not(.btn) { color: var(--vb-text-muted); }
/* The filter bar with the search box lines its parts up on their centres (the ListPage board). The
   older filter bars, with labelled fields, keep their bottom alignment. */
.filter-bar:has(> .filter-search) { align-items: center; }
/* The list row anatomy of the ListPage board, new with the queue (M24.6): the row's name in weight,
   a second line beneath it, and a column only as wide as its buttons. */
.row-primary { font-weight: 600; }
.row-sub { display: block; font-size: 0.8rem; color: var(--vb-text-muted); }
.row-sub .badge { margin-top: 0.3rem; }
.cell-tight { width: 1%; white-space: nowrap; }
/* The PIREP queue (M24.6, the ListPage and Mobile boards): one line per row on a wide screen, cards on a
   phone. The day a report landed reads in lower case mid-sentence on a card ("landed today"). */
table.data-nowrap tbody td { white-space: nowrap; }
.queue-cards { display: none; }
.queue-card { padding: 0.9rem 1rem; margin-bottom: 0.75rem; }
.queue-card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.queue-card-line { margin-top: 0.3rem; font-size: 0.85rem; color: var(--vb-text-muted); }
.queue-card-flags { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.queue-card-who { color: var(--vb-text); }
.queue-card-action { display: flex; align-items: center; justify-content: center; width: 100%; min-height: 44px; margin-top: 0.75rem; }
@media (max-width: 640px) {
    .queue-table { display: none; }
    .queue-cards { display: block; }
}
/* Between a phone and 1440 the queue's eleven one-line columns can be wider than the column (M24.7: at 768
   the page scrolled sideways). There the TABLE scrolls sideways, never the page, and so its header
   cannot stick to the page; from 1440 it fits and the header sticks under the top bar as drawn. */
@media (min-width: 641px) and (max-width: 1439px) {
    /* position: relative keeps the header's visually-hidden "Actions" label inside the scroll: absolutely
       positioned, it otherwise took the page as its box and pushed it 270px wide. */
    .table-wrap.queue-table { overflow-x: auto; position: relative; }
    .table-wrap.queue-table table.data.data-sticky thead th { position: static; }
}
/* The Switch component (M24.6): a real checkbox drawn as the .toggle-switch, so it posts in a static
   form and binds in an island. Its row is the .switch-row the category pages already use. */
.switch-field { border-top: none; padding: 0.35rem 0; margin-bottom: 0.6rem; }
.switch-field label { cursor: pointer; }
input.switch-input {
    appearance: none;
    width: 40px;
    height: 22px;
    flex: none;
    margin: 0.15rem 0 0;
    border-radius: 999px;
    position: relative;
    background: var(--vb-border-strong);
    cursor: pointer;
}
input.switch-input::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--vb-text);
    transition: left 0.15s ease;
}
input.switch-input:checked { background: var(--vb-primary); }
input.switch-input:checked::after { left: 21px; }
input.switch-input:focus-visible { outline: 2px solid var(--vb-primary-bright); outline-offset: 2px; }
input.switch-input:disabled { opacity: 0.5; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) { input.switch-input::after { transition: none; } }

/* ==========================================================================
   M24.8 - the held proposal.css restyles land (Myles, 2026-09-27: "B, rules approved, land the
   restyles, build it"; closes product/37 M24.1 correction 1 and M24.2 correction 10). Copied from
   design/admin-shell/proposal.css as drawn; later in the cascade than the rules they restyle.
   ========================================================================== */
body { display: block; }
.page-header { margin-bottom: 1.4rem; }
.page-header h1 { margin: 0; font-size: 1.55rem; }
.page-header-title-row { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; }
.page-header .lede { margin-top: 0.3rem; font-size: 0.95rem; }
/* Side by side only above a phone: there the header is a column (align-items: stretch), where
   centring would shrink the actions and a 24rem basis would become a 24rem HEIGHT. */
@media (min-width: 641px) {
    .page-header { align-items: center; }
    .page-header > div:not(.page-header-leading):not(.page-header-actions) { flex: 1 1 24rem; min-width: 0; }
}
.filter-bar { align-items: center; gap: 0.6rem; padding: 0.7rem 0.9rem; margin-bottom: 1rem; }
table.data th {
    font-size: 0.74rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0.55rem 1rem;
}
table.data tbody tr:hover td { background: var(--vb-bg-soft); }
.pager { justify-content: space-between; margin-top: 0.9rem; }
.btn .icon { width: 16px; height: 16px; }
.btn-danger { color: var(--vb-danger); border-color: color-mix(in srgb, var(--vb-danger) 50%, transparent); background: transparent; }
.btn-danger:hover { background: var(--vb-danger-soft); color: var(--vb-danger); border-color: var(--vb-danger); }
.badge-marked::before { display: none; }
.card table.data { box-shadow: none; border: none; border-radius: 0; }
.card table.data td.mono, .card table.data td.num { white-space: nowrap; }
.modal { padding: 1.4rem 1.5rem; }
.modal h2 { font-size: 1.1rem; margin: 0 0 0.5rem; }
.modal p { margin: 0 0 0.5rem; font-size: 0.92rem; }
.badge { white-space: nowrap; }

/* The row, one way (ListConsistency board, option B). One button size in every row: the small
   outlined button, .btn-sm (M24.8 moved the last page off its old alias, .btn-small).
   The row's actions sit in a <div class="row-actions"> inside a tight last cell - a table cell
   itself must never be display: flex. */
.btn-sm { padding: 0.35rem 0.8rem; font-size: 0.85rem; }
.btn-sm.btn-icon { padding: 0.35rem 0.5rem; }
.btn-sm.btn-icon .icon { width: 16px; height: 16px; }
/* flex-wrap: nowrap, not just white-space (Myles, 2026-09-28: "the bottoms wrap"): an older .row-actions rule
   above sets flex-wrap: wrap, and a tight cell shrinks to its narrowest content - so every row's ⋯ dropped under
   its button and the row grew. */
.row-actions { display: flex; flex-wrap: nowrap; justify-content: flex-end; align-items: center; gap: 0.35rem; white-space: nowrap; }
.row-actions .btn { padding: 0.35rem 0.8rem; font-size: 0.85rem; }
.row-actions .btn.btn-icon { padding: 0.35rem 0.5rem; }
/* A row menu's list floats on its own layer (admin-shell.js places it under its button), so a
   table's rounded, clipped wrapper cannot cut it off. */
.menu-list.menu-list-floating { position: fixed; right: auto; top: auto; z-index: 70; }

@media (max-width: 640px) {
    /* Phone: every control keeps a 44px target (the Mobile board). */
    .btn { min-height: 44px; }
    /* A list's table becomes a card per row: the name first, the facts beneath it on one wrapping
       line, the row's one button and its menu full width at the foot (ListConsistency frame 5). */
    table.data.data-cards thead { display: none; }
    table.data.data-cards, table.data.data-cards tbody { display: block; }
    table.data.data-cards tr { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.7rem; padding: 0.85rem 1rem; border-top: 1px solid var(--vb-border); }
    table.data.data-cards tr:first-child { border-top: none; }
    table.data.data-cards td { display: block; padding: 0; border: none; background: none; font-size: 0.85rem; color: var(--vb-text-muted); white-space: normal; }
    table.data.data-cards td:first-child { flex: 1 1 60%; min-width: 0; font-size: 0.95rem; color: var(--vb-text); }
    table.data.data-cards td:empty { display: none; }
    table.data.data-cards td.cell-actions { flex: 1 1 100%; margin-top: 0.5rem; }
    table.data.data-cards td.cell-actions .row-actions { justify-content: stretch; }
    table.data.data-cards td.cell-actions .row-actions > .btn:not(.btn-icon) { flex: 1; }
    table.data.data-cards tbody tr:hover td { background: none; }
    /* A selection checkbox leads the card without being its title; the cell after it is. */
    table.data.data-cards td.col-select { flex: 0 0 auto; width: auto; }
    table.data.data-cards td.col-optional { display: none; }
    table.data.data-cards td.col-select + td { flex: 1 1 70%; min-width: 0; font-size: 0.95rem; color: var(--vb-text); }
    /* A row whose content is a sentence (an activity entry) gives it the card's full width. */
    table.data.data-cards td.cell-lead { flex: 1 1 100%; order: -1; font-size: 0.92rem; color: var(--vb-text); }
    table.data.data-cards tr:has(td.cell-lead) td:first-child { flex: 0 1 auto; font-size: 0.85rem; color: var(--vb-text-muted); }
}
/* A table's scroll wrapper is the box for anything absolutely placed inside it - the header's
   visually-hidden "Actions" label otherwise took the page as its box and widened it (M24.7 fixed
   the PIREP queue alone; M24.8 found Aircraft types and the File library doing the same). */
.table-wrap { position: relative; }
/* Dropdown lists in the theme (Myles, 2026-09-28: the Jump to list's hover was the platform's
   blue). Where the browser supports customizable selects (appearance: base-select, Chromium), every
   styled select opens a list drawn like the ⋯ menu: raised surface, the theme's hover. Elsewhere
   the platform's own list stays, which is still a working select. Our chevron stays, so the
   browser's picker icon is hidden. */
@supports (appearance: base-select) {
    .field select, .condition-row select, .condition-match-select, .template-rows select,
    .pirep-sections-jump select, .filter-select { appearance: base-select; }
    .field select::picker-icon, .condition-row select::picker-icon, .condition-match-select::picker-icon,
    .template-rows select::picker-icon, .pirep-sections-jump select::picker-icon, .filter-select::picker-icon { display: none; }
    .field select::picker(select), .condition-row select::picker(select), .condition-match-select::picker(select),
    .template-rows select::picker(select), .pirep-sections-jump select::picker(select), .filter-select::picker(select) {
        appearance: base-select;
        margin-top: 0.3rem;
        padding: 0.35rem;
        background: var(--vb-surface-raised);
        border: 1px solid var(--vb-border-strong);
        border-radius: var(--vb-radius);
        box-shadow: var(--vb-shadow);
        color: var(--vb-text);
    }
    :is(.field, .condition-row, .template-rows, .pirep-sections-jump) select option,
    :is(.condition-match-select, .filter-select) option {
        padding: 0.45rem 0.7rem;
        border-radius: var(--vb-radius-sm);
        color: var(--vb-text-muted);
        font-size: 0.9rem;
    }
    :is(.field, .condition-row, .template-rows, .pirep-sections-jump) select option:is(:hover, :focus-visible),
    :is(.condition-match-select, .filter-select) option:is(:hover, :focus-visible) {
        background: var(--vb-primary-soft);
        color: var(--vb-text);
        outline: none;
    }
    :is(.field, .condition-row, .template-rows, .pirep-sections-jump) select option:checked,
    :is(.condition-match-select, .filter-select) option:checked { color: var(--vb-text); font-weight: 600; }
    :is(.field, .condition-row, .template-rows, .pirep-sections-jump) select option::checkmark,
    :is(.condition-match-select, .filter-select) option::checkmark { color: var(--vb-primary-hover); }
}
/* Stacked boxes keep the rhythm (Myles, 2026-09-28: Scoring and Leaderboards had a card flush
   against the box above it). Scoped to the page column itself, where vertical margins collapse, so a
   page that already spaces its boxes keeps its own gap rather than gaining a second one; grids and
   flex rows space their children with gap and are untouched. */
.page > :is(.card, .table-wrap) + :is(.card, .table-wrap, .alert-success, .alert-error) { margin-top: 1rem; }
/* A section head that is all its card holds carries no gap under it, and nor does a card's closing
   sentence: the browser's paragraph margin would sit on top of the card's own padding. */
.settings-section-head:last-child,
.card > p:last-child { margin-bottom: 0; }
/* ...and a card's opening sentence adds none on top (the PIREP page's Credited hours card, 2026-09-28). */
.card > p:first-child { margin-top: 0; }
/* Skeletons the size of what they stand in for (Myles, 2026-09-28: "they don't really represent the
   page... tight up at the top"). A whole page's skeleton opens with the page header's shape (the h1 and
   its lede, at their real heights) and puts full-height rows in a card; the dashboard's copies its
   board: tiles as tall as the real ones, attention rows, the pulse's four figures, the side cards. */
.skeleton-head { margin: 0.2rem 0 1.4rem; }
.skeleton-h1 { height: 1.9rem; width: min(16rem, 60%); border-radius: 8px; }
.skeleton-head .skeleton-lede { height: 1rem; margin-top: 0.75rem; }
.skeleton-card > .skeleton-title { height: 1.35rem; width: min(14rem, 45%); margin-bottom: 0.9rem; }
.skeleton-card > .skeleton-row:first-child { border-top: none; padding-top: 0.2rem; }
.skeleton-page .skeleton-row { padding: 1rem 0; }
.skeleton-page .skeleton-row .skeleton { height: 1.05rem; }
.skeleton-page .skeleton-short { flex: 0 0 6rem; }
.skeleton-stat { gap: 0.7rem; pointer-events: none; }
.skeleton-stat-label { width: 55%; height: 0.8rem; }
.skeleton-stat-value { width: 38%; height: 1.9rem; border-radius: 8px; }
.skeleton-stat-sub { width: 70%; height: 0.75rem; }
.skeleton-title-side { width: 50%; }
.skeleton-attention { display: flex; align-items: center; gap: 0.8rem; padding: 0.8rem 0; border-top: 1px solid var(--vb-border); }
.skeleton-attention:first-of-type { border-top: none; }
.skeleton-attention-icon { flex: none; width: 2rem; height: 2rem; border-radius: 8px; }
.skeleton-attention-text { flex: 1; display: grid; gap: 0.4rem; }
.skeleton-attention-sub { width: 55%; height: 0.75rem; }
.skeleton-attention-btn { flex: none; width: 4.5rem; height: 2rem; border-radius: var(--vb-radius-sm); }
.skeleton-pulse { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.25rem; }
.skeleton-pulse > div { display: grid; gap: 0.55rem; }
.skeleton-spark { height: 1.8rem; }
@media (max-width: 640px) { .skeleton-pulse { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* A second, narrower box in the filter bar - a thing to narrow by, after the search (M24.8). */
.filter-search-narrow { flex: 0 1 220px; min-width: 170px; }
/* A date in the filter bar carries its word ("From", "To") beside it, since a date box has no
   first option to name itself with. */
.filter-date { display: inline-flex; align-items: center; gap: 0.4rem; margin: 0; font-size: 0.85rem; color: var(--vb-text-muted); }
.filter-date input {
    background: var(--vb-bg-soft);
    border: 1px solid var(--vb-border-strong);
    border-radius: var(--vb-radius-sm);
    color: var(--vb-text);
    padding: 0.4rem 0.6rem;
    font: inherit;
    font-size: 0.88rem;
}
/* A list inside a card (Data ops' history) keeps its filter bar flat: one card, not a card in a card. */
.card > .filter-bar.card { background: transparent; border: none; box-shadow: none; padding: 0; }
@media (max-width: 640px) {
    /* Phone: the bar's controls wrap to full-width 44px rows, the count on its own line last. */
    .filter-bar .filter-search, .filter-bar .filter-select, .filter-bar .filter-date { flex: 1 1 100%; min-height: 44px; }
    .filter-bar .filter-date input { flex: 1; min-height: 44px; }
    .filter-bar .filter-count { margin-left: 0; }
}


/* --------------------------------------------------------------------------
   Pilot detail redesign (product/40 stage 1; design/pilot-detail/proposal.css merged as it
   stood). Additive: every rule names the shipped class it extends; none restyles one.
   -------------------------------------------------------------------------- */

/* 1. The two-column detail grid (M24, app.css 6765) has no collapse rule; a tablet gets one column.
   .detail-grid-halves is the same grid split evenly (the Flights tab's two lower cards) and
   collapses with it. */
.detail-grid-halves { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 960px) { .detail-grid, .detail-grid-halves { grid-template-columns: minmax(0, 1fr); } }

/* Hub create/edit redesign (product/51; design/hub-form/proposal.css merged as it stood).
   The hub form is one EditForm rendered as the detail grid, 3:2: the main column holds what the hub
   IS (details, the image), the side column how it BEHAVES (status and visibility, the monthly
   activity target). 3:2 rather than the detail page's 1fr 21rem because the side column carries
   sentence-length hints; 3:2 rather than halves because the image field's text column must keep
   room for its purpose sentence and two buttons beside a 16rem preview. One column at or below
   1100px (the .dash-grid breakpoint): between 960 and 1100 a 2fr side would squeeze that text
   column back under 200px. The 960px rule for .detail-grid applies beneath it. */
.detail-grid-form { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
@media (max-width: 1100px) { .detail-grid-form { grid-template-columns: minmax(0, 1fr); } }

/* 2. Page header: a second line of operational facts under the lede (where the pilot is, when
   they last flew, when they last signed in). Icons are the shell's own, 14px, muted. */
.page-header-facts { display: flex; flex-wrap: wrap; gap: 0.35rem 1.1rem; margin: 0.5rem 0 0; font-size: 0.88rem; color: var(--vb-text-muted); }
.page-header-facts > span { display: inline-flex; align-items: center; gap: 0.35rem; }
.page-header-facts .icon { width: 14px; height: 14px; }
.page-header-facts .mono { color: var(--vb-text); }
.page-header-title-row .badge { align-self: center; }

/* 3. A labelled menu button ("Actions" with a chevron): Menu.razor gains a Text parameter and
   renders .btn with the label instead of .btn-icon with the dots. Same list, same script. */
.menu-btn-text .icon { width: 16px; height: 16px; margin-left: 0.1rem; }
.menu-list-grouped .menu-group-label { padding: 0.35rem 0.7rem 0.15rem; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); }

/* 4. Rank progress: one bar per requirement the next rank sets, a purple fill (the progress-line
   is 4px and green, for setup and tours; a rank bar is the pilot's own measure). */
.rank-ladder { display: flex; align-items: center; gap: 0.6rem; font-weight: 650; font-size: 1rem; }
.rank-ladder .icon { width: 16px; height: 16px; color: var(--vb-text-muted); }
.rank-progress { display: grid; gap: 0.7rem; margin-top: 0.8rem; }
.rank-progress-row { display: grid; gap: 0.3rem; }
.rank-progress-labels { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.85rem; }
.rank-progress-labels .mono { font-variant-numeric: tabular-nums; }
.rank-bar { height: 8px; border-radius: 999px; background: var(--vb-border); overflow: hidden; }
.rank-bar > span { display: block; height: 100%; background: var(--vb-primary-bright); }
.rank-bar-done > span { background: var(--vb-success); }

/* 5. Awards as tiles: emblem (the award's image where one is configured, the award icon where
   not), name, how it was earned and when. Wraps to the width. */
.award-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 0.75rem; }
.award-tile { display: flex; gap: 0.75rem; align-items: center; padding: 0.7rem 0.8rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); min-width: 0; }
.award-tile-emblem { width: 44px; height: 44px; border-radius: 10px; flex: none; display: inline-flex; align-items: center; justify-content: center; background: var(--vb-primary-soft); color: var(--vb-primary-bright); overflow: hidden; }
.award-tile-emblem .icon { width: 22px; height: 22px; }
.award-tile-emblem img { width: 100%; height: 100%; object-fit: contain; }
.award-tile-text { min-width: 0; flex: 1; font-size: 0.85rem; color: var(--vb-text-muted); }
.award-tile-text strong { display: block; font-size: 0.92rem; color: var(--vb-text); }
.award-tile .menu { flex: none; }
/* product/79: the name and its count pill, spaced by the gap, never a typed space. */
.award-tile-title { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
.award-tile-title strong { display: inline; }
/* A table cell's text and the count pill beside it (the award page's holder table). */
.cell-with-pill { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
/* An award earned more than once lists every occurrence under its tile. */
.award-tile:has(> .award-occurrences) { flex-wrap: wrap; }
.award-tile > .award-occurrences { flex-basis: 100%; margin: 0; }
.award-occurrences ul { list-style: none; margin: 0; padding: 0.3rem 0.9rem 0.5rem; display: grid; gap: 0.35rem; }
.award-occurrences li { display: flex; align-items: center; gap: 0.5rem; font-size: 0.82rem; color: var(--vb-text-muted); }
.award-occurrence-text { min-width: 0; flex: 1; display: flex; flex-direction: column; }

/* 6. The pilot's timeline: the PIREP page's timeline rows (app.css 528) with a kind mark and no
   inner scroll (the card grows; the list is paged). */
.pilot-timeline .timeline-list { max-height: none; overflow: visible; padding-right: 0; }
.pilot-timeline .timeline-row { align-items: flex-start; }
.timeline-mark { width: 8px; height: 8px; border-radius: 50%; background: var(--vb-text-muted); flex: none; margin-top: 0.5rem; }
.timeline-mark-success { background: var(--vb-success); }
.timeline-mark-warning { background: var(--vb-warning); }
.timeline-mark-danger { background: var(--vb-danger); }
.timeline-mark-info { background: var(--vb-primary-bright); }
.timeline-text { flex: 1 1 auto; min-width: 0; }
.timeline-text .row-sub { margin-top: 0.05rem; }

/* 7. The assignment card's lead line: flight number, route, pills. */
.assignment-lead { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.8rem; margin-bottom: 0.7rem; }
.assignment-lead .code { font-family: var(--vb-mono); font-weight: 700; font-size: 1.2rem; }
.assignment-lead .route { font-family: var(--vb-mono); font-size: 1.05rem; }
.assignment-facts { grid-template-columns: 7rem minmax(0, 1fr) 7rem minmax(0, 1fr); }
@media (max-width: 700px) { .assignment-facts { grid-template-columns: 9rem minmax(0, 1fr); } }

/* 8. A small proportion bar inside a table cell (aircraft flown, places). */
.mini-bar { height: 4px; border-radius: 999px; background: var(--vb-border); overflow: hidden; margin-top: 0.3rem; min-width: 6rem; }
.mini-bar > span { display: block; height: 100%; background: var(--vb-primary-bright); }

/* 9. Location card: the airport as the lead fact. */
.location-lead { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.location-lead .code { font-family: var(--vb-mono); font-weight: 700; font-size: 1.25rem; }
.location-lead .name { font-size: 0.95rem; }

/* 10. A tile's sub line sits a touch closer to the value. */
.kpi-grid .stat-tile .row-sub { margin-top: 0.1rem; }

/* 11. The chart card's segmented control is the section-tabs component at small size. */
.card-head .section-tabs { margin: 0; box-shadow: none; gap: 0; border: 1px solid var(--vb-border-strong); border-radius: var(--vb-radius-sm); overflow: hidden; }
.card-head .section-tabs button { padding: 0.25rem 0.7rem; font-size: 0.8rem; border-bottom: none; }
.card-head .section-tabs [aria-selected="true"] { background: var(--vb-primary-soft); color: var(--vb-text); }

/* 12. The modal that shows a result (a temporary password): the value in a copyable box. */
.result-box { display: flex; align-items: center; gap: 0.6rem; padding: 0.7rem 0.9rem; border: 1px solid var(--vb-border-strong); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); font-family: var(--vb-mono); font-size: 1.05rem; }
.result-box .btn { margin-left: auto; }

/* 12b. A fact's value never escapes its card: a long email, hub name or username wraps (the
   Profile tab's Pilot record, found at the polish pass). Applies to every facts list. */
.facts dd, .settings-facts dd { min-width: 0; overflow-wrap: anywhere; }
.facts dd .mono, .settings-facts dd .mono { overflow-wrap: anywhere; }
/* In the 21rem side column a 9rem label column leaves too little for a badge or an email
   (the Overview's Account card, found at the polish pass): there the label column takes
   only what its longest label needs. */
.detail-side .facts { grid-template-columns: max-content minmax(0, 1fr); }
/* A card head carrying a badge AND a button (the Profile tab's Public profile) wraps them under
   the heading on a phone instead of shrinking the heading until its words overflow into them. */
.card-head-wrap { flex-wrap: wrap; }
.card-head-wrap h2, .card-head-wrap h3 { flex: 1 0 auto; }

/* 12c. A list of places or routes with their counts (the Flights tab's Where they fly): each item
   its own token, wrapping, so codes never run together. */
.place-list { display: flex; flex-wrap: wrap; gap: 0.35rem 0.5rem; margin: 0; padding: 0; list-style: none; }
.place-list li { display: inline-flex; align-items: baseline; gap: 0.35rem; padding: 0.15rem 0.55rem; border: 1px solid var(--vb-border); border-radius: 999px; background: var(--vb-bg-soft); font-size: 0.85rem; white-space: nowrap; }
.place-list .mono { font-weight: 600; }
.place-list .count { color: var(--vb-text-muted); font-variant-numeric: tabular-nums; }

/* 12d. The bar chart without its own card (BarChart Bare), inside a host card with its head. */
.bar-chart-body { margin-top: 0.25rem; }

/* 13. Facts in two columns inside a wide card (the profile's About). */
.facts-2 { grid-template-columns: 9rem minmax(0, 1fr) 9rem minmax(0, 1fr); }
@media (max-width: 700px) { .facts-2 { grid-template-columns: 9rem minmax(0, 1fr); } }
/* A fact whose value is a sentence with actions (the pilot's Leave) takes the rest of its row
   instead of wrapping word by word in one value column. */
.facts-2 dd.fact-wide { grid-column: 2 / -1; }

/* ==========================================================================
   Rank detail (product/42; design/rank-detail/, gate G1 passed 2026-10-01). The board set's
   proposal.css, merged as it stands, then the page's responsive rules from RankMobile.
   ========================================================================== */


/* 1. The rank badge frame: the header's leading slot and the badge preview. A badge is a shape
   (an epaulette, wings), so it is CONTAINED, never cropped (the .rank-insignia rule, at size).
   Extends .award-tile-emblem. As a button it opens the shared MediaPreviewModal. */
.rank-badge-frame { width: 72px; height: 72px; border-radius: 14px; flex: none; display: inline-flex; align-items: center; justify-content: center; padding: 8px; background: var(--vb-bg-soft); border: 1px solid var(--vb-border); color: var(--vb-text-muted); overflow: hidden; }
.rank-badge-frame img, .rank-badge-frame svg.badge-art { width: 100%; height: 100%; object-fit: contain; }
.rank-badge-frame .icon { width: 28px; height: 28px; }
button.rank-badge-frame { cursor: zoom-in; font: inherit; }
button.rank-badge-frame:hover { border-color: var(--vb-border-strong); filter: brightness(1.12); }
.rank-badge-frame-empty { border-style: dashed; border-color: var(--vb-border-strong); }

/* 2. Requirements: one row per requirement the rank SETS (label, figure). A requirement the rank
   does not set has no row; a points row the airline does not enforce says so. Extends .card-list. */
.rank-req-list { list-style: none; margin: 0; padding: 0; }
.rank-req-list li { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.55rem 0; border-top: 1px solid var(--vb-border); font-size: 0.9rem; }
.rank-req-list li:first-child { border-top: none; padding-top: 0; }
.rank-req-list .rank-req-figure { font-family: var(--vb-mono); font-weight: 650; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rank-req-list .row-sub { display: block; }

/* 3. The ladder, vertical: every active progression rank in order, this one marked, with how many
   active pilots are on each rung. Extends .card-list. */
.rank-rungs { list-style: none; margin: 0; padding: 0; }
.rank-rungs li { display: grid; grid-template-columns: 1.4rem minmax(0, 1fr) auto; gap: 0.5rem; align-items: baseline; padding: 0.45rem 0.5rem; border-radius: var(--vb-radius-sm); font-size: 0.9rem; }
.rank-rungs .rung-n { color: var(--vb-text-muted); font-size: 0.8rem; font-variant-numeric: tabular-nums; }
.rank-rungs .rung-count { color: var(--vb-text-muted); font-size: 0.82rem; white-space: nowrap; }
.rank-rungs li[aria-current="true"] { background: var(--vb-primary-soft); font-weight: 650; }
.rank-rungs li[aria-current="true"] .rung-count { color: var(--vb-text); }

/* 4. A requirement cell in the promotion table: the remaining figure, and under it what it is
   measured against. "Met" is words plus a mark, never colour alone. */
.req-remaining { font-variant-numeric: tabular-nums; white-space: nowrap; }
.req-remaining strong { font-weight: 650; }
.req-met { color: var(--vb-success); white-space: nowrap; }
.req-met .icon { width: 14px; height: 14px; vertical-align: -2px; margin-right: 0.2rem; }

/* 5. The activity card's coverage line: where the record starts. */
.coverage-line { display: flex; align-items: center; gap: 0.45rem; margin: 0.8rem 0 0; padding-top: 0.7rem; border-top: 1px solid var(--vb-border); font-size: 0.8rem; color: var(--vb-text-muted); }
.coverage-line .icon { width: 14px; height: 14px; flex: none; }

/* 6. A quiet statement card ("off the ladder", "manually granted"): the neutral sibling of
   .alert-info, for a fact about the rank rather than a warning. */
.rank-statement { display: flex; gap: 0.7rem; align-items: flex-start; padding: 0.8rem 1rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); font-size: 0.9rem; }
.rank-statement .icon { width: 18px; height: 18px; flex: none; margin-top: 0.15rem; color: var(--vb-primary-bright); }
.rank-statement p { margin: 0; }

/* 7. The page at each width (RankMobile). Three tiles a row above a phone; on a phone the third
   folds into the first two as their sub-lines (two grids, one shown). Between 641 and 960 the page
   is one column with the tables as tables, minus the holders' Note column and the lede's last
   clause; on a phone the header is a grid (badge beside the title, actions full width beneath)
   and each pilot row is a card with every requirement as a labelled pair. */
.rank-kpi { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.rank-kpi-phone { display: none; }
.rank-pilots .req-label { display: none; }
.rank-phone-only { display: none; }
@media (max-width: 960px) {
    .rank-lede-extra { display: none; }
    .rank-holders .col-optional { display: none; }
}
@media (max-width: 640px) {
    .kpi-grid.rank-kpi { display: none; }
    .kpi-grid.rank-kpi-phone { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.6rem; }
    .rank-wide-only, table.data.data-cards td.rank-wide-only { display: none; }
    .rank-phone-only { display: inline; }
    .rank-page .page-header { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.7rem 0.8rem; }
    .rank-page .page-header-leading + div { margin-right: 0; }
    .rank-page .page-header-actions { grid-column: 1 / -1; }
    .rank-page .page-header-actions .btn { flex: 1; justify-content: center; }
    .rank-page .page-header-leading .rank-badge-frame { width: 56px; height: 56px; }
    table.data.data-cards.rank-pilots td:first-child { flex: 1 1 100%; }
    table.data.data-cards.rank-pilots td:not(:first-child) { min-width: 45%; flex: 1 1 0; }
    .rank-pilots .req-label { display: block; }
    .rank-pilots td:not(:first-child) .row-sub { display: none; }
    .rank-pilots td:not(:first-child) .req-remaining strong { color: var(--vb-text); }
    table.data.data-cards.rank-holders td.cell-actions { flex: 0 0 auto; margin-top: 0; }
}

/* product/52 Targets & Challenges: merged from design/targets/proposal.css as it stands. */
/* ---------------------------------------------------------------- Admin Portal (app.css) */

/* The three mode groups on the hub override page (target, bonus, award): the shared option cards
   side by side on a wide screen, stacked below 900px. Reuses .radio-options / .radio-option. */
.radio-options-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 900px) { .radio-options-row { grid-template-columns: minmax(0, 1fr); } }

/* A progress cell in a list: the rank bar from the pilot page with its figure beside it. */
.target-cell { display: grid; gap: 0.3rem; min-width: 11rem; }
.target-cell .rank-bar { width: 100%; }
.target-cell-figure { font-size: 0.85rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.target-cell-figure .muted { margin-left: 0.3rem; }

/* The Monthly competition table: an ineligible hub sits beneath the ranked ones with no rank, its
   figures quieter, and the reason it is out in words. */
.standings-out td { color: var(--vb-text-muted); }
.standings-out td:first-child { color: var(--vb-text-muted); }
.standings-reason { font-size: 0.8rem; color: var(--vb-text-muted); display: block; }
.standings-rank { font-weight: 700; font-variant-numeric: tabular-nums; }
.standings-divider td { padding: 0.35rem 0.75rem; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); background: var(--vb-surface-2, transparent); }

/* The effective-target sentence on the cards and the read-only hub form card. */
.target-sentence { margin: 0 0 0.75rem; font-size: 0.95rem; }
.target-sentence strong { font-weight: 600; }
.target-sentence .badge { margin-left: 0.4rem; vertical-align: middle; }
.target-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.6rem 1rem; margin: 0.5rem 0 0.9rem; font-size: 0.9rem; }
.target-facts dt { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); margin: 0 0 0.15rem; }
.target-facts dd { margin: 0; }

/* A finalised standings row whose live outcome has since moved: the judged figure stays, the
   current one is said beside it. */
.judged-now { font-size: 0.8rem; color: var(--vb-text-muted); white-space: nowrap; }

/* The result page's supersession chain. */
.result-chain { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; font-size: 0.9rem; }
.result-chain li { display: flex; gap: 0.6rem; align-items: baseline; }
.result-chain .badge { flex: none; }

/* product/52: the two layouts the boards drew as inline styles. The Overview's three side cards sit in
   a row beneath the full-width standings (README choice 1; the Mobile board keeps the row to 720px);
   the Hub targets and Airline targets cards sit two across, one column at or below 1100px. */
.targets-side-row { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 1.25rem; }
@media (max-width: 640px) { .targets-side-row { grid-template-columns: minmax(0, 1fr); } }
.targets-halves { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; margin-bottom: 1.25rem; }
@media (max-width: 1100px) { .targets-halves { grid-template-columns: minmax(0, 1fr); } }

/* Text beside a pill is spaced by the LAYOUT, never by a typed space (Myles, 2026-10-09: "no text
   touching the pill", reported on several features). Blazor strips a space at the start or end of an
   element and before or after an @if block, so a typed space silently disappears; and where one
   survives, a bare space beside a pill reads as touching. */
.targets-pill-text { margin-left: 0.5rem; }
.targets-mode-note { font-size: 0.8rem; margin-left: 0.5rem; }
.targets-grow { flex: 1; }
.targets-month-note { margin-top: 0.6rem; }
.targets-steps { margin: 0; padding-left: 1.1rem; font-size: 0.9rem; display: grid; gap: 0.4rem; }
.targets-result-grid { margin-top: 1.25rem; }
.targets-facts-single { grid-template-columns: minmax(0, 1fr); }

/* ---- product/54 E4: Settings > Emails and the email editor (design/email/proposal.css, merged) ---- */
/* The editor beside the preview: the Discord designer's grid (.template-designer, 1fr 420px) with a
   preview column wide enough for a 600px email at its desktop width. One column at or below 1200. */
.email-designer { display: grid; grid-template-columns: minmax(0, 1fr) 684px; gap: 1.25rem; align-items: start; }
@media (max-width: 1200px) { .email-designer { grid-template-columns: minmax(0, 1fr); } }
/* The subject, message and values cards stand apart, at the gap the two columns keep (Myles). */
.email-designer-column { display: grid; gap: 1.25rem; min-width: 0; }
/* Quill gives its container height: 100%, which inside a grid item resolves against the card and
   stretched the message past it; the editors are as tall as their words. */
.email-editor-body.ql-container, .email-editor-subject.ql-container { height: auto; }

/* The rendered email inside the preview card: its own light or dark canvas, never the portal's. */
.email-preview-frame { border: 1px solid var(--vb-border-strong); border-radius: var(--vb-radius-sm); overflow: hidden; background: #f3f4f6; }
.email-preview-frame.is-dark { background: #0c0d0f; }
.email-preview-frame iframe { display: block; width: 100%; border: 0; min-height: 420px; }
.email-preview-phone { width: 375px; max-width: 100%; margin: 0 auto; }

/* A protected section inside the editor: drawn where it will appear, locked, never editable. */
.email-protected { display: flex; gap: 0.6rem; align-items: flex-start; padding: 0.7rem 0.85rem; margin: 0.6rem 0; border: 1px dashed var(--vb-border-strong); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); color: var(--vb-text-muted); font-size: 0.88rem; line-height: 1.45; }
.email-protected .icon { flex: none; width: 16px; height: 16px; margin-top: 0.15rem; }
.email-protected strong { color: var(--vb-text); font-weight: 600; }

/* A value in the subject or body: inserted as text, shown as a chip so it is never mistaken for prose. */
.email-var { display: inline-block; padding: 0 0.4rem; border-radius: 6px; background: var(--vb-primary-soft); color: var(--vb-primary-bright); font-family: var(--vb-mono); font-size: 0.85em; line-height: 1.5; }

/* The editor's toolbar (the shipped Quill toolbar, drawn with the Email profile's four buttons). */
.email-editor-toolbar { display: flex; gap: 0.25rem; padding: 0.4rem 0.5rem; border: 1px solid var(--vb-border-strong); border-bottom: 0; border-radius: var(--vb-radius-sm) var(--vb-radius-sm) 0 0; background: var(--vb-bg-soft); }
.email-editor-toolbar .btn { min-width: 2rem; }
.email-editor-body { border: 1px solid var(--vb-border-strong); border-radius: 0 0 var(--vb-radius-sm) var(--vb-radius-sm); padding: 0.9rem 1rem; min-height: 11rem; background: var(--vb-bg); line-height: 1.6; }
.email-editor-body p { margin: 0 0 0.75rem; }
.email-editor-body p:last-child { margin-bottom: 0; }
.email-editor-body[aria-readonly="true"] { color: var(--vb-text-muted); background: var(--vb-bg-soft); }

/* The kind rows on the Emails page: the kind's name opens its editor; its state sits beside it. */
.email-kind-name { display: flex; flex-direction: column; gap: 0.1rem; }
.email-kind-name small { color: var(--vb-text-muted); font-size: 0.8rem; }

/* product/54 E4: the editor's own pieces beyond the board's proposal.css. */
/* Board 5/6: the status reads as one sentence that wraps under the badge; the badge is spaced by its
   margin (Blazor strips a typed space beside it). */
.email-status > .badge { margin-right: 0.5rem; vertical-align: 0.1em; }
.email-siblings { margin: 0 0 1rem; }
.email-picker { margin: 0 0 0.6rem; }
.email-values { margin-top: 0.4rem; }
.email-values .choice-chip:disabled { cursor: default; opacity: 0.75; }
/* The Quill hosts, drawn as the board's fields rather than Quill's own snow chrome. */
.email-editor-subject { border: 1px solid var(--vb-border-strong); border-radius: var(--vb-radius-sm); background: var(--vb-bg); }
.email-editor-subject .ql-editor { min-height: 0; padding: 0.6rem 0.8rem; line-height: 1.5; }
.email-editor-subject[aria-readonly="true"], .email-editor-body[aria-readonly="true"] { color: var(--vb-text-muted); background: var(--vb-bg-soft); }
.email-editor-subject.ql-container, .email-editor-body.ql-container { font: inherit; font-size: 0.95rem; }
.email-editor-body.ql-container { border: 1px solid var(--vb-border-strong); }
.email-editor-body .ql-editor { padding: 0; min-height: 9rem; }
.email-editor-body .ql-editor p { margin: 0 0 0.75rem; }
.email-editor-toolbar.ql-toolbar { padding: 0.4rem 0.5rem; border: 1px solid var(--vb-border-strong); border-bottom: 0; font-family: inherit; }
.email-editor-toolbar .btn.ql-active { background: var(--vb-primary-soft); color: var(--vb-primary-bright); }
.email-editor-body .email-protected { user-select: none; }
.email-preview-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.75rem; }
.email-preview-head h2 { margin: 0; }
.email-preview-switches { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.template-preview .email-preview-with { margin-top: 1.5rem; }
.email-preview-actions { justify-content: flex-start; margin-top: 1rem; }

/* ---- File manual PIREP: the change confirmations. Parts of the shared Modal body: what the new
   flight supplies, what is cleared, and the first link's one explicit choice. */
.apf-dialog { max-width: 32rem; }
.apf-dialog > p.muted { margin: 0 0 0.9rem; }
.apf-dialog-part { display: grid; gap: 0.2rem; margin: 0 0 0.8rem; font-size: 0.875rem; }
.apf-dialog-part strong { font-size: 0.75rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--vb-text-muted); }
.apf-dialog-part ul { margin: 0; padding-left: 1.1rem; }
.apf-dialog-keep { display: flex; align-items: flex-start; gap: 0.6rem; margin: 0 0 0.8rem; padding: 0.6rem 0.75rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); cursor: pointer; font-size: 0.875rem; }
.apf-dialog-keep input { margin-top: 0.2rem; width: auto; flex: none; }
.apf-dialog-keep strong { display: block; }
.apf-dialog-keep .muted { display: block; font-size: 0.82rem; }
/* Keep or unlink, beside the lookup while the schedule or the pilot is being changed. */
.apf-change-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
/* product/74 4.4 and 4.6: a possible duplicate. On the File manual PIREP form, the reports flown at
   these times with the tick for Approved on entry; on the PIREP page, the overlap flag lists each
   report (it wraps, so a phone reads every fact) and the decision bar says it above the buttons. */
.apf-dup { margin: 0; }
.apf-dup strong { display: block; margin-bottom: 0.3rem; }
.apf-dup ul { margin: 0 0 0.5rem; padding-left: 1.1rem; font-size: 0.9rem; }
.apf-dup .checkbox-field { margin: 0.6rem 0 0; }
.apf-dup-invalid label { color: var(--vb-danger); }
.pirep-warning .apf-overlaps { list-style: none; padding: 0; margin: 0.5rem 0 0; }
.apf-overlaps li { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.6rem; padding: 0.45rem 0; border-top: 1px solid var(--vb-border); }
.apf-overlap-facts { color: var(--vb-text-muted); }
.apf-bar-overlap { margin: 0 0 0.55rem; font-size: 0.86rem; color: var(--vb-warning); }
.apf-bar-overlap a { font-family: var(--vb-mono); }
@media (max-width: 480px) { .apf-dialog .modal-actions { flex-direction: column-reverse; } .apf-dialog .modal-actions .btn { width: 100%; } }

/* A corrected PIREP and the rejected report it replaces: the line under the header that links the
   other one. */
.pirep-replaces { margin: -0.5rem 0 1rem; font-size: 0.9rem; color: var(--vb-text-muted); }

/* product/74 P11: correcting a report's times. The wider dialog, the UTC guidance and reference clock above
   its two times, and the action beside the times on the report. */
.apf-dialog-wide { max-width: 38rem; }
.apf-dialog-wide .field-grid { margin: 0 0 0.6rem; }
/* In the dialog's narrower fields "+1 day" sits under the date and time rather than squeezing them. */
.apf-dialog .apf-dt { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); }
.apf-dialog .apf-dt .apf-plus { grid-column: 1 / -1; }
.apf-times-error { margin: -0.35rem 0 0.75rem; }
.apf-inline-gap { margin-left: 0.35rem; }

/* product/74 P9: the location chronology card on the form, the flag's actions and the approved-with-it notice. */
.apf-chrono-text { margin: 0 0 0.5rem; font-size: 0.9rem; }
.apf-chrono .field { margin-top: 0.6rem; }
.apf-flag-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.6rem; }
.apf-outstanding { margin: 0 0 1rem; }
.apf-bar-overlap a.apf-bar-plain { font-family: inherit; }

/* product/74 P12: withdrawing a staff move - the before and after, and a withdrawn move struck through where it was. */
.apf-before-after { display: grid; grid-template-columns: 4rem minmax(0, 1fr); gap: 0.2rem 0.6rem; }
.apf-before-after > span:nth-child(odd) { color: var(--vb-text-muted); }
.apf-withdrawn td:nth-child(2) { text-decoration: line-through; color: var(--vb-text-muted); }
.apf-nowrap { white-space: nowrap; }
/* product/74 P12: the live staff moves older than the movement timeline, still withdrawable. */
.apf-earlier-moves { margin-top: 0.9rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); }
.apf-earlier-moves > summary { padding: 0.6rem 0.9rem; cursor: pointer; font-size: 0.88rem; font-weight: 550; }
.apf-earlier-moves > summary:hover { color: var(--vb-primary-bright); }
.apf-earlier-moves[open] > summary { border-bottom: 1px solid var(--vb-border); }
.apf-earlier-moves .table-wrap { margin: 0; border: 0; }
/* A dialog opened from a table's actions cell must not inherit the cell's no-wrap and alignment. */
.cell-actions .apf-dialog { white-space: normal; text-align: left; }
/* Emphasis inside a part's list is emphasis, not another of the part's small uppercase headings. */
.apf-dialog-part li strong { font-size: inherit; letter-spacing: normal; text-transform: none; color: var(--vb-text); }
.pirep-times-action { margin-top: 0.4rem; }

/* product/77: a recorded moment shown as the pilot's computer recorded it, beside its time. */
.computer-clock { margin-left: 6px; }

/* product/79: the Service anniversaries page. The pilot number beside a name is spaced by CSS. */
.sa-number { margin-left: 0.4rem; }
/* The Awards list's one-line service anniversary summary: its words and the Configure link spaced by the gap. */
.awards-anniversary-summary { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; margin: 0 0 1rem; }

/* Top landings (docs/product/80): the product CSS the Admin Portal gains, merged from
   design/top-landings/proposal-admin.css: the Engagement page's live preview rows, the
   "leading this month" strip and the previous-winners table's outcome cell. Everything else on the
   page is the shipped card, switch, option-card, facts, alert, table and modal primitives. */

/* The preview card: the public board as the website renders it, in the portal's own type. */
.tl-preview-row {
  display: grid; grid-template-columns: 2rem minmax(0, 1fr) auto;
  gap: 0.65rem; align-items: center; padding: 0.55rem 0;
  border-top: 1px solid var(--vb-border); font-size: 0.875rem;
}
.tl-preview-row:first-child { border-top: 0; padding-top: 0; }
.tl-preview-place {
  width: 2rem; height: 2rem; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 0.72rem; background: var(--vb-surface-raised); color: var(--vb-text-muted);
}
.tl-preview-place.is-first { background: var(--vb-primary); color: #fff; }
.tl-preview-who { display: flex; align-items: center; gap: 0.55rem; min-width: 0; }
.tl-preview-who .avatar-initials { width: 28px; height: 28px; font-size: 0.7rem; flex: none; }
.tl-preview-text { display: flex; flex-direction: column; min-width: 0; }
.tl-preview-text .row-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tl-preview-value { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; white-space: nowrap; }
.tl-preview-value .row-sub { display: block; font-weight: 400; }
/* A landing the public board withholds (the pilot's own privacy choice) is still shown to staff,
   dimmed, with the reason, so the preview and the award never silently disagree. */
.tl-preview-row.is-withheld { opacity: 0.72; }
.tl-preview-row.is-withheld .tl-preview-place { border: 1px dashed var(--vb-border); background: transparent; }

/* "Leading this month": the provisional winner and the instant the month is decided. */
.tl-leader { display: flex; align-items: center; gap: 0.9rem; padding: 0.8rem 0.9rem; border-radius: 0.6rem; background: var(--vb-surface-raised); margin: 0.75rem 0; }
.tl-leader .avatar-initials { width: 36px; height: 36px; flex: none; }
.tl-leader-text { display: flex; flex-direction: column; min-width: 0; gap: 0.1rem; }
.tl-leader-figure { margin-left: auto; font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tl-leader-figure small { font-size: 0.75rem; font-weight: 600; color: var(--vb-text-muted); margin-left: 0.2rem; }

/* The previous-winners table's outcome cell: the chip and its one-line reason. */
.tl-outcome { display: flex; flex-direction: column; gap: 0.15rem; }

/* The winners table: a month, a pilot, a flight and a figure never wrap on a desktop; on a phone the
   table is cards (data-cards) and wraps as every card does. */
@media (min-width: 761px) { .data .tl-nowrap { white-space: nowrap; } }

/* The Re-finalise dialog's confirmation (settings changed since the month was decided): the box sits
   apart from the sentence above and the reason below. */
.modal .checkbox-field { margin: 0.35rem 0 1rem; }

/* Custom links (product/83 Phase 1; the LinksManage, LinkEditor and SidebarRoles boards): sections 1 to 3 of
   design/custom-links/proposal-admin.css, merged as they stand. Nothing here restyles a shipped class. */

/* ---- 1. The sidebar's Custom links group -------------------------------------------------------
   A custom link is a .nav-item like any other, so it inherits the row height, the rail and the phone
   rules. The one addition is the glyph that says the row leaves vaBase. */
.nav-item .nav-ext { width: 12px; height: 12px; margin-left: auto; color: var(--vb-border-strong); }
.nav-item:hover .nav-ext, .nav-item:focus-visible .nav-ext { color: var(--vb-text-muted); }
.shell[data-sidebar="rail"] .nav-ext { display: none; }
/* The phone drawer is the sidebar unchanged, even if the rail was chosen on a desktop. */
@media (max-width: 960px) { .shell[data-sidebar="rail"] .shell-sidebar .nav-ext { display: revert; } }

/* ---- 2. Settings > Custom links: the list ------------------------------------------------------- */
.cl-list { padding-bottom: 0.6rem; }
.cl-count { margin-left: auto; font-size: 0.85rem; color: var(--vb-text-muted); }
/* One link: grip, icon, name over its host, who sees it, then the controls (arrows, switch, Edit, menu).
   The row is ReorderList's .reorder-row laid out as a grid so the columns line up down the list. */
.cl-row {
    display: grid;
    grid-template-columns: auto 36px minmax(0, 1.25fr) minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.75rem;
    padding: 0.65rem 0;
}
.cl-row .reorder-grip { padding: 0 0.1rem; }
.cl-row .reorder-grip .icon { width: 16px; height: 16px; display: block; }
.cl-name { min-width: 0; }
.cl-name-line { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.cl-name-line strong { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cl-host { display: block; font-family: var(--vb-mono); font-size: 0.78rem; color: var(--vb-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cl-seen { min-width: 0; }
.cl-seen .row-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cl-controls { display: flex; align-items: center; gap: 0.5rem; justify-content: flex-end; }
.cl-arrows { display: flex; gap: 0.1rem; }
.cl-arrows .btn .icon { width: 15px; height: 15px; }
.cl-row.is-off .integration-card-icon, .cl-row.is-off .cl-name-line strong, .cl-row.is-off .cl-seen { opacity: 0.55; }
/* Where the dragged row will land. */
.cl-row.is-drop-target { box-shadow: inset 0 2px 0 var(--vb-primary-bright); }
.cl-row.is-dragging { background: var(--vb-surface-raised); border-radius: var(--vb-radius-sm); }
.cl-foot { margin: 0.75rem 0 0; }
@media (max-width: 1100px) {
    .cl-row { grid-template-columns: auto 36px minmax(0, 1fr) auto; }
    .cl-seen { grid-column: 3 / -1; grid-row: 2; margin-top: -0.35rem; }
}
/* A phone: the link on the first line, who sees it on the second, the controls on the third. */
@media (max-width: 640px) {
    .cl-row { grid-template-columns: 36px minmax(0, 1fr); row-gap: 0.55rem; padding: 0.85rem 0; }
    .cl-row .reorder-grip { display: none; }
    .cl-seen { grid-column: 1 / -1; grid-row: 2; margin-top: 0; }
    .cl-controls { grid-column: 1 / -1; grid-row: 3; justify-content: flex-start; }
    .cl-controls .toggle-switch { margin-left: auto; order: 5; }
}

/* ---- 3. The add / edit dialog -------------------------------------------------------------------- */
.cl-dialog { max-width: 54rem; }
.cl-editor { display: grid; grid-template-columns: minmax(0, 1fr) 16.5rem; gap: 1.5rem; align-items: start; margin-top: 1rem; }
.cl-editor .field { margin-bottom: 1.05rem; }
.cl-counter { float: right; font-size: 0.78rem; font-weight: 400; color: var(--vb-text-muted); }
/* The icon choice: a grid of the curated icons, one chosen. */
.cl-icons { display: grid; grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); gap: 0.35rem; }
.cl-icon {
    height: 38px;
    border-radius: var(--vb-radius-sm);
    border: 1px solid var(--vb-border);
    background: var(--vb-bg-soft);
    color: var(--vb-text-muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    cursor: pointer;
}
.cl-icon .icon { width: 18px; height: 18px; }
.cl-icon:hover { color: var(--vb-text); border-color: var(--vb-border-strong); }
.cl-icon[aria-checked="true"] { color: var(--vb-primary-bright); border-color: var(--vb-primary); background: var(--vb-primary-soft); }
.cl-icon:focus-visible { outline: 2px solid var(--vb-primary-bright); outline-offset: 2px; }
/* Who sees it: three compact tiles, and the permission areas beneath when the third is chosen. */
.cl-audience { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.4rem; }
.cl-audience label.radio-option { margin: 0; padding: 0.6rem 0.85rem; }
.cl-audience .radio-option-body { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 0.1rem 0.6rem; }
.cl-areas { margin-top: 0.65rem; padding: 0.75rem 0.85rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); }
.cl-areas-group { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--vb-text-muted); margin: 0.6rem 0 0.35rem; }
.cl-areas-group:first-child { margin-top: 0; }
/* The preview: the sidebar's own rows, so the link is seen exactly as it will sit there. */
.cl-preview { position: sticky; top: 0; }
.cl-preview-label { font-size: 0.8rem; font-weight: 600; color: var(--vb-text-muted); margin: 0 0 0.4rem; }
.cl-preview-pane { background: var(--vb-bg); border: 1px solid var(--vb-border); border-radius: var(--vb-radius); padding: 0.75rem 0.6rem; }
.cl-preview-pane .nav-group-label { margin-top: 0; }
.cl-preview-pane .nav-item.is-context { opacity: 0.4; }
.cl-preview-pane .nav-item.is-this { color: var(--vb-text); background: var(--vb-surface); }
.cl-preview-pane .nav-item.is-placeholder .nav-item-label { color: var(--vb-text-muted); font-style: italic; }
.cl-preview-note { font-size: 0.8rem; color: var(--vb-text-muted); margin: 0.6rem 0 0; overflow-wrap: anywhere; }
.cl-preview-note .icon { width: 13px; height: 13px; vertical-align: -2px; margin-right: 0.3rem; }
@media (max-width: 760px) {
    .cl-editor { grid-template-columns: minmax(0, 1fr); }
    .cl-preview { position: static; }
}

/* ---- 5. Login with vaBase: the confirmation and the sign-ins that cannot continue (board 5) --------- */
.sso-page { max-width: 29rem; margin: 2.25rem auto 0; }
.sso-page .card { padding: 1.6rem 1.6rem 1.4rem; }
.sso-page .card:hover { transform: none; }
/* The airline and the application it registered, side by side: who is asking, on whose behalf. */
.sso-pair { display: flex; align-items: center; justify-content: center; gap: 0.9rem; margin-bottom: 1.1rem; }
.sso-tile { width: 52px; height: 52px; border-radius: 13px; display: inline-flex; align-items: center; justify-content: center; background: var(--vb-surface-raised); border: 1px solid var(--vb-border-strong); font-weight: 700; font-size: 1.05rem; color: var(--vb-text); }
.sso-tile .icon { width: 24px; height: 24px; color: var(--vb-primary-bright); }
.sso-tile-airline { background: var(--vb-primary); border-color: var(--vb-primary); color: var(--vb-on-primary); }
.sso-pair-arrow { color: var(--vb-text-muted); }
.sso-pair-arrow .icon { width: 18px; height: 18px; display: block; }
.sso-page h1 { font-size: 1.35rem; text-align: center; margin: 0 0 0.3rem; }
.sso-sub { text-align: center; color: var(--vb-text-muted); margin: 0 0 1.2rem; font-size: 0.92rem; }
.sso-sub .mono { color: var(--vb-text); }
.sso-who { display: flex; align-items: center; gap: 0.7rem; padding: 0.7rem 0.8rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); margin-bottom: 1.1rem; }
.sso-who-text { min-width: 0; flex: 1; }
.sso-who-text strong { display: block; }
.sso-who-text span { display: block; font-size: 0.82rem; color: var(--vb-text-muted); overflow-wrap: anywhere; }
.sso-shared-title { font-size: 0.8rem; font-weight: 600; color: var(--vb-text-muted); margin: 0 0 0.45rem; }
.sso-shared { list-style: none; margin: 0 0 1.1rem; padding: 0; display: grid; gap: 0.5rem; }
.sso-shared li { display: flex; align-items: flex-start; gap: 0.6rem; font-size: 0.9rem; }
.sso-shared .icon { width: 16px; height: 16px; flex: none; margin-top: 0.2rem; color: var(--vb-text-muted); }
.sso-shared li.is-new .icon { color: var(--vb-primary-bright); }
.sso-shared .tag-new { margin-left: 0.45rem; }
.sso-shared .muted { display: block; font-size: 0.8rem; }
.sso-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); gap: 0.6rem; }
.sso-actions .btn { width: 100%; justify-content: center; }
.sso-foot { font-size: 0.8rem; color: var(--vb-text-muted); text-align: center; margin: 1rem 0 0; }
/* A sign-in that cannot continue: said on vaBase's own page, never by sending the reader back to an
   address vaBase has not verified. */
.sso-problem { text-align: center; }
.sso-problem .empty-state-icon { width: 52px; height: 52px; margin: 0 auto 1.1rem; }
.sso-problem p { color: var(--vb-text-muted); margin: 0 0 1.1rem; font-size: 0.92rem; }
.sso-problem .sso-actions { grid-template-columns: minmax(0, 1fr); max-width: 16rem; margin: 0 auto; }
.sso-ref { font-family: var(--vb-mono); font-size: 0.78rem; color: var(--vb-text-muted); margin: 1rem 0 0; }

/* ==== Two-factor sign-in and the Connected application selector (product/84 sections 5, 6 and 8; the
   approved design/two-factor boards). Nothing here restyles a shipped class. ==== */

/* ---- 1. The code step on the Admin Portal's sign-in -----------------------------------------
   The same input as the Account Portal's: large, spaced, centred digits. */
.field input.otp-input { font-family: var(--vb-mono); font-size: 1.55rem; letter-spacing: 0.42em; text-align: center; padding: 0.45rem 0.5rem 0.45rem 0.92rem; }
.field input.recovery-input { font-family: var(--vb-mono); font-size: 1.15rem; letter-spacing: 0.12em; text-align: center; text-transform: uppercase; }
.tf-switch { text-align: center; margin: 0.9rem 0 0; font-size: 0.88rem; }
.tf-lead { margin: 0 0 0.9rem; font-size: 0.92rem; }
.auth-page .tf-who { font-size: 0.85rem; color: var(--vb-text-muted); margin: 0 0 1rem; }

/* ---- 2. Staff: the Two-factor column and the member's facts ------------------------------------
   The cell is a pill (the word inside it) over a quiet line; the pill's own margin never matters
   because the line is a block. */
.tf-cell .row-sub { margin-top: 0.25rem; }
.tf-required-note { margin: 0 0 1rem; }
.tf-member { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.7rem; padding: 0.7rem 0.85rem; border: 1px solid var(--vb-border); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); margin: 0 0 1rem; font-size: 0.88rem; }
.tf-member strong { font-weight: 600; }
.tf-member .muted { flex: 1 1 16rem; min-width: 0; }
/* An Owner's confirmation that a lost-device request really is the person (product/84 section 5.5). */
.tf-identity { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.4rem 0.85rem; align-items: center; }
.tf-identity > .icon { width: 20px; height: 20px; color: var(--vb-primary-bright); }
.tf-identity p { margin: 0; }
.tf-identity .muted { font-size: 0.85rem; }
.tf-identity-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
@media (max-width: 760px) {
    .tf-identity { grid-template-columns: auto minmax(0, 1fr); }
    .tf-identity-actions { grid-column: 1 / -1; }
    .tf-identity-actions .btn { flex: 1 1 auto; }
}

/* ---- 3. The Custom link dialog: the Connected application selector (ruling 24) ----------------
   When an application is chosen the address is its start address: shown, never typed. */
.cl-address-locked { display: flex; align-items: center; gap: 0.5rem; padding: 0.55rem 0.8rem; border: 1px dashed var(--vb-border-strong); border-radius: var(--vb-radius-sm); background: var(--vb-bg-soft); color: var(--vb-text-muted); font-family: var(--vb-mono); font-size: 0.85rem; overflow-wrap: anywhere; }
.cl-address-locked .icon { width: 14px; height: 14px; flex: none; }
.cl-app-note { margin: 0.6rem 0 0; }
.cl-preview-note.is-warning .icon { color: var(--vb-warning); }

/* ---- 4. The SSO screens: what the two-factor step adds --------------------------------------- */
.sso-page .sso-when { display: block; font-size: 0.8rem; color: var(--vb-text-muted); }
.sso-problem .sso-actions-2 { grid-template-columns: minmax(0, 1fr); max-width: 18rem; gap: 0.5rem; }
.sso-problem .sso-note { font-size: 0.82rem; margin: 1rem 0 0; }
/* Staff: the co-Owner request card sits above the roster note. */
.tf-identity-card { margin-bottom: 1rem; }
/* The SSO problem page: its sign-out form fills the column like the link beside it. */
.sso-action-wide { width: 100%; justify-content: center; }
/* The fresh sign-in an application asks for: when the session began, under the person. */
.auth-page .sso-when { display: block; font-size: 0.8rem; color: var(--vb-text-muted); }

/* ==== Login with vaBase: Integrations > Developer > Applications (product/83 board 3, approved;
   design/custom-links/proposal-admin.css section 4). ==== */
.ca-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 1.25rem; align-items: start; }
.ca-main, .ca-side { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem; min-width: 0; }
.ca-grid .card { margin: 0; }
.ca-grid .card:hover { transform: none; }
.ca-card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.75rem; }
.ca-card-head h2 { font-size: 1.05rem; margin: 0; }
.ca-copy { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.ca-copy .mono { min-width: 0; overflow-wrap: anywhere; }
/* Redirect addresses: one per row, exact matches only. */
.ca-uris { display: grid; gap: 0.4rem; }
.ca-uri { display: flex; align-items: center; gap: 0.4rem; }
.ca-uri input { flex: 1; min-width: 0; font-family: var(--vb-mono); font-size: 0.85rem; }
.ca-uri-read { font-family: var(--vb-mono); font-size: 0.82rem; padding: 0.4rem 0; border-top: 1px solid var(--vb-border); overflow-wrap: anywhere; }
.ca-uri-read:first-child { border-top: none; padding-top: 0; }
/* What the application may be told: one always-on line and the optional scopes. */
.ca-scopes { display: grid; gap: 0.55rem; }
.ca-scope { display: flex; align-items: flex-start; gap: 0.6rem; font-size: 0.9rem; }
.ca-scope input { margin-top: 0.25rem; flex: none; }
.ca-scope .muted { display: block; font-size: 0.8rem; }
.ca-scope .icon { width: 16px; height: 16px; flex: none; margin-top: 0.2rem; color: var(--vb-text-muted); }
.ca-steps { margin: 0; padding-left: 1.2rem; display: grid; gap: 0.45rem; font-size: 0.9rem; }
@media (max-width: 1100px) { .ca-grid { grid-template-columns: minmax(0, 1fr); } }

/* ---- Login with vaBase: what the built Applications screens add to board 3's classes ------------------
   Small spacing rules the board drew inline, given names so the page carries no inline styles. */
/* The form's "+ Add an address" sits a little below the last row. */
.ca-add-uri { margin-top: 0.5rem; }
/* The copy confirmation beside a copied value. */
.ca-copied { font-size: 0.8rem; flex: none; }
/* The guide's issuer and discovery address, and its download row. */
.ca-guide-facts { margin: 0 0 0.9rem; }
.ca-guide-links { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.9rem; margin-top: 0.9rem; font-size: 0.88rem; }
/* One application: the disabled notice and the new secret sit above the two columns. */
.ca-disabled-note { margin: 0 0 1.25rem; }
.ca-issued { margin-bottom: 1.25rem; }
.ca-card-note { font-size: 0.85rem; }
.ca-none { margin: 0; }
.ca-secret-facts { grid-template-columns: 7rem minmax(0, 1fr); }
.ca-who { margin: 0; }
.ca-who-areas { margin: 0.35rem 0 0; font-size: 0.88rem; }
/* A custom link and where it shows: spaced by the gap, never by a typed space. */
.ca-link-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.45rem; margin: 0 0 0.35rem; }
/* Staff: the recovery checklist in the Owner's authorise dialog (product/84 5.5). */
.tf-checklist-head { display: block; font-weight: 600; font-size: 0.88rem; margin-bottom: 0.45rem; }
label.tf-statement { display: flex; align-items: flex-start; gap: 0.6rem; font-size: 0.9rem; margin: 0 0 0.5rem; cursor: pointer; }
.tf-statement input { margin: 0.2rem 0 0; flex: none; }
/* Developer: the link to the public example beside the register form; spaced by the gap. */
.ca-example-link { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.6rem; margin: 0 0 1rem; font-size: 0.9rem; }
.ca-example-link a { display: inline-flex; align-items: center; gap: 0.3rem; font-weight: 600; }

/* ---- product/88: flight details on tour legs and events, and the flight-number composer ---- */
/* ---- The flight-number composer (FlightNumberField.razor: schedule create/edit, tour leg, event) ----
   The marketing designator is a fixed lead-in joined to the digits box, so the box only ever takes the
   part a person types. The preview under it is the whole number exactly as it will be stored. */
.fn-composer { display: flex; align-items: stretch; min-width: 0; }
.fn-designator {
    display: inline-flex;
    align-items: center;
    flex: none;
    padding: 0 0.8rem;
    border: 1px solid var(--vb-border-strong);
    border-right: none;
    border-radius: var(--vb-radius-sm) 0 0 var(--vb-radius-sm);
    background: var(--vb-surface-raised);
    color: var(--vb-text-muted);
    font-family: var(--vb-mono);
    font-size: 0.9rem;
    font-weight: 600;
    letter-spacing: 0.02em;
}
.field .fn-composer input { border-top-left-radius: 0; border-bottom-left-radius: 0; min-width: 0; flex: 1; }
.fn-composer.is-invalid .fn-designator { border-color: var(--vb-danger); }
/* "Shows as FW4029": a quiet line with the number as a code pill, spaced by the gap (never a typed space). */
.fn-preview { display: flex; align-items: center; flex-wrap: wrap; gap: 0.45rem; margin-top: 0.45rem; font-size: 0.82rem; color: var(--vb-text-muted); }
.fn-preview-value {
    font-family: var(--vb-mono);
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--vb-text);
    padding: 0.08rem 0.55rem;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--vb-primary) 45%, transparent);
    background: var(--vb-primary-soft);
}
.fn-preview-none { font-style: italic; }
/* The custom-number switch sits directly under the composer, inside the same field, so it reads as
   "or type it yourself" rather than a separate setting. */
.fn-custom.switch-row { border-top: none; padding: 0.55rem 0 0; }

/* ---- Flight details: one bordered group inside a form (tour leg editor, event form) ----
   A group, not a card: it lives inside a card already. The heading says it is optional and the hint says
   what "set" means for a pilot; every field inside is the shared field markup. */
.flight-details {
    margin: 1.1rem 0 0.4rem;
    padding: 0.95rem 1.1rem 0.2rem;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    background: color-mix(in srgb, var(--vb-surface-raised) 60%, transparent);
}
.flight-details-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.2rem; }
.flight-details-head h3, .flight-details-head h4 { margin: 0; font-size: 0.95rem; font-weight: 650; }
.flight-details-head .muted { font-size: 0.82rem; }
.flight-details > .hint:first-of-type { margin: 0 0 0.9rem; }

/* ---- The tour leg editor: the row being edited, and the panel opened beneath it ----
   One editor at a time, as the description and route editors are. The row stays visible above the
   panel and is marked with the existing is-selected tint, so the admin sees which leg they are changing. */
.leg-editor { padding: 0.4rem 0.2rem 0.3rem; }
.leg-editor-title { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 0.9rem; font-size: 1rem; font-weight: 650; }
.leg-editor-title .muted { font-weight: 400; font-size: 0.88rem; }
.leg-editor .card-actions { justify-content: flex-start; margin-top: 0.9rem; }
.leg-editor .card-actions .hint { margin: 0 0 0 auto; align-self: center; }
/* The Flight column: the configured number, its type underneath, or "Not set". */
.leg-flight { display: flex; flex-direction: column; gap: 0.1rem; white-space: nowrap; }
.leg-flight .muted { font-size: 0.8rem; }
/* On a phone the legs table scrolls sideways inside .table-wrap. The editor would then be as wide as
   the table; holding it to the visible width and pinning it left keeps the whole form on screen. */
@media (max-width: 640px) {
    .leg-editor { position: sticky; left: 0; width: calc(100vw - 6rem); max-width: 100%; box-sizing: border-box; }
    .leg-editor .card-actions .hint { margin: 0.4rem 0 0; flex-basis: 100%; }
    .flight-details { padding: 0.85rem 0.85rem 0.1rem; }
}

/* ---- Flight numbering (SettingsFlightNumbering.razor) ----
   product/88 section 3.1. The rules table's Example and In use cells, and the add/edit rule form: a bordered group inside
   the Rules card, the same weight as the Flight details group in the leg editor. */
/* "FW0100 to FW0999": two code values and a quiet word, spaced by the gap. */
.numbering-example { display: inline-flex; align-items: baseline; gap: 0.35rem; white-space: nowrap; }
/* "94 of 99" and, from 90%, a Nearly full or Full pill beside it - spaced by the gap, never a typed space. */
.numbering-use { display: inline-flex; align-items: center; gap: 0.45rem; white-space: nowrap; }
/* The range stays on one line; the flight type and prefix may wrap so the row fits the card at 1280. On a
   phone the table scrolls sideways inside .table-wrap, as every admin table does. */
.numbering-table td:nth-child(3) { white-space: nowrap; }
.numbering-form {
    margin: 1rem 0 0.4rem;
    padding: 0.95rem 1.1rem 0.4rem;
    border: 1px solid var(--vb-border);
    border-radius: var(--vb-radius-sm);
    background: color-mix(in srgb, var(--vb-surface-raised) 60%, transparent);
}
.numbering-form-title { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 0.9rem; font-size: 1rem; font-weight: 650; }
.numbering-form-title .muted { font-weight: 400; font-size: 0.88rem; }
.numbering-form .card-actions { justify-content: flex-start; margin-top: 0.4rem; }
.numbering-form .card-actions .hint { margin: 0 0 0 auto; align-self: center; }
/* A rule's flight type once it exists: plain text where the select was. */
.numbering-fixed { margin: 0.35rem 0 0; font-weight: 600; }
.numbering-add { justify-content: flex-start; margin-top: 0.9rem; }
.numbering-add-empty { justify-content: center; margin-top: 0; }
@media (max-width: 640px) {
    .numbering-form { padding: 0.85rem 0.85rem 0.3rem; }
    .numbering-form .card-actions .hint { margin: 0.4rem 0 0; flex-basis: 100%; }
}

/* ---- SimBrief planning defaults (SettingsSimBrief.razor, PlanningLock.razor) ----
   product/88 section 4.2: one row per value with its lock beside it; a pill beside text spaced by the gap. */
.badge-line { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
.planning-list { margin-top: 0.6rem; }
.planning-row { display: grid; grid-template-columns: minmax(0, 1fr) 13rem; gap: 0.3rem 1.5rem; align-items: start; padding: 0.85rem 0; border-top: 1px solid var(--vb-border); }
.planning-row:first-child { border-top: 0; }
.planning-row .field { margin: 0; }
.planning-control { display: flex; align-items: center; gap: 0.6rem; max-width: 28rem; }
.planning-control > input, .planning-control > select { flex: 1; min-width: 0; }
.planning-control .planning-unit { flex: 0 0 8rem; }
.planning-lock { display: flex; align-items: center; gap: 0.6rem; padding-top: 1.75rem; font-size: 0.85rem; color: var(--vb-text-muted); }
.planning-lock-on { color: var(--vb-text); font-weight: 600; }
@media (max-width: 640px) {
    .planning-row { grid-template-columns: minmax(0, 1fr); }
    .planning-lock { padding-top: 0.2rem; }
}
