/* ── Skip navigation link ────────────────────────────────── */

.skip-link {
    position: absolute;
    top: -100%;
    left: 0;
    z-index: 1100;
    padding: 0.75rem 1.5rem;
    background-color: var(--bs-primary);
    color: #fff;
    font-weight: 600;
    text-decoration: none;
    border-radius: 0 0 0.375rem 0;
}
.skip-link:focus {
    top: 0;
    color: #fff;
    outline: 3px solid var(--bs-secondary);
    outline-offset: 2px;
}

/* ── Sticky footer (flexbox) ─────────────────────────────── */

body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    /* Positioning ancestor so body::before's z-index: -1 stays
       contained to the body's stacking context. Without this, a
       child with its own stacking context could float above the
       atmosphere layer in unexpected orders. */
    position: relative;
}

/* ── Atmosphere layer ─────────────────────────────────────
   Fixed-position radial gradient anchored above the viewport
   so only the bottom half of the bleed shows up — soft brand-
   green glow at the top of every page, fading to transparent
   well before the content reading area. Alpha pinned low
   enough that axe-core contrast calculations are unaffected
   (text bg resolves to body's opaque background; this pseudo
   isn't an ancestor of any text node). Pointer-events: none
   keeps it out of hit-testing; z-index: -1 keeps it behind
   every body child. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: -1;
    background:
        radial-gradient(ellipse 80% 60% at 50% -20%,
            rgba(0, 112, 85, 0.06),
            transparent 65%);
}

[data-bs-theme=dark] body::before {
    /* Slightly higher alpha + the lighter dark-mode brand green so
       the glow registers against #212529. The lighter shade matches
       what .text-brand uses in dark mode. */
    background:
        radial-gradient(ellipse 80% 60% at 50% -20%,
            rgba(92, 188, 148, 0.08),
            transparent 65%);
}

body > .container-lg,
body > .container-fluid,
body > .container-xl,
body > .container-md,
body > .container-sm,
body > [class*="container"] {
    flex: 1;
}

.footer {
    width: 100%;
    padding: 1rem 0;
    /* Brand gradient on the top edge brackets the page with the same
       motif that anchors the navbar bottom — green→gold sweep top and
       bottom of every page. border-image overrides the .border-top
       utility class that the helper still emits (Bootstrap's plain
       1px border-color); when border-image-source is set, the slice
       defines what fills the border area instead of border-*-color.
       Use a 3px border so the gradient registers visually rather than
       reading as a hairline. */
    border-top: 3px solid;
    border-image-source: var(--capstone-brand-gradient);
    border-image-slice: 1;
}

.footer p {
    margin-bottom: 0.25rem;
}

.footer-links {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.25rem 0;
    font-size: 0.85rem;
}

.footer-links a {
    text-decoration: underline;
}

.footer-separator {
    margin: 0 0.5rem;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-secondary-color);
}

/* ── User menu (Variant D — Green Banner) ───────────────── */

/* Open / close animation for both the authenticated menu (.um-drop) and the
   standalone theme dropdown (.um-theme-dropdown) is pure CSS. Bootstrap only
   toggles `.show` (display:none ↔ block); `@starting-style` supplies the
   "from" values for the open transition, and `transition: display …
   allow-discrete` keeps the element painted until the close transition
   finishes. Engines without those features (pre-2024) skip the animation and
   snap — no JS fallback, no hide.bs.dropdown interception (the old
   interception could wedge the menu open if transitionend never fired).
   Everything animated lives under prefers-reduced-motion: no-preference. */
.dropdown-menu.um-drop {
    width: 260px;
    padding: 0;
    overflow: hidden;
    border-radius: 14px;
    border: 1px solid var(--bs-border-color);
    transform-origin: top right;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.1);
}

.dropdown-menu.um-theme-dropdown {
    min-width: 200px;
    border-radius: 10px;
    transform-origin: top right;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.1);
}

.dropdown-menu.um-drop,
.dropdown-menu.um-theme-dropdown {
    opacity: 0;
    transform: translateY(-6px) scale(0.97);
}

.dropdown-menu.um-drop.show,
.dropdown-menu.um-theme-dropdown.show {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: no-preference) {
    .dropdown-menu.um-drop,
    .dropdown-menu.um-theme-dropdown {
        /* Close: the plain form first for engines that reject allow-discrete
           in the shorthand, then the discrete form that keeps display:block
           for the duration. */
        transition: opacity 0.15s ease-in, transform 0.15s ease-in;
        transition: opacity 0.15s ease-in, transform 0.15s ease-in,
                    display 0.15s allow-discrete, overlay 0.15s allow-discrete;
    }

    .dropdown-menu.um-drop.show,
    .dropdown-menu.um-theme-dropdown.show {
        transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                    transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    }

    @starting-style {
        .dropdown-menu.um-drop.show,
        .dropdown-menu.um-theme-dropdown.show {
            opacity: 0;
            transform: translateY(-8px) scale(0.96);
        }
    }
}

.um-trigger {
    display: flex;
    align-items: center;
    padding: 0.25rem 0.5rem;
    line-height: 1;
    position: relative;
    overflow: hidden;
    border-radius: 1.5rem;
}

.um-trigger::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(circle at var(--ripple-x, 50%) var(--ripple-y, 50%),
                rgba(var(--bs-primary-rgb, 0, 112, 85), 0.12) 0%,
                transparent 70%);
    opacity: 0;
    transform: scale(0.6);
    pointer-events: none;
}

.um-trigger:hover::after {
    opacity: 1;
    transform: scale(1);
}

@media (prefers-reduced-motion: no-preference) {
    .um-trigger {
        transition: transform 0.1s ease;
    }

    .um-trigger::after {
        transition: opacity 0.35s ease, transform 0.4s ease;
    }

    .um-trigger:active {
        transform: scale(0.97);
    }
}

/* Green gradient banner. Both stops must keep white text ≥ 4.5:1 — the
   name is 14px and the email/account rows are 12px, none of it "large".
   --bs-primary (#007055) is 6.09:1; the deeper --brand-on-secondary
   (#004633) is higher still. (An earlier build faded toward #0f9a4e, a
   lighter non-brand green where white text fell to ~3.65:1.) */
.um-banner {
    background: linear-gradient(135deg, var(--bs-primary), var(--brand-on-secondary));
    padding: 1rem;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    cursor: default;
    /* Positioning ancestor for the ::after brand stripe below. The
       .um-drop parent has overflow: hidden + border-radius, so the
       stripe is clipped cleanly at the banner's rounded top corners. */
    position: relative;
}

/* Brand-gradient stripe along the banner's bottom edge — same motif
   used under the primary navbar and along the footer's top edge so
   the user-identity surface joins the page-bracket family. The
   --capstone-brand-gradient (green→gold) can't be the banner's field
   because white text on SRU gold = 1.49:1 and fails WCAG AA; the
   stripe is a 3px accent so the gold appears as a sliver of brand
   reference without touching the banner's white-on-green contrast. */
.um-banner::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    background: var(--capstone-brand-gradient);
    pointer-events: none;
}

.um-name {
    font-size: 0.875rem;
    /* Lato Black (900) — the heaviest weight loaded by <capstone-head>.
       The user banner is the identity moment ("this is YOU"); the extra
       weight pulls the display name forward over the smaller email +
       account-settings rows below it without leaning on Fjalla One,
       which is reserved for marquee-sized headings (h1/h2/.display-*). */
    font-weight: 900;
    color: #fff;
    line-height: 1.2;
}

/* 12px floor for the secondary rows — small enough to defer to the name,
   large enough to read without leaning in. */
.um-email {
    font-size: 0.75rem;
    color: rgba(255, 255, 255, 0.88);
    margin-top: 0.2rem;
    overflow-wrap: anywhere;
}

.um-account {
    display: inline-block;
    font-size: 0.75rem;
    color: rgba(255, 255, 255, 0.88);
    text-decoration: underline;
    margin-top: 0.15rem;
    border-radius: 0.25rem;
}

.um-account:hover {
    color: #fff;
    text-decoration: underline;
}

.um-account:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* User avatar (populated via HttpContext.Items by Primer or consuming app) */
@keyframes um-avatar-shimmer {
    0%   { background-position: 200% center; }
    100% { background-position: -200% center; }
}

.um-avatar-wrap {
    display: inline-flex;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    flex-shrink: 0;
    vertical-align: middle;
    overflow: hidden;
    background: linear-gradient(90deg,
/* ReSharper disable once CssNotResolved */
/* ReSharper disable once InvalidValue */
        var(--bs-secondary-bg) 25%,
/* ReSharper disable once CssNotResolved */
        var(--bs-tertiary-bg) 50%,
/* ReSharper disable once CssNotResolved */
        var(--bs-secondary-bg) 75%);
    background-size: 200% 100%;
}

/* The shimmer sweep is an INFINITE animation — exactly the category that
   matters for vestibular-disorder users, so it only runs when the user
   hasn't asked for reduced motion. Reduced-motion users keep the static
   gradient as the loading placeholder (the skeleton still reads as
   "not loaded yet"; only the sweep is dropped). */
@media (prefers-reduced-motion: no-preference) {
    .um-avatar-wrap {
        animation: um-avatar-shimmer 1.4s ease-in-out infinite;
    }
}

/* capstone-theme.js adds .um-loaded once the <img> has loaded (or removes
   the <img> on error, leaving the wrap as a neutral disc). Keyed off a class
   rather than inline onload= handlers so the avatar survives a CSP that
   forbids inline script. */
.um-avatar-wrap.um-loaded {
    animation: none;
    background: none;
}

.um-banner .um-avatar-wrap {
    width: 60px;
    height: 60px;
    border: 2px solid rgba(255, 255, 255, 0.3);
    background: linear-gradient(90deg,
        rgba(255, 255, 255, 0.1) 25%,
        rgba(255, 255, 255, 0.25) 50%,
        rgba(255, 255, 255, 0.1) 75%);
    background-size: 200% 100%;
}

.um-avatar {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
    opacity: 0;
    transition: opacity 0.2s ease;
}

.um-avatar-wrap.um-loaded .um-avatar {
    opacity: 1;
}

/* Menu items */
.um-items {
    padding: 0.375rem;
}

.um-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.625rem;
    border-radius: 0.5rem;
    font-size: 0.8125rem;
    font-weight: 500;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
    text-decoration: none;
    transition: background-color 0.12s, color 0.12s, border-color 0.12s;
    cursor: pointer;
    border: none;
    border-left: 2px solid transparent;
    background: none;
    width: 100%;
    text-align: left;
}

/* Hover/focus fill is Bootstrap's own dropdown-item hover token — theme-aware
   without needing color-mix(), which has no fallback path in Safari < 16.2. */
.um-item:hover,
.um-item:focus-visible {
    /* ReSharper disable once CssNotResolved */
    background-color: var(--bs-tertiary-bg);
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
    text-decoration: none;
    border-left-color: var(--bs-primary);
}

.um-item:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
}

/* Sign out: the destructive item shifts to the theme-aware invalid tokens
   (the same pair the validation rules mandate) so the red text clears AA in
   both themes — a fixed #f87171 on the light hover tint was ~2.8:1. */
.um-danger:hover,
.um-danger:focus-visible {
    /* ReSharper disable once CssNotResolved */
    background-color: var(--bs-danger-bg-subtle);
    /* ReSharper disable once CssNotResolved */
    /* ReSharper disable once InvalidValue */
    background-color: color-mix(in srgb, var(--bs-form-invalid-color) 10%, transparent);
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-form-invalid-color);
    /* ReSharper disable once CssNotResolved */
    border-left-color: var(--bs-form-invalid-color);
}

.um-danger:focus-visible {
    /* ReSharper disable once CssNotResolved */
    outline-color: var(--bs-form-invalid-color);
}

/* Divider */
.um-div {
    height: 1px;
    /* ReSharper disable once CssNotResolved */
    background: var(--bs-border-color-translucent);
    margin: 0.125rem 0.375rem;
}

/* ── Theme segmented toggle ─────────────────────────────── */

.um-theme-section {
    padding: 0.5rem 0.625rem 0.625rem;
}

.um-theme-label {
    font-size: 0.6875rem;
    font-weight: 600;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-secondary-color);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    margin-bottom: 0.4rem;
}

.um-theme-toggle {
    display: flex;
    position: relative;
    border-radius: 0.5rem;
    padding: 3px;
    gap: 2px;
    background-color: rgba(0, 0, 0, 0.06);
    border: 1px solid rgba(0, 0, 0, 0.06);
}

.um-theme-pill {
    position: absolute;
    top: 3px;
    height: calc(100% - 6px);
    border-radius: 0.375rem;
    background-color: var(--bs-primary);
    /* Shadow halo tracks --bs-primary so it stays brand-aligned if a
       consumer ever overrides the brand color. */
    box-shadow: 0 2px 10px rgba(var(--bs-primary-rgb), 0.3);
    z-index: 0;
    pointer-events: none;
}

/* Slide between active buttons only when the user hasn't asked for reduced
   motion. Without this guard, the first dropdown open animates the pill
   from its hidden-parent (0, 0) layout to the active button's position,
   visually crossing the non-active buttons mid-transition. axe-core 4.x
   detects that overlap and reports a contrast failure on whichever button
   the pill is sliding past (WCAG 2.3.3 also applies — motion should be
   opt-in). The JS still snaps left/width to the active button's offsets;
   reduced-motion users get an instant snap instead of a 0.25s slide. */
@media (prefers-reduced-motion: no-preference) {
    .um-theme-pill {
        transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
    }
}

[data-bs-theme=dark] .um-theme-toggle {
    background-color: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 255, 255, 0.06);
}

.um-theme-btn {
    position: relative;
    z-index: 1;
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    padding: 0.375rem 0.3rem;
    border-radius: 0.375rem;
    border: none;
    cursor: pointer;
    font-size: 0.72rem;
    font-weight: 500;
    transition: color 0.15s ease, background-color 0.15s ease;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-secondary-color);
    background: transparent;
}

.um-theme-btn:hover:not(.active) {
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
    /* ReSharper disable once CssNotResolved */
    background-color: var(--bs-tertiary-bg);
}

.um-theme-btn:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

.um-theme-btn i {
    font-size: 0.75rem;
}

.um-theme-btn.active {
    /* Solid green background on the button itself rather than only on the
       sibling .um-theme-pill underneath. axe-core's contrast algorithm
       walks ancestors for the background color — it can't see the pill
       because it's a sibling, not a parent, and would otherwise resolve
       to the toggle's near-white tinted bg (white text → 1.13:1 fail).
       The pill still slides for the active-button transition; the two
       greens coincide when the slide completes.
       ReSharper disable once CssNotResolved */
    background-color: var(--bs-primary) !important;
    color: #fff !important;
    box-shadow: none;
}

.um-theme-btn.active:focus-visible {
    outline-color: var(--bs-body-color);
}

/* ── Alternate header spacer ────────────────────────────── */

.alt-header-spacer {
    width: 38px;
    flex-shrink: 0;
}

/* ── SRU navbar emblem sizing ────────────────────────────── */

/* The emblem is an SVG (476×148 viewBox) so it has no useful intrinsic
   pixel size — pin the height to the 34px the old PNG rendered at and
   let the width follow the aspect ratio. The width/height attributes
   the helper emits only reserve the box before load (no CLS). */
.capstone-navbar-emblem {
    height: 34px;
    width: auto;
}

/* ── SRU seal image sizing ───────────────────────────────── */

.sruSeal {
    width: 48px;
    height: 48px;
}

/* ── Header text size ────────────────────────────────────── */

.text-header {
    /* The alternate-header branding is a <p>, not an <h1> (the page owns
       its h1), so it doesn't pick up the h1/h2 display rule in
       capstone-bootstrap.css — set the display face here explicitly.
       Bumped from 2em → 2.25rem so the marquee branding earns its
       seat between the altNavBar's gradient brackets. Tightened
       line-height because Fjalla One's condensed letterforms wrap
       awkwardly at Bootstrap's default 1.5. */
    font-family: var(--capstone-display-font-family);
    letter-spacing: 0.01em;
    font-weight: 500;
    font-size: 2.25rem;
    line-height: 1.1;
    user-select: none;
    /* Brand green in light mode (6.1:1 on body bg). The .text-primary class
       on Bootstrap uses --bs-primary directly which fails dark-mode
       contrast (2.53:1, dark green on #212529), so we set color here. */
    color: var(--bs-primary);
}

[data-bs-theme=dark] .text-header {
    /* Brand #007055 fails on dark bg (2.53:1, below 3:1 large-text AA).
       Use a vivid emerald (4.58:1) so the header still reads as green
       rather than washing out to a pale sage. */
    color: #00a86b;
}

/* ── Capstone page title (opt-in ornament) ────────────────
   Drop .capstone-page-title onto a page-level <h1> to get
   the display font (already covered by the global h1/h2 rule
   in capstone-bootstrap.css) plus a gradient accent rule that
   anchors the heading visually. Opt-in rather than applied to
   every h1 globally so existing pages don't suddenly grow
   ornaments under headings that were never designed for it
   (e.g. the alternate-header branding sits between altNavBar
   borders that already do this job).
   Hairline width pinned to a short visual unit rather than
   the heading's text width so it reads as an ornament, not
   an underline. */
.capstone-page-title {
    font-size: clamp(2rem, 1.5rem + 1.5vw, 2.75rem);
    line-height: 1.1;
    color: var(--bs-primary);
    margin-bottom: 1.25rem;
    position: relative;
    padding-bottom: 0.625rem;
}

[data-bs-theme=dark] .capstone-page-title {
    color: #5cbc94;
}

.capstone-page-title::after {
    content: '';
    display: block;
    width: 3rem;
    height: 3px;
    margin-top: 0.625rem;
    background: var(--capstone-brand-gradient);
    border-radius: 999px;
    /* aria-hidden by virtue of being decorative content — screen readers
       skip ::after pseudo-elements by default. */
}

/* ── BootstrapPrompts component ─────────────────────────── */

.message-card {
    border: 1px solid var(--bs-border-color);
    /* ReSharper disable once CssNotResolved */
    background: var(--bs-body-bg);
    overflow: hidden;
    opacity: 0;
    /* Paired with opacity:0 so the card is fully out of the layout AND
       the accessibility tree when it's faded out. axe-core (and DevTools)
       skip color-contrast checks on visibility:hidden, which avoids a
       false-positive: cards sitting at style="opacity: 0" inline after
       the auto-fade timer fires (and before .remove() runs ~100ms later)
       otherwise get flagged for alpha-composited text contrast against
       the body bg. */
    visibility: hidden;
}

.message-card.shown {
    opacity: 1;
    visibility: visible;
}

/* Fade-in/out only when the user hasn't asked for reduced motion. Without
   this guard, axe scans mid-fade alpha-blend white-ish text against the
   dark body bg and report 3:1 contrast on what is actually correct markup
   (WCAG 2.3.3 also applies — motion should be opt-in). The JS toggles the
   .shown class; reduced-motion users get an instant show/hide.
   Visibility uses a directional delay: 0s when fading in (so the fade is
   visible from frame 1), 0.4s when fading out (so visibility:hidden only
   takes effect after the opacity transition completes). */
@media (prefers-reduced-motion: no-preference) {
    .message-card {
        transition: opacity 0.4s ease, visibility 0s linear 0.4s;
    }
    .message-card.shown {
        transition: opacity 0.4s ease, visibility 0s linear 0s;
    }
}

.message-card .btn-close {
    /* Visible at rest: touch users have no hover, and an invisible dismiss
       left them waiting on the auto-fade. Hover/focus brings it to full. */
    opacity: 0.5;
    transition: opacity 0.2s ease;
}

.message-card:hover .btn-close,
.message-card:focus-within .btn-close,
.message-card .btn-close:focus-visible {
    opacity: 1;
}

.message-card-accent {
    width: 5px;
    flex-shrink: 0;
    background-color: var(--message-card-accent);
}

.message-card-icon {
    color: var(--message-card-accent);
}

/* Per-type accent (bar + icon), split per theme. Success/danger ride
   Bootstrap's theme-aware form tokens; the rest are the same emphasis shades
   Bootstrap's dark alerts use, with a light-theme shade that clears 3:1 on
   white for the icon. The title text carries the meaning; the colour is
   the redundant cue. */
.message-card-enterpriseServices { --message-card-accent: #007055; }
.message-card-warning            { --message-card-accent: #997404; }
.message-card-info               { --message-card-accent: #087990; }
.message-card-success            { --message-card-accent: var(--bs-form-valid-color, #198754); }
.message-card-danger             { --message-card-accent: var(--bs-form-invalid-color, #dc3545); }

[data-bs-theme="dark"] .message-card-enterpriseServices { --message-card-accent: #5cbc94; }
[data-bs-theme="dark"] .message-card-warning            { --message-card-accent: var(--bs-warning-text-emphasis, #ffda6a); }
[data-bs-theme="dark"] .message-card-info               { --message-card-accent: var(--bs-info-text-emphasis, #6edff6); }

.message-card-text {
    white-space: pre-line;
}

.message-clamp {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.message-toggle {
    font-size: 0.8rem;
}

.announcement-card {
    flex: 0 0 300px;
}

.announcements-nav {
    position: relative;
}

.announcements-scroll {
    flex: 1 1 0;
    overflow-x: auto;
    /* ReSharper disable once CssNotResolved */
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding-bottom: 0.25rem;
    justify-content: center;
}

.announcements-scroll::-webkit-scrollbar {
    display: none;
}

.announcements-arrow {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    border: none;
    background: transparent;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-secondary-color);
    font-size: 1.1rem;
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
    border-radius: 0.375rem;
    padding: 0;
}

.announcements-arrow:hover {
    color: var(--bs-primary);
    /* ReSharper disable once CssNotResolved */
    /* ReSharper disable once InvalidValue */
    background: color-mix(in srgb, var(--bs-primary) 8%, transparent);
}

.announcements-arrow.hidden {
    /* visibility (not display) so the arrow keeps its 2rem slot and the rail
       doesn't jump sideways when one arrow hides. The global .hidden utility
       in capstone-bootstrap.css sets display:none — restore flex here so it
       can't collapse the slot. */
    display: flex;
    visibility: hidden;
    pointer-events: none;
}

/* showAlert() stack: a fixed, top-centre overlay. It used to sit in normal
   flow at the top of <main>, so every transient alert pushed the whole page
   down and back up again. Sits above modals (1055) so a post-modal "saved"
   card is never hidden; pointer-events pass through the empty container so
   it never blocks the navbar. */
.capstone-alert-stack {
    position: fixed;
    top: 1rem;
    left: 50%;
    transform: translateX(-50%);
    width: min(500px, calc(100% - 2rem));
    z-index: 1090;
    pointer-events: none;
}

.capstone-alert-stack > .message-card {
    pointer-events: auto;
}

.announcements-chevron {
    transition: transform 0.3s ease;
}

.announcements-chevron-open {
    transform: rotate(180deg);
}

@keyframes jiggle {
    0%   { transform: rotate(0); }
    25%  { transform: rotate(-3deg); }
    50%  { transform: rotate(3deg); }
    75%  { transform: rotate(-2deg); }
    100% { transform: rotate(0); }
}

/* One-shot hover flourish — interaction-scoped, but still motion, so it
   honors reduced-motion like every other Capstone animation. */
@media (prefers-reduced-motion: no-preference) {
    .announcements-btn:hover .announcements-chevron,
    .announcements-btn:active .announcements-chevron {
        animation: jiggle 0.4s ease;
    }
}

/* ── MessageBox component ───────────────────────────────── */

/* Visual identity for the loading-overlay box. The element is now a Bootstrap
   modal-content (see MessageBox view component) — Bootstrap handles position,
   z-index, scroll lock, focus trap, and backdrop, so we only style the box's
   appearance here. */
.messageBox {
    /* 1rem matches the card and file-upload surfaces (was 20px, rounder than
       anything else in Capstone). The gold .border-alt stays: it is the one cue
       that this box, unlike every other modal, is not interactive. */
    border-radius: 1rem;
}

/* ── Loading dots (shared indicator) ─────────────────────
   THE Capstone loading animation: pulsating dots whose background-color
   cycles through the brand palette (green → gold → bright green → sky) on
   staggered delays so the row reads as a wave. One source of truth for the
   MessageBox overlay (.capstone-loading-dots, 10 dots — a deliberate
   exception to the 5-dot count so the wave spans the modal-sm width like
   the full-bleed bar it replaced), the grid overlay
   (.capstone-grid-loading-dots, 5 dots), and the autocomplete / multiselect
   in-input spinners (3 small dots). Component files keep only their own
   positioning/visibility rules. Opacity + color only — no translation — so
   no prefers-reduced-motion split is needed.
   The cycle is green → gold → bright green: the two anchor brand colours
   plus the one secondary that reads on both themes. Sky blue was dropped
   in 9.5 — a supporting colour the brand guide says to use sparingly had
   been in every loading state of every app. */
.capstone-loading-dots,
.capstone-grid-loading-dots,
.capstone-autocomplete-spinner,
.capstone-multiselect-spinner {
    /* Resting/anchor green. #007055 at 25% opacity is close to invisible on
       the dark surfaces, so dark mode starts from the same lighter green
       .text-brand uses (9.5). */
    /* ReSharper disable once CssNotResolved */
    --capstone-dot-green: var(--bs-primary, #007055);
}

[data-bs-theme=dark] .capstone-loading-dots,
[data-bs-theme=dark] .capstone-grid-loading-dots,
[data-bs-theme=dark] .capstone-autocomplete-spinner,
[data-bs-theme=dark] .capstone-multiselect-spinner {
    --capstone-dot-green: #5cbc94;
}

.capstone-loading-dots,
.capstone-grid-loading-dots {
    display: inline-flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 0.625rem;
}

.capstone-loading-dots > span[aria-hidden="true"],
.capstone-grid-loading-dots > span[aria-hidden="true"],
.capstone-autocomplete-spinner > span[aria-hidden="true"],
.capstone-multiselect-spinner > span[aria-hidden="true"] {
    width: 1rem;
    height: 1rem;
    background-color: var(--capstone-dot-green);
    border-radius: 50%;
    opacity: 0.25;
    animation: capstone-loading-dot 1.7s ease-in-out infinite;
}

/* Small in-input variant for the pickers. */
.capstone-autocomplete-spinner > span[aria-hidden="true"],
.capstone-multiselect-spinner > span[aria-hidden="true"] {
    width: 0.5rem;
    height: 0.5rem;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(2),
.capstone-grid-loading-dots > span[aria-hidden="true"]:nth-child(2),
.capstone-autocomplete-spinner > span[aria-hidden="true"]:nth-child(2),
.capstone-multiselect-spinner > span[aria-hidden="true"]:nth-child(2) {
    animation-delay: 0.16s;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(3),
.capstone-grid-loading-dots > span[aria-hidden="true"]:nth-child(3),
.capstone-autocomplete-spinner > span[aria-hidden="true"]:nth-child(3),
.capstone-multiselect-spinner > span[aria-hidden="true"]:nth-child(3) {
    animation-delay: 0.32s;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(4),
.capstone-grid-loading-dots > span[aria-hidden="true"]:nth-child(4) {
    animation-delay: 0.48s;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(5),
.capstone-grid-loading-dots > span[aria-hidden="true"]:nth-child(5) {
    animation-delay: 0.64s;
}

/* Dots 6–10 exist only in the MessageBox's 10-dot row; the 0.16s stagger
   continues so the wave sweeps the full width. */
.capstone-loading-dots > span[aria-hidden="true"]:nth-child(6) {
    animation-delay: 0.8s;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(7) {
    animation-delay: 0.96s;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(8) {
    animation-delay: 1.12s;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(9) {
    animation-delay: 1.28s;
}

.capstone-loading-dots > span[aria-hidden="true"]:nth-child(10) {
    animation-delay: 1.44s;
}

@keyframes capstone-loading-dot {
    /* ReSharper disable CssNotResolved */
    0%   { opacity: 0.25; background-color: var(--capstone-dot-green); }
    25%  { opacity: 1;    background-color: var(--capstone-dot-green); }
    50%  { opacity: 1;    background-color: var(--bs-secondary, #f7d117); }
    75%  { opacity: 1;    background-color: var(--capstone-green-bright, #6cc24a); }
    100% { opacity: 0.25; background-color: var(--capstone-dot-green); }
    /* ReSharper restore CssNotResolved */
}

/* ── File upload component ─────────────────────────────── */

.capstone-file-upload {
    /* ReSharper disable once CssNotResolved */
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: 1rem;
    padding: 1.75rem;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
    max-width: 380px;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
}

.capstone-file-upload-title {
    font-weight: 700;
    text-align: center;
    margin-bottom: 1.25rem;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
}

.capstone-file-upload-zone {
    position: relative;
    border-radius: 0.75rem;
    padding: 1.25rem 1.5rem;
    text-align: center;
    background: transparent;
    transition: background-color 0.2s;
    cursor: pointer;
    border: none;
    display: block;
    width: 100%;
    color: inherit;
    font: inherit;
    -webkit-appearance: none;
    appearance: none;
}

.capstone-file-upload-zone:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

.capstone-file-upload-zone:disabled {
    cursor: not-allowed;
    opacity: 0.65;
}

.capstone-file-upload-zone-border {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    overflow: hidden;
}

.capstone-file-upload-zone-border svg {
    width: 100%;
    height: 100%;
}

.capstone-file-upload-zone-border rect {
    transition: stroke 0.2s;
}

.capstone-file-upload-zone:hover {
    /* ReSharper disable once CssNotResolved */
    /* ReSharper disable once InvalidValue */
    background-color: color-mix(in srgb, var(--bs-primary) 4%, transparent);
}

.capstone-file-upload-zone:hover .capstone-file-upload-zone-border rect {
    stroke: var(--bs-primary);
}

.capstone-file-upload-zone-active {
    /* ReSharper disable once CssNotResolved */
    /* ReSharper disable once InvalidValue */
    background-color: color-mix(in srgb, var(--bs-primary) 8%, transparent) !important;
}

.capstone-file-upload-zone-active .capstone-file-upload-zone-border rect {
    stroke: var(--bs-primary);
    stroke-dasharray: none;
}

.capstone-file-upload-icon {
    font-size: 2.25rem;
    color: #f0a500;
    display: block;
    margin-bottom: 0.375rem;
}

@media (prefers-reduced-motion: no-preference) {
    .capstone-file-upload-icon {
        animation: capstone-file-upload-bounce 1s 5;
    }
}

@keyframes capstone-file-upload-bounce {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-6px); }
}

.capstone-file-upload-text {
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
    font-size: 0.9375rem;
}

.capstone-file-upload-browse {
    color: var(--bs-primary);
    text-decoration: underline;
    font-weight: 500;
    cursor: pointer;
}

.capstone-file-upload-browse:hover {
    color: var(--bs-primary);
    text-decoration: none;
}

.capstone-file-upload-list {
    margin-top: 0.75rem;
}

/* Single-file preview */
.capstone-file-upload-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0;
}

.capstone-file-upload-thumb {
    width: 2rem;
    height: 2rem;
    object-fit: cover;
    border-radius: 0.25rem;
    flex-shrink: 0;
}

.capstone-file-upload-file-name {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
    font-size: 0.875rem;
    font-weight: 500;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
}

.capstone-file-upload-remove {
    flex-shrink: 0;
    line-height: 1;
}

.capstone-file-upload-remove-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    border: 1px solid transparent;
    background: transparent;
    color: #dc3545;
    font-size: 0.7rem;
    cursor: pointer;
    padding: 0;
    transition: background-color 0.15s, border-color 0.15s;
}

.capstone-file-upload-remove-icon:hover {
    background-color: rgba(220, 53, 69, 0.1);
    border-color: #dc3545;
}

.capstone-file-upload-remove-icon:focus-visible,
.capstone-file-upload-chip-remove:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* Multi-file pill chips */
.capstone-file-upload-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    max-height: 180px;
    overflow-y: auto;
}

.capstone-file-upload-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    max-width: 14rem;
    padding: 0.25rem 0.5rem 0.25rem 0.25rem;
    border-radius: 9999px;
    border: 1px solid var(--bs-border-color);
    /* ReSharper disable once CssNotResolved */
    background: var(--bs-body-bg);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
    font-size: 0.8125rem;
    font-weight: 500;
    transition: background-color 0.15s;
}

.capstone-file-upload-chip:hover {
    /* ReSharper disable once CssNotResolved */
    /* ReSharper disable once InvalidValue */
    background-color: color-mix(in srgb, var(--bs-body-color) 4%, transparent);
}

.capstone-file-upload-chip-thumb {
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}

.capstone-file-upload-chip-icon {
    font-size: 1rem;
    margin-left: 0.25rem;
    flex-shrink: 0;
}

.capstone-file-upload-chip-name {
    overflow-wrap: anywhere;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
}

.capstone-file-upload-chip-remove {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 50%;
    border: 1px solid transparent;
    background: transparent;
    color: #dc3545;
    font-size: 0.55rem;
    cursor: pointer;
    padding: 0;
    transition: background-color 0.15s, border-color 0.15s;
    flex-shrink: 0;
}

.capstone-file-upload-chip-remove:hover {
    background-color: rgba(220, 53, 69, 0.1);
    border-color: #dc3545;
}

/* Fade-in animation for new items */
@keyframes capstone-file-upload-fade-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: no-preference) {
    .capstone-file-upload-fade-in {
        animation: capstone-file-upload-fade-in 0.3s ease;
    }
}

/* Upload button */
.capstone-file-upload-btn:disabled {
    cursor: no-drop;
    pointer-events: auto;
}

.capstone-file-upload-errors:not(:empty) {
    margin-top: 0.5rem;
}

/* ── Autocomplete component ──────────────────────────────── */

.capstone-autocomplete-wrap {
    position: relative;
}

/* Wrap-scoped to beat Bootstrap's .form-floating > .form-control (specificity 0,2,0).
   Without the wrap selector, this rule loses the cascade tie and the input falls back
   to Bootstrap's 0.75rem right-padding — letting long values render under the clear ×. */
.capstone-autocomplete-wrap .capstone-autocomplete-input {
    padding-right: 2.25rem;
}

/* Size variants — mirror Bootstrap's form-control-sm / -lg for the floating-label
   autocomplete. Opt in via the `size` attribute on <capstone-autocomplete>, which
   stamps `.capstone-autocomplete-sm` / `-lg` on the container; omit for default.
   Scoped to the modifier so default sizing is untouched. Selectors carry the
   modifier class so they out-specify Bootstrap's `.form-floating > .form-control`
   (0,2,0) rules. The clear ×, toggle, and spinner center on top:50%, so they
   follow the new height automatically — and the right-padding (clear/toggle
   clearance) rules above are left to own padding-right; these only retune height,
   font, the left inset, and the floating-label transform. */

/* sm — compact height + smaller font for dense forms with several stacked pickers. */
.capstone-autocomplete-sm .form-floating > .capstone-autocomplete-input {
    height: calc(2.75rem + 2px);
    min-height: calc(2.75rem + 2px);
    font-size: 0.875rem;
    padding-top: 1.125rem;
    padding-bottom: 0.125rem;
    padding-left: 0.5rem;
}

.capstone-autocomplete-sm .form-floating > label {
    padding: 0.5rem 0.5rem;
    font-size: 0.875rem;
}

/* Floated label pulls up a touch less than Bootstrap's default (-0.5rem) so it
   stays inside the shorter control. */
.capstone-autocomplete-sm .form-floating > .capstone-autocomplete-input:focus ~ label,
.capstone-autocomplete-sm .form-floating > .capstone-autocomplete-input:not(:placeholder-shown) ~ label {
    transform: scale(0.8) translateY(-0.35rem) translateX(0.15rem);
}

/* lg — taller, larger-font control for prominent/standalone search fields. */
.capstone-autocomplete-lg .form-floating > .capstone-autocomplete-input {
    height: calc(4.25rem + 2px);
    min-height: calc(4.25rem + 2px);
    font-size: 1.125rem;
    padding-top: 1.75rem;
    padding-bottom: 0.5rem;
    padding-left: 1rem;
}

.capstone-autocomplete-lg .form-floating > label {
    padding: 1rem 1rem;
    font-size: 1.125rem;
}

/* Floated label pulls up further to clear the taller control. */
.capstone-autocomplete-lg .form-floating > .capstone-autocomplete-input:focus ~ label,
.capstone-autocomplete-lg .form-floating > .capstone-autocomplete-input:not(:placeholder-shown) ~ label {
    transform: scale(0.85) translateY(-0.85rem) translateX(0.15rem);
}

/* A real <button> (keyboard/AT reachable) — strip the native chrome so it
   renders as the bare × glyph it always was. */
.capstone-autocomplete-clear {
    position: absolute;
    right: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    display: none;
    background: transparent;
    border: none;
    padding: 0;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
    font-size: 1.25rem;
    cursor: pointer;
    line-height: 1;
    opacity: 0.4;
    transition: opacity 0.15s;
    z-index: 5;
    user-select: none;
}

.capstone-autocomplete-clear:focus-visible {
    opacity: 1;
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
    border-radius: 0.25rem;
}

.capstone-autocomplete-clear:hover {
    opacity: 1;
}

.capstone-autocomplete-toggle {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
    border: none;
    padding: 0.25rem 0.4rem;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-body-color);
    cursor: pointer;
    opacity: 0.5;
    transition: opacity 0.15s, transform 0.15s;
    z-index: 5;
    line-height: 1;
    font-size: 0.875rem;
}

.capstone-autocomplete-toggle:hover {
    opacity: 1;
}

.capstone-autocomplete-toggle.open {
    transform: translateY(-50%) rotate(180deg);
}

/* When the toggle button is present, reserve room for it and shift the clear × to its left */
.capstone-autocomplete-wrap:has(> .capstone-autocomplete-toggle) .capstone-autocomplete-input {
    padding-right: 3.75rem;
}

.capstone-autocomplete-wrap:has(> .capstone-autocomplete-toggle) .capstone-autocomplete-clear {
    right: 2.25rem;
}

/* position: fixed (not absolute) so the list escapes any ancestor overflow clip
   — notably the grid's .table-responsive horizontal-scroll container — without
   being reparented out of .capstone-autocomplete-wrap. left/top/width are set by
   JS (_positionList) from the input's bounding rect and tracked on scroll/resize.
   z-index sits above Bootstrap modals (1055) / popovers (1070) since a fixed list
   is no longer inside the modal's stacking context. */
.capstone-autocomplete-list {
    position: fixed;
    z-index: 1080;
    /* ReSharper disable once CssNotResolved */
    background: var(--bs-body-bg);
    /* ReSharper disable once CssNotResolved */
    border: 1px solid var(--bs-border-color);
    border-top: none;
    border-radius: 0 0 0.375rem 0.375rem;
    max-height: 240px;
    overflow-y: auto;
    /* Wheel-scrolling past either end of the list must not chain into the
       scroll container behind it (a modal, the page): the list would ride
       along via the scroll-tracking repositioning and feel like it slipped. */
    overscroll-behavior: contain;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

/* Flipped above the input when there isn't room below (JS adds this class and
   sets top). Move the open edge to the top so it still reads as attached to the
   field — mirrors the default bottom-attached border/radius. */
.capstone-autocomplete-list-up {
    /* ReSharper disable once CssNotResolved */
    border-top: 1px solid var(--bs-border-color);
    border-bottom: none;
    border-radius: 0.375rem 0.375rem 0 0;
}

.capstone-autocomplete-item {
    padding: 0.5rem 0.75rem;
    cursor: pointer;
    font-size: 0.875rem;
    /* ReSharper disable once CssNotResolved */
    border-bottom: 1px solid var(--bs-border-color);
    transition: background-color 0.1s;
}

.capstone-autocomplete-item:last-child {
    border-bottom: none;
}

.capstone-autocomplete-item:hover {
    /* ReSharper disable once CssNotResolved */
    background-color: var(--bs-secondary-bg);
}

.capstone-autocomplete-active {
    color: #ffffff;
}

/* Empty-state entry — non-interactive, mirrors the grid's "No Data Available."
   look (centered, muted, leading warning icon). No hover/active treatment
   since there's nothing to select. */
.capstone-autocomplete-empty {
    padding: 0.5rem 0.75rem;
    font-size: 0.875rem;
    text-align: center;
    cursor: default;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-secondary-color, #6c757d);
}

/* Truncation footer — rendered as the last list entry when more items match
   than max-results allows. Non-interactive like the empty-state, but muted +
   italic so it reads as a hint ("keep typing to narrow"), not a selectable
   row. No hover/active treatment. */
.capstone-autocomplete-more {
    padding: 0.375rem 0.75rem;
    font-size: 0.8125rem;
    font-style: italic;
    text-align: center;
    cursor: default;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-secondary-color, #6c757d);
    /* ReSharper disable once CssNotResolved */
    border-top: 1px solid var(--bs-border-color);
}

.capstone-autocomplete-validation {
    position: absolute;
    top: 0;
    right: 0.5rem;
    transform: translateY(-50%);
    display: none;
    padding: 0.125rem 0.5rem;
    /* ReSharper disable once CssNotResolved */
    background: var(--bs-danger, #dc3545);
    color: #fff;
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.2;
    border-radius: 0.25rem;
    z-index: 6;
    pointer-events: none;
    white-space: nowrap;
}

.capstone-autocomplete-input.is-invalid {
    /* ReSharper disable once CssNotResolved */
    border-color: var(--bs-form-invalid-border-color, #dc3545);
}

.capstone-autocomplete-input.is-valid {
    /* ReSharper disable once CssNotResolved */
    border-color: var(--bs-form-valid-border-color, #198754);
}

/* Focus must not wash out a validation state: capstone-bootstrap.css paints a
   brand-green focus ring on every .form-control (its dark-theme rule is
   (0,3,0) and loads AFTER this sheet), so these run at (0,4,0) to keep the
   state border on the focused picker and tint the ring to match — the same
   .is-invalid:focus convention stock Bootstrap uses. */
.form-control.capstone-autocomplete-input.is-invalid:focus {
    /* ReSharper disable once CssNotResolved */
    border-color: var(--bs-form-invalid-border-color, #dc3545);
    /* ReSharper disable once CssNotResolved */
    box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb, 220, 53, 69), 0.25);
}

.form-control.capstone-autocomplete-input.is-valid:focus {
    /* ReSharper disable once CssNotResolved */
    border-color: var(--bs-form-valid-border-color, #198754);
    /* ReSharper disable once CssNotResolved */
    box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb, 25, 135, 84), 0.25);
}

/* Red exclamation-circle shown on the invalid state (parity with the multiselect).
   Bootstrap's own .is-invalid icon is a background-image pinned to the input's right
   edge — suppressed below because it can't be repositioned and would render under
   the clear ×/chevron. We render our own foreground icon in a controlled slot just
   LEFT of the clear × instead, so it never overlaps the clear/toggle controls.
   Shown only when the input carries .is-invalid (via :has on the wrap, since the
   icon is a sibling of the .form-floating that holds the input). */
.capstone-autocomplete-validation-icon {
    position: absolute;
    right: 2.25rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 6;
    display: none;
    align-items: center;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-form-invalid-color, #dc3545);
    font-size: 1rem;
    line-height: 1;
    pointer-events: none;
}

.capstone-autocomplete-wrap:has(.capstone-autocomplete-input.is-invalid) .capstone-autocomplete-validation-icon {
    display: inline-flex;
}

/* Validated-OK state (validate() passed): same slot, green check — the glyph
   swap overrides the ::before content the bi- icon class sets. Parity with
   aspnet-validation's green .input-validation-valid on plain fields. */
.capstone-autocomplete-wrap:has(.capstone-autocomplete-input.is-valid) .capstone-autocomplete-validation-icon {
    display: inline-flex;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-form-valid-color, #198754);
}

.capstone-autocomplete-wrap:has(.capstone-autocomplete-input.is-valid) .capstone-autocomplete-validation-icon .bi::before {
    content: "\f26a"; /* bi-check-circle-fill */
}

/* With the show-all chevron present, the clear × already shifts to 2.25rem — move
   the validation icon further left so it clears both the clear × and the chevron. */
.capstone-autocomplete-wrap:has(> .capstone-autocomplete-toggle) .capstone-autocomplete-validation-icon {
    right: 3.75rem;
}

/* Reserve room for the icon so the floating-label value doesn't slide under it
   while the input is flagged invalid or validated-OK. */
.capstone-autocomplete-wrap:has(.capstone-autocomplete-input.is-invalid) .capstone-autocomplete-input,
.capstone-autocomplete-wrap:has(.capstone-autocomplete-input.is-valid) .capstone-autocomplete-input {
    padding-right: 3.75rem;
}

.capstone-autocomplete-wrap:has(> .capstone-autocomplete-toggle):has(.capstone-autocomplete-input.is-invalid) .capstone-autocomplete-input,
.capstone-autocomplete-wrap:has(> .capstone-autocomplete-toggle):has(.capstone-autocomplete-input.is-valid) .capstone-autocomplete-input {
    padding-right: 5.25rem;
}

/* Suppress Bootstrap's was-validated / .is-valid / .is-invalid icon on the
   autocomplete's search input. The visible input has no HTML5 `required` (the
   hidden value field is the source of truth), so it ALWAYS reads as :valid to
   Bootstrap — painting a green checkmark on fields the component itself has
   flagged invalid. Capstone owns the autocomplete's validation visualization
   end-to-end (red "Required" badge + .is-invalid border); Bootstrap's parallel
   icons only contradict it. Suppress background-image only — padding stays with
   the wrap-scoped rules above (2.25rem default, 3.75rem with the chevron). */
.was-validated .capstone-autocomplete-input.form-control:valid,
.was-validated .capstone-autocomplete-input.form-control:invalid,
.capstone-autocomplete-input.form-control.is-valid,
.capstone-autocomplete-input.form-control.is-invalid {
    background-image: none;
}

/* Three-dot loading indicator shown inside the input while a read-url fetch is
   in flight. Hidden by default; the container's data-loading attribute (set by
   the TagHelper initially and managed by JS afterwards) gates visibility.
   Sits where the clear button normally lives — both can't be visible at once
   because the input is disabled while loading. */
.capstone-autocomplete-spinner {
    position: absolute;
    right: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 5;
    display: none;
    align-items: center;
    gap: 0.3rem;
}

.capstone-autocomplete[data-loading="true"] .capstone-autocomplete-spinner {
    display: inline-flex;
}

/* Dot styling and color-cycle animation come from the shared
   "Loading dots" block earlier in this file. */

/* Hide clear/toggle buttons while loading — input is disabled, no value to clear,
   and the chevron toggle would compete with the spinner for the same slot. */
.capstone-autocomplete[data-loading="true"] .capstone-autocomplete-clear,
.capstone-autocomplete[data-loading="true"] .capstone-autocomplete-toggle {
    display: none;
}

/* Read-error state — shown when the read-url fetch fails. The decorative icon
   sits in the same slot as the spinner (input is disabled, so neither the
   clear × nor the chevron toggle can compete). The icon is anchored inside
   the .form-floating wrapper (which has position: relative thanks to
   Bootstrap's form-floating styles) so it stays vertically centered on the
   input even when the message block below grows the outer .capstone-autocomplete-wrap.
   The message + Retry button render below the input as a role="alert" live
   region — that block is what screen readers announce, the icon is purely a
   sighted-user cue. */
.capstone-autocomplete-error-icon {
    position: absolute;
    right: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 5;
    display: none;
    align-items: center;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-form-invalid-color, #dc3545);
    font-size: 1rem;
    line-height: 1;
    pointer-events: none;
}

.capstone-autocomplete[data-read-error="true"] .capstone-autocomplete-error-icon {
    display: inline-flex;
}

/* Hide clear/toggle in the error state too — there are no options to clear or
   expand, and the toggle would render on top of the error icon. */
.capstone-autocomplete[data-read-error="true"] .capstone-autocomplete-clear,
.capstone-autocomplete[data-read-error="true"] .capstone-autocomplete-toggle {
    display: none;
}

/* The read-error field is readonly + aria-disabled rather than `disabled`, so it
   stays focusable and its aria-describedby link to the message actually reaches
   assistive tech (a disabled input is out of the tab order entirely). Bootstrap
   only paints :disabled, so carry the same non-interactive surface across
   ourselves — otherwise a field that refuses input would still look editable. */
.capstone-autocomplete[data-read-error="true"] .capstone-autocomplete-input {
    /* ReSharper disable once CssNotResolved */
    background-color: var(--bs-secondary-bg);
    opacity: 1;
    cursor: not-allowed;
}

/* Visible message + Retry button under the input. Mirrors Bootstrap's
   .invalid-feedback rhythm so the error block lines up with the validation
   pattern users already know. */
.capstone-autocomplete-error {
    display: none;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.25rem;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-form-invalid-color, #dc3545);
    font-size: 0.875rem;
    line-height: 1.3;
}

.capstone-autocomplete-error-message {
    flex: 0 1 auto;
}

.capstone-autocomplete-error-retry {
    /* btn-link defaults stay; we only tweak rhythm so the button sits flush
       with the message without inheriting the link's underline-on-hover
       making the row jump. */
    padding: 0;
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-form-invalid-color, #dc3545);
    font-size: 0.875rem;
    text-decoration: underline;
    line-height: 1.3;
}

.capstone-autocomplete-error-retry:hover,
.capstone-autocomplete-error-retry:focus {
    /* ReSharper disable once CssNotResolved */
    color: var(--bs-form-invalid-color, #dc3545);
    text-decoration: none;
}


/* ── Stepper (<capstone-stepper>) ────────────────────────────
   Multi-step progress. Two presentations in the markup: the full labelled
   stepper (≥576px) and a compact "Step X of Y — Label" bar with one segment
   per step (below that). CSS picks which shows; the tag helper hides both
   from AT behind one visually-hidden summary line (unless jump-back links
   make the list interactive).
   --bs-primary (#007055) is pinned for white-on-green fills, so the done
   markers use it as a background (6.09:1 white text), but as foreground
   (current ring, labels, connector) it is ~2.5:1 on dark surfaces — the
   accent switches to the .text-brand dark green there. */
.capstone-stepper {
    --capstone-stepper-accent: #007055;
    --capstone-stepper-track: var(--bs-border-color);
    --capstone-stepper-muted: var(--bs-secondary-color);
    --capstone-stepper-marker: 2rem;
    max-width: var(--capstone-stepper-max-width, 720px);
    margin: 1rem auto 1.75rem;
    /* Status indicator, not editable text. */
    cursor: default;
    user-select: none;
}

[data-bs-theme=dark] .capstone-stepper {
    --capstone-stepper-accent: #5cbc94;
}

.capstone-stepper-steps {
    display: none;
}

.capstone-stepper-compact-text {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.4rem;
}

.capstone-stepper-compact-count {
    font-size: 0.8rem;
    color: var(--capstone-stepper-muted);
}

.capstone-stepper-compact-label {
    font-weight: 600;
    color: var(--capstone-stepper-accent);
}

.capstone-stepper-compact-track {
    display: flex;
    gap: 3px;
    height: 0.4rem;
}

.capstone-stepper-compact-track > span {
    flex: 1 1 0;
    border-radius: 999px;
    background: var(--capstone-stepper-track);
}

.capstone-stepper-compact-track > span.is-filled {
    background: var(--capstone-stepper-accent);
}

@media (min-width: 576px) {
    .capstone-stepper-steps {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        list-style: none;
        margin: 0;
        padding: 0;
    }

    .capstone-stepper-compact {
        display: none;
    }

    .capstone-stepper-item {
        position: relative;
        flex: 1 1 0;
        min-width: 0;
        text-align: center;
    }

    .capstone-stepper-item,
    .capstone-stepper-link {
        display: flex;
        flex-direction: column;
        align-items: center;
    }

    .capstone-stepper-link {
        text-decoration: none;
        cursor: pointer;
        border-radius: 0.375rem;
    }

    .capstone-stepper-link:hover .capstone-stepper-label {
        text-decoration: underline;
    }

    /* Connector from the previous step, running behind the markers. */
    .capstone-stepper-item::before {
        content: "";
        position: absolute;
        top: calc(var(--capstone-stepper-marker) / 2 - 1px);
        right: 50%;
        width: 100%;
        height: 2px;
        background: var(--capstone-stepper-track);
    }

    .capstone-stepper-item:first-child::before {
        display: none;
    }

    .capstone-stepper-item.is-done::before,
    .capstone-stepper-item.is-current::before {
        background: var(--capstone-stepper-accent);
    }

    .capstone-stepper-marker {
        position: relative;
        z-index: 1;
        width: var(--capstone-stepper-marker);
        height: var(--capstone-stepper-marker);
        border-radius: 50%;
        display: grid;
        place-items: center;
        font-size: 0.85rem;
        font-weight: 600;
        line-height: 1;
        border: 2px solid var(--capstone-stepper-track);
        background: var(--bs-body-bg);
        color: var(--capstone-stepper-muted);
    }

    .capstone-stepper-item.is-done .capstone-stepper-marker {
        border-color: var(--bs-primary);
        background: var(--bs-primary);
        color: #fff;
    }

    .capstone-stepper-item.is-current .capstone-stepper-marker {
        border-color: var(--capstone-stepper-accent);
        color: var(--capstone-stepper-accent);
        box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.2);
    }

    .capstone-stepper-label {
        margin-top: 0.55rem;
        padding: 0 0.25rem;
        font-size: 0.8rem;
        line-height: 1.2;
        color: var(--capstone-stepper-muted);
    }

    .capstone-stepper-item.is-current .capstone-stepper-label {
        color: var(--capstone-stepper-accent);
        font-weight: 600;
    }

    .capstone-stepper-item.is-done .capstone-stepper-label {
        color: var(--bs-body-color);
    }
}

@media (prefers-reduced-motion: no-preference) {
    .capstone-stepper-marker,
    .capstone-stepper-compact-track > span {
        transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
    }
}


/* ── Status page (<capstone-status-page>) ─────────────────────
   Terminal pages: centred icon, heading, a sentence or two, optional
   action, muted contact line. The icon is aria-hidden (the title carries
   the meaning) but still clears the 3:1 graphics minimum per tone and
   theme — raw --bs-warning / --bs-info are ~2:1 on white, so light uses
   darker shades; dark uses the emphasis shades. */
.capstone-status-page {
    --capstone-status-color: #007055;
    max-width: 36rem;
    margin: 3rem auto;
    text-align: center;
}

.capstone-status-success { --capstone-status-color: #198754; }
.capstone-status-warning { --capstone-status-color: #b07c0e; }
.capstone-status-danger  { --capstone-status-color: #dc3545; }
.capstone-status-info    { --capstone-status-color: #087990; }

[data-bs-theme=dark] .capstone-status-brand   { --capstone-status-color: #5cbc94; }
[data-bs-theme=dark] .capstone-status-success { --capstone-status-color: #75b798; }
[data-bs-theme=dark] .capstone-status-warning { --capstone-status-color: #e8a317; }
[data-bs-theme=dark] .capstone-status-danger  { --capstone-status-color: #ea868f; }
[data-bs-theme=dark] .capstone-status-info    { --capstone-status-color: #8bd3e6; }

.capstone-status-icon {
    display: block;
    font-size: 3.5rem;
    line-height: 1;
    margin-bottom: 1rem;
    color: var(--capstone-status-color);
}

.capstone-status-title {
    font-size: calc(1.325rem + 0.9vw);
    margin-bottom: 0.75rem;
}

@media (min-width: 1200px) {
    .capstone-status-title {
        font-size: 2rem;
    }
}

.capstone-status-body > :last-child {
    margin-bottom: 0;
}

.capstone-status-action {
    margin-top: 1.5rem;
}

.capstone-status-contact {
    margin: 1.5rem 0 0;
    font-size: 0.875rem;
    color: var(--bs-secondary-color);
}


/* ── Notice (<capstone-notice>) ───────────────────────────────
   Persistent in-page notice (disclaimers, data caveats, maintenance
   notes) — the static sibling of the showAlert() message card, sharing
   its vocabulary: neutral surface, per-tone accent bar + icon, body-colour
   text. Text never takes the tone colour, so every tone clears AA in both
   themes; the accents are the message-card shades (≥3:1 graphics on the
   tertiary surface). role="note", never alert — see the tag helper. */
.capstone-notice {
    --capstone-notice-accent: #087990;
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    margin-bottom: 1rem;
    padding: 0.75rem 1rem;
    background-color: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-left: 4px solid var(--capstone-notice-accent);
    border-radius: var(--bs-border-radius);
    color: var(--bs-body-color);
}

.capstone-notice-brand   { --capstone-notice-accent: #007055; }
.capstone-notice-success { --capstone-notice-accent: var(--bs-form-valid-color, #198754); }
.capstone-notice-warning { --capstone-notice-accent: #997404; }
.capstone-notice-danger  { --capstone-notice-accent: var(--bs-form-invalid-color, #dc3545); }
.capstone-notice-neutral { --capstone-notice-accent: #6c757d; }

[data-bs-theme=dark] .capstone-notice-info    { --capstone-notice-accent: var(--bs-info-text-emphasis, #6edff6); }
[data-bs-theme=dark] .capstone-notice-brand   { --capstone-notice-accent: #5cbc94; }
[data-bs-theme=dark] .capstone-notice-warning { --capstone-notice-accent: var(--bs-warning-text-emphasis, #ffda6a); }
[data-bs-theme=dark] .capstone-notice-neutral { --capstone-notice-accent: #adb5bd; }

.capstone-notice-icon {
    flex-shrink: 0;
    font-size: 1.25rem;
    line-height: 1.2;
    color: var(--capstone-notice-accent);
}

.capstone-notice-content {
    flex: 1 1 auto;
    min-width: 0;
}

.capstone-notice-title {
    display: block;
    margin-bottom: 0.125rem;
}

.capstone-notice-body > :last-child {
    margin-bottom: 0;
}

/* Compact: a one-line footnote. Title runs inline as a lead-in. */
.capstone-notice-compact {
    gap: 0.5rem;
    padding: 0.375rem 0.75rem;
    font-size: 0.875rem;
}

.capstone-notice-compact .capstone-notice-icon {
    font-size: 1rem;
    line-height: 1.3125rem;
}

.capstone-notice-compact .capstone-notice-title,
.capstone-notice-compact .capstone-notice-body {
    display: inline;
}

.capstone-notice-compact .capstone-notice-title {
    margin: 0 0.25rem 0 0;
}

.capstone-notice-compact .capstone-notice-body > * {
    display: inline;
}
