:root {
    /* Kept in sync with public.css's palette so the whole site -- public
       kiosk pages and the login-gated app alike -- reads as one
       consistent, deliberately designed product rather than two.

       Azure is the primary interactive color throughout (borders, glow,
       buttons, focus states). Gold is intentionally rare -- it appears
       only where something has actually been earned: a ranked recruit's
       rating pill, the gauge fill, the ranked-this-cycle stat. Nowhere
       else. See recruitment-planning-notes.md-adjacent design
       conversation for the reasoning if this ever needs revisiting. */
    --blue: #5ec4ff;
    --blue-dark: #2f6fa8;
    --blue-light: #8fd4ff;

    --bg: #070a17;
    --surface: #141d3a;
    --surface-hover: #1a2648;
    --surface-raised: #1e2b52;
    --border: rgba(255, 255, 255, 0.1);
    --text: #f3f5fb;
    --text-muted: #99a2c0;
    /* --error is deliberately NOT redefined for light mode -- it's the
       .btn-danger background, and that button's text is the fixed
       --on-accent dark navy, which needs --error to stay bright enough
       to read against. --error-text is the separate, theme-varying
       token for every other spot (form errors, the incomplete-profile
       flag) where --error is small text sitting directly on the page. */
    --error: #ff8080;
    --error-text: var(--error);

    --brick: #a8503c;
    --gold: #c9a24d;
    --gold-bright: #ffcf6b;
    --success: #4fae72;
    --success-dark: #3d9159;

    /* Fixed regardless of theme -- the text/icon color for anything
       sitting on top of a saturated blue or gold surface (buttons,
       badges, the gauge, the avatar initials). Dark navy reads cleanly
       on both accent colors in both themes, so this deliberately does
       NOT track --bg the way it used to (that only worked by
       coincidence while --bg itself was always dark). */
    --on-accent: #0b0f1f;

    /* One typeface, one weight scale, everywhere -- a dashboard tool
       reads as professional through consistency and restraint, not
       through a second "brand" font drawing attention to itself.
       --font-display is kept as a separate token (rather than deleted)
       purely so every place that referenced it keeps working -- it's
       intentionally the same value as --font-body now. */
    --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-display: var(--font-body);

    --radius: 14px;
    --radius-sm: 10px;
    --ease-snap: cubic-bezier(0.16, 1, 0.3, 1);

    /* One shared elevation scale instead of every component hand-rolling
       its own shadow rgba/blur. Kept deliberately light -- sm is barely
       there (a resting card should read as flat, bordered, not lifted),
       md is a mild hover response, lg is reserved for things genuinely
       floating above the page content (a dialog, a popover). */
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 4px 14px -6px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 20px 48px -16px rgba(0, 0, 0, 0.55);
}

/* Light mode -- opt-in per account via Profile.theme (see profile_edit),
   applied server-side as data-theme on <html> so there's no flash of
   the wrong theme on load. Only the tokens that actually need to move
   are redefined here; anything not listed (--blue, --gold, --brick,
   --success, --error, --on-accent, radii, fonts) already works on both
   grounds and is inherited from :root unchanged. --blue-light and
   --error-text both get darkened specifically because they're used as
   small text (nav links, badges, form errors) -- the pale dark-mode
   values that read fine on near-black would be too low-contrast on a
   light page. */
:root[data-theme="light"] {
    --bg: #f4f6fb;
    --surface: #fbfcfe;
    --surface-hover: #eef1f8;
    --surface-raised: #ffffff;
    --border: rgba(15, 23, 42, 0.12);
    --text: #12172b;
    --text-muted: #5b6478;
    --blue-light: #1f6fb8;
    --error-text: #c23a3a;

    /* Dark, high-opacity shadows read as heavy smudges on a white page --
       lighter and tighter here so elevation still registers without
       looking like a mistake. */
    --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.08);
    --shadow-md: 0 4px 12px -6px rgba(15, 23, 42, 0.14);
    --shadow-lg: 0 18px 40px -14px rgba(15, 23, 42, 0.2);
}

* {
    box-sizing: border-box;
}

/* The HTML `hidden` attribute's default browser rule is `display: none`
   with no !important, so any element that's both toggled via `hidden`
   and targeted by an author `display` rule (e.g. `.bulk-actions {
   display: flex }`) would win the cascade on origin alone and never
   actually hide -- this file does that combination in several places
   (quick-text/bulk-text dialog panels, the bulk-actions bar). Force it
   to always win instead of relying on rule ordering. */
[hidden] {
    display: none !important;
}

html, body {
    margin: 0;
    min-height: 100%;
}

body {
    font-family: var(--font-body);
    background: var(--bg);
    color: var(--text);
}

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

.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.85rem 1.5rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
    flex-wrap: wrap;
    position: relative;
    z-index: 10;
}

.topbar .brand {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    font-weight: 700;
    font-size: 1.15rem;
    letter-spacing: -0.01em;
    text-decoration: none;
    color: var(--text);
}

.topbar nav {
    display: flex;
    align-items: center;
    gap: 1rem;
    font-size: 0.95rem;
}

.topbar nav a {
    text-decoration: none;
    color: var(--text-muted);
}

.topbar nav a:hover {
    color: var(--text);
}

/* Navigation reads as tabs, not buttons -- flat text + icon, a 2px
   underline marking "you are here" (see context_processors.site_links's
   nav_section), nothing else competing for attention in the topbar. No
   background fill on hover or on the current item; a tab bar earns its
   "clean" read by staying quiet even when active. The icons alone tell
   Potential Members and Events apart; they don't need color to do that
   job too. (.topbar nav a beats a bare class on specificity, hence
   matching that shape here.) */
.nav-link-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.15rem;
    border-bottom: 2px solid transparent;
    font-weight: 600;
    font-size: 0.88rem;
    white-space: nowrap;
    background: transparent;
    transition: color 0.2s var(--ease-snap), border-color 0.2s var(--ease-snap);
}

.nav-link-pill svg {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

.topbar nav a.nav-link-pill:hover {
    color: var(--text);
}

.topbar nav a.nav-link-pill.nav-link-current {
    color: var(--blue-light);
    border-bottom-color: var(--blue);
}

.topbar nav a.nav-link-pill.nav-link-current:hover {
    color: var(--blue-light);
}

.logout-form {
    /* Transparent to the flex layout so the button inside behaves as a
       direct flex item of whatever it's dropped into (the confirm
       dialog's .message-actions row) -- same trick as the purge
       dialog's forms. */
    display: contents;
}

main {
    max-width: 960px;
    margin: 0 auto;
    padding: 2rem 1.25rem 3rem;
}

h1 {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(1.5rem, 3vw, 2.05rem);
    letter-spacing: -0.01em;
    margin: 0 0 1rem;
}

.page-subtitle {
    margin: -0.5rem 0 1.25rem;
    color: var(--text-muted);
    font-size: 0.95rem;
    max-width: 60ch;
}

.roster {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.bid-slate-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    flex-shrink: 0;
}

.bid-slate-actions .btn {
    padding: 0.5rem 1rem;
    font-size: 0.82rem;
}

.roster-row {
    position: relative;
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.85rem 1.1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    text-decoration: none;
    color: var(--text);
    transition: background 0.15s var(--ease-snap), border-color 0.15s var(--ease-snap);
}

.roster-row:hover {
    background: var(--surface-hover);
    border-color: rgba(94, 196, 255, 0.3);
}

/* .roster-row is a <div> now, not an <a> -- it needs a checkbox/button/
   form to sit beside the "go to detail" link without nesting an
   interactive element inside an interactive element (invalid HTML).
   roster-row-link is an absolutely-positioned full-card overlay, given
   z-index:1 so it paints above plain content (name, avatar, badges) --
   those stay position:relative with no z-index of their own, so a click
   anywhere on the visible text/whitespace still reaches the link
   underneath instead of being silently swallowed by a non-interactive
   span sitting on top of it (the bug this replaced: every sibling used
   to get bumped above the link indiscriminately, so only the empty gaps
   between elements were actually clickable). The row's *real* controls
   (checkbox, buttons, a form wrapping buttons) get z-index:2 -- one
   step above the link -- so they stay independently clickable. */
.roster-row-link {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    z-index: 1;
}

.roster-row > *:not(.roster-row-link) {
    position: relative;
}

.roster-row > button,
.roster-row > input,
.roster-row > select,
.roster-row > form,
.roster-row > a:not(.roster-row-link) {
    z-index: 2;
}

/* Spreadsheet import preview -- reuses .roster-row as its base but each
   row carries more than a single line (meta + skip flags), so it needs
   top alignment instead of .roster-row's default vertical centering. */
.import-row {
    align-items: flex-start;
}

.import-row-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.import-row-meta {
    color: var(--text-muted);
    font-size: 0.85rem;
}

.import-row-flags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    background: linear-gradient(160deg, var(--blue), var(--blue-dark));
    color: var(--on-accent);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-family: var(--font-display);
    font-size: 1rem;
    border: 2px solid var(--border);
}

.avatar-small {
    width: 26px;
    height: 26px;
    font-size: 0.75rem;
    border-width: 1px;
}

.topbar-profile-link {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none;
    color: var(--text);
    font-weight: 600;
}

.topbar-profile-link:hover {
    color: var(--blue-light);
}

.roster-name {
    font-weight: 600;
    flex: 1;
    min-width: 0;
}

.roster-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem 1rem;
    font-size: 0.9rem;
    color: var(--text-muted);
}

/* Custom checkbox/radio -- the native control's unchecked state ignores
   accent-color entirely in most browsers and just renders the OS
   default (a stark white square on this dark UI), which was the one
   place still reading as un-designed. appearance:none + a hand-drawn
   checked state fixes both states, board rows and the filters popover
   alike. */
.row-select,
.filter-check input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    width: 18px;
    height: 18px;
    border: 1px solid var(--border);
    border-radius: 5px;
    background: var(--bg);
    flex-shrink: 0;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s var(--ease-snap), border-color 0.15s var(--ease-snap);
}

.row-select {
    width: 20px;
    height: 20px;
}

.row-select:checked,
.filter-check input[type="checkbox"]:checked {
    background: var(--blue);
    border-color: var(--blue);
}

.row-select:checked::after,
.filter-check input[type="checkbox"]:checked::after {
    content: "";
    width: 5px;
    height: 9px;
    border: solid var(--on-accent);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg) translate(-1px, -1px);
}

.filter-check input[type="radio"] {
    appearance: none;
    -webkit-appearance: none;
    width: 18px;
    height: 18px;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--bg);
    flex-shrink: 0;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s var(--ease-snap), border-color 0.15s var(--ease-snap);
}

.filter-check input[type="radio"]:checked {
    border-color: var(--blue);
    border-width: 5px;
}

.row-select:focus-visible,
.filter-check input:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(94, 196, 255, 0.3);
}

.bulk-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.75rem 1rem;
    margin-bottom: 0.75rem;
}

.bulk-select-all {
    font-size: 0.9rem;
    white-space: nowrap;
}

.bulk-count {
    font-size: 0.85rem;
    color: var(--text-muted);
    font-weight: 600;
}

.bulk-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-left: auto;
}

.bulk-actions .field-select {
    width: auto;
    min-width: 160px;
}

.bulk-text-progress {
    font-size: 0.85rem;
    color: var(--text-muted);
    margin: 0 0 0.75rem;
}

.board-filters {
    margin-bottom: 1.25rem;
}

.board-search-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    position: relative;
}

.board-search-row input[type="search"] {
    flex: 1;
    min-width: 160px;
}

.filters-clear {
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* Compound .btn.btn-secondary / .btn.btn-danger (not bare .btn-secondary)
   on purpose -- these are always applied alongside .btn in the markup
   (class="btn btn-secondary"), and a bare single-class selector has the
   exact same specificity as .btn itself, so whichever rule happens to
   come later in the file silently wins on every shared property. That
   was this bug for the whole session: .btn is defined further down, so
   every "secondary"/"danger" button rendered as solid primary blue
   regardless of intent. The compound selector's higher specificity
   makes the modifier win unconditionally, independent of source order. */
.btn.btn-secondary {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text);
}

.btn.btn-secondary:hover {
    background: var(--surface-hover);
    border-color: rgba(94, 196, 255, 0.4);
}

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

.btn.btn-danger:hover {
    background: #ff9d9d;
    border-color: #ff9d9d;
}

/* Filters popover -- a <details> disclosure, same no-JS-required
   pattern as the topbar menu. Stage and profile-status are real
   multi-select checkboxes; rating and assignment stay single-choice
   (a radio group styled the same way) since their options overlap/are
   mutually exclusive rather than combinable. */
.filters-popover {
    position: relative;
}

.filters-popover summary {
    /* <summary> defaults to display:list-item -- list-style:none alone
       hides the marker glyph but some browsers still reserve its gutter
       width, shifting "Filters" a few px off-center from the plain
       <button>/<a> elements (Search, Export CSV) beside it. Forcing
       flex display removes the list-item box entirely so it centers
       exactly like every other .btn. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    list-style: none;
    user-select: none;
}

.filters-popover summary::-webkit-details-marker {
    display: none;
}

.filters-panel {
    position: absolute;
    top: calc(100% + 0.6rem);
    left: 0;
    z-index: 20;
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lg);
    padding: 1.1rem;
    width: min(320px, 82vw);
    max-height: 70vh;
    overflow-y: auto;
}

@media (max-width: 600px) {
    /* Anchoring to the trigger button's left edge (above) can push the
       panel past the right edge of a narrow viewport, depending on
       where the button lands once the search row wraps -- anchor to
       the viewport itself instead of the button below this width, so
       it can never overflow sideways regardless of trigger position.

       top and bottom are both pinned (not top+max-height) so the box's
       height is computed to exactly fill the space between them --
       with only `top: auto` and a max-height, a fixed-position element
       falls back to its static (in-flow) position, which can land low
       enough on a tall page that the bottom of the panel -- including
       the Apply button -- renders below the visible viewport. Since
       it's position:fixed, scrolling the page can't bring that back
       into view. Pinning both edges guarantees the whole panel is
       always on-screen and lets overflow-y:auto (above) handle
       anything that still doesn't fit. */
    .filters-panel {
        position: fixed;
        left: 1rem;
        right: 1rem;
        top: 1rem;
        bottom: 1rem;
        width: auto;
        max-height: none;
    }
}

.filters-group-label {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    margin: 1rem 0 0.5rem;
}

.filters-group-label:first-child {
    margin-top: 0;
}

.filters-checks {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.filter-check {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.9rem;
    cursor: pointer;
}


.filters-actions {
    margin-top: 1.1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}

.filters-actions .btn {
    width: 100%;
}

/* Ranked-board signals (Committee/Exec only) -- see pnm_list.html and
   views._can_view_aggregate. Gold marks the one recruit signal that's
   actually earned (enough reads to rank); everything else stays in the
   neutral/azure family so gold keeps meaning something. */
.rating-pill {
    display: inline-block;
    padding: 0.25rem 0.75rem;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    white-space: nowrap;
    background: linear-gradient(120deg, var(--gold-bright), var(--gold));
    color: var(--on-accent);
    border: none;
}

.rating-pill-muted {
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text-muted);
}

/* The board's simplified rating signal -- just the number, styled to
   actually stand out (unlike the compact text pill above, which still
   carries the fuller count/spread detail on the bid slate and the
   profile itself). A gold coin when ranked; a quiet placeholder dash
   when there's not enough data yet, without exposing the raw count on
   a screen meant for scanning, not analysis. */
.rating-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 42px;
    height: 42px;
    padding: 0 0.4rem;
    border-radius: 999px;
    font-weight: 800;
    font-size: 1.05rem;
    flex-shrink: 0;
    background: var(--gold);
    color: var(--on-accent);
}

.rating-badge-muted {
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text-muted);
    font-weight: 700;
    box-shadow: none;
}

/* A complete profile shows no badge at all -- this only ever renders
   for an incomplete one, so it's deliberately small and red: a "needs
   attention" flag, not another status pill competing for space. */
.profile-flag {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    font-size: 0.68rem;
    font-weight: 700;
    white-space: nowrap;
    color: var(--error-text);
    border: 1px solid rgba(255, 128, 128, 0.4);
    background: rgba(255, 128, 128, 0.12);
}

.assign-badge {
    display: inline-block;
    padding: 0.25rem 0.75rem;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    white-space: nowrap;
    color: var(--blue-light);
    border: 1px solid rgba(94, 196, 255, 0.4);
    background: rgba(94, 196, 255, 0.12);
}

.assign-badge-lg {
    /* Who's texting this recruit used to be a board column -- now it's
       only shown here, so it needs to read clearly the moment a profile
       opens rather than blend in as a small pill. */
    font-size: 0.88rem;
    padding: 0.35rem 0.9rem;
}

.assignment-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    margin: -0.5rem 0 1.5rem;
}

.reassign-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.reassign-form .field-select {
    flex: 1;
    min-width: 0;
}

.funnel-form {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    max-width: 420px;
    margin-bottom: 1.5rem;
}

.funnel-form .btn {
    align-self: flex-start;
}

.message-preview {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 480px;
}

.message-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.message-actions .btn {
    flex: 1;
    min-width: 120px;
}

.log-touchpoint-form {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
}

.log-touchpoint-form label {
    font-weight: 600;
    font-size: 0.9rem;
}

.token-buttons {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: -0.5rem;
}

.token-buttons-label {
    font-size: 0.85rem;
    color: var(--text-muted);
    margin-right: 0.25rem;
}

.template-preview {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 1.1rem;
}

.template-preview .label {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    margin: 0 0 0.35rem;
}

.template-preview .value {
    white-space: pre-wrap;
    margin: 0;
}

/* Status, not a control -- deliberately has none of .btn's chrome (no
   pill background, no border, no hover shine). A colored dot + plain
   label is the universal "this is information" pattern, so a stage
   never gets mistaken for something you can click, even sitting right
   next to buttons that actually are. One color per stage so the funnel
   still reads at a glance, not just from the label text. Interview
   uses brick rather than gold -- gold is reserved for the ranked/
   achievement signal specifically, and re-using it here would blur
   that rule. */
.stage-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 0.82rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--text-muted);
}

.stage-badge::before {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
}

.stage-badge.stage-active {
    color: var(--blue-light);
}
.stage-badge.stage-active::before {
    background: var(--blue);
    animation: stageLivePulse 2s ease-out infinite;
}
@keyframes stageLivePulse {
    0% { box-shadow: 0 0 0 0 rgba(94, 196, 255, 0.65); }
    70% { box-shadow: 0 0 0 7px rgba(94, 196, 255, 0); }
    100% { box-shadow: 0 0 0 0 rgba(94, 196, 255, 0); }
}
.stage-badge.stage-interview { color: var(--brick); }
.stage-badge.stage-bid_extended,
.stage-badge.stage-accepted { color: var(--success); }
.stage-badge.stage-declined,
.stage-badge.stage-released { color: var(--text-muted); }

@media (prefers-reduced-motion: reduce) {
    .stage-badge.stage-active::before { animation: none; }
}

.empty-state {
    color: var(--text-muted);
    padding: 2.5rem 1.5rem;
    text-align: center;
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    font-size: 0.95rem;
}

.pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    justify-content: center;
    margin-top: 1.5rem;
    font-size: 0.95rem;
}

.messages {
    list-style: none;
    margin: 0 0 1.5rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.message {
    padding: 0.75rem 1rem;
    border-radius: var(--radius-sm);
    font-size: 0.95rem;
    border: 1px solid var(--border);
    border-left: 3px solid var(--border);
    background: var(--surface);
}

.message-success {
    border-left-color: var(--success);
}

.message-error {
    border-left-color: var(--error);
    color: var(--error-text);
}

.detail-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 1.25rem;
    margin-bottom: 1.5rem;
}

.detail-header .avatar {
    width: 88px;
    height: 88px;
    font-size: 1.8rem;
}

.detail-header h1 {
    margin: 0 0 0.5rem;
}

.detail-header-badges {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.6rem 1.25rem;
    border: 1px solid var(--blue);
    border-radius: var(--radius-sm);
    background: var(--blue);
    color: var(--on-accent);
    font-family: inherit;
    font-weight: 700;
    font-size: 0.92rem;
    line-height: 1.3;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    /* <button> elements don't inherit font/text-align from the page by
       default in most browsers -- without these explicit, a <button
       class="btn"> (e.g. the reassign form) can render off-center
       relative to a plain <a class="btn"> link using the same class. */
    transition: background 0.2s var(--ease-snap), border-color 0.2s var(--ease-snap);
}

.btn:hover {
    background: var(--blue-light);
    border-color: var(--blue-light);
}

.btn:active {
    background: var(--blue-dark);
    border-color: var(--blue-dark);
}

.header-action {
    margin-left: auto;
    flex-shrink: 0;
}

.detail-header-actions {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
}

.back-link {
    display: inline-block;
    margin-bottom: 1rem;
    font-size: 0.9rem;
    text-decoration: none;
    color: var(--text-muted);
}

.detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    margin-bottom: 1.5rem;
}

.detail-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 1.1rem;
    transition: border-color 0.15s var(--ease-snap);
}

.detail-card:hover {
    border-color: rgba(94, 196, 255, 0.3);
}

.detail-card .label {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    margin: 0 0 0.35rem;
}

.detail-card .value {
    font-size: 1.25rem;
    font-weight: 700;
    margin: 0;
    overflow-wrap: anywhere;
}

.section-title {
    font-family: var(--font-display);
    font-size: 1.1rem;
    font-weight: 800;
    color: var(--text);
    margin: 2rem 0 0.75rem;
    display: block;
}

.answers-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.answers-list li {
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--border);
}

.answers-list .question {
    font-weight: 600;
    margin: 0 0 0.25rem;
}

.answers-list .answer {
    color: var(--text-muted);
    margin: 0;
}

.comment-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.comment-item {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.85rem 1rem;
}

.comment-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.85rem;
}

.comment-author {
    font-weight: 600;
}

.comment-rating {
    color: var(--blue);
    font-weight: 700;
}

.comment-date {
    color: var(--text-muted);
    margin-left: auto;
}

.comment-body {
    margin: 0.5rem 0 0;
    color: var(--text);
}

/* ---------- Rating gauge (pnm_detail's avg-rating widget) ---------- */

.rating-gauge {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}

.gauge-wrap {
    position: relative;
    width: 150px;
    margin: 0 auto;
}

.gauge {
    width: 100%;
    height: auto;
    overflow: visible;
    display: block;
}

.gauge-track {
    fill: none;
    stroke: var(--border);
    stroke-width: 10;
    stroke-linecap: round;
}

.gauge-fill {
    fill: none;
    stroke: var(--gold-bright);
    stroke-width: 10;
    stroke-linecap: round;
    stroke-dasharray: 157;
    filter: drop-shadow(0 0 7px rgba(255, 207, 107, 0.5));
    transition: stroke-dashoffset 1.1s var(--ease-snap) 0.15s;
}

.gauge-center {
    position: absolute;
    left: 50%;
    bottom: 2px;
    transform: translateX(-50%);
}

.gauge-value {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 1.75rem;
    color: var(--text);
    margin: 0;
}

.event-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.event-list li {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 0.3rem 0.8rem;
    font-size: 0.9rem;
}

.login-wrap {
    display: flex;
    align-items: center;
    /* See the matching comment in public.css -- plain `center` can clip
       content past the top edge with no way to scroll to it. */
    align-items: safe center;
    justify-content: center;
    min-height: 100vh;
    padding: 1.5rem;

    /* Same chapter-house treatment as the public kiosk pages (public.css)
       -- the login screen is as much a "front door" as those are, so it
       gets the same photographic backdrop for a consistent first
       impression. Duplicated here rather than shared because app.css and
       public.css are deliberately separate stylesheets for separate page
       families (see the file layout notes elsewhere in this repo). */
    background:
        linear-gradient(150deg, rgba(10, 18, 48, 0.78) 0%, rgba(22, 58, 104, 0.55) 45%, rgba(6, 9, 23, 0.92) 100%),
        url("../img/house-hero.432befa2f345.jpg") center 35% / cover no-repeat fixed;
}

.login-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    padding: 2.5rem 2rem;
    width: 100%;
    max-width: 380px;
    text-align: center;
}

.login-card h1 {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 1.7rem;
    letter-spacing: -0.01em;
    margin: 0 0 0.15rem;
}

.login-subtitle {
    color: var(--blue-light);
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    margin: 0 0 1.75rem;
}

.login-error {
    color: var(--error-text);
    font-size: 0.9rem;
    margin: 0 0 1.25rem;
}

.login-card form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    text-align: left;
}

.activation-step {
    /* The account-activation form nests each step's fields one level
       deeper than .login-card form's flex/gap reaches (form > .activation-
       step > field divs), so without this the name and password fields
       within a step would render with no gap between them at all --
       cramped and hard to tap accurately on a phone. */
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.login-card label {
    font-size: 0.9rem;
    font-weight: 600;
    margin-bottom: 0.3rem;
    display: block;
}

.login-card input {
    width: 100%;
    padding: 0.65em 0.85em;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--text);
    font-size: 1rem;
    transition: border-color 0.15s var(--ease-snap), box-shadow 0.15s var(--ease-snap);
}

.login-card input:focus {
    outline: none;
    border-color: var(--blue);
    box-shadow: 0 0 0 3px rgba(94, 196, 255, 0.2);
}

.login-card button {
    margin-top: 0.5rem;
    width: 100%;
    padding: 0.75em;
    border: 1px solid var(--blue);
    border-radius: var(--radius-sm);
    background: var(--blue);
    color: var(--on-accent);
    font-weight: 700;
    font-size: 1rem;
    cursor: pointer;
    transition: background 0.2s var(--ease-snap), border-color 0.2s var(--ease-snap);
}

.login-card button:hover {
    background: var(--blue-light);
    border-color: var(--blue-light);
}

.login-links {
    margin: 1.25rem 0 0;
    font-size: 0.85rem;
    text-align: center;
}

/* Mobile rating page */

.rating-form {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    max-width: 480px;
}

.rating-field label:not(.rating-label) {
    display: block;
    font-weight: 600;
    font-size: 0.95rem;
    margin-bottom: 0.4rem;
}

.rating-label {
    display: block;
    font-weight: 600;
    font-size: 0.95rem;
    margin-bottom: 0.6rem;
}

.field-input,
.field-select,
.field-textarea {
    width: 100%;
    font-size: 1rem;
    font-family: inherit;
    padding: 0.6em 0.85em;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    transition: border-color 0.15s var(--ease-snap), box-shadow 0.15s var(--ease-snap);
}

.field-input:focus,
.field-select:focus,
.field-textarea:focus {
    outline: none;
    border-color: var(--blue);
    box-shadow: 0 0 0 3px rgba(94, 196, 255, 0.2);
}

.field-textarea {
    resize: vertical;
    min-height: 5rem;
}

.field-errors {
    color: var(--error-text);
    font-size: 0.9rem;
    margin: 0.4rem 0 0;
}

.field-help {
    color: var(--text-muted);
    font-size: 0.82rem;
    margin: 0.4rem 0 0;
}

.field-help ul {
    margin: 0;
    padding-left: 1.1rem;
}

.field-help li {
    margin: 0.1rem 0;
}

.rating-form > button[type="submit"] {
    align-self: flex-start;
    font-size: 1rem;
    font-weight: 700;
    padding: 0.75em 1.9em;
    border: 1px solid var(--blue);
    border-radius: var(--radius-sm);
    background: var(--blue);
    color: var(--on-accent);
    cursor: pointer;
    min-height: 3rem;
    transition: background 0.2s var(--ease-snap), border-color 0.2s var(--ease-snap);
}

.rating-form > button[type="submit"]:hover {
    background: var(--blue-light);
    border-color: var(--blue-light);
}

/* Gem rating widget -- a Phi Delt-flavored star rating. Five radio
   inputs in reverse DOM order (5 first, 1 last) + `flex-direction:
   row-reverse` display them left-to-right as 1..5, which is what lets a
   plain CSS sibling selector fill "this gem and everything before it"
   on hover/select without any JS. Standard CSS star-rating trick.

   Azure, not gold -- this is an in-progress input a brother is actively
   giving, not an earned/achievement signal, so it follows the same
   "azure is primary interactive" rule as everything else. */

.gem-rating {
    display: flex;
    flex-direction: row-reverse;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.gem-rating input {
    /* Hidden but still real, tabbable, labeled radio inputs -- the label
       clicks/taps are the actual widget. */
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.gem-rating label {
    cursor: pointer;
    /* Generous padding around a 48px icon keeps the tap target well
       above the ~44px touch-target minimum for thumbs on a phone. */
    padding: 0.35rem;
    line-height: 0;
}

.gem-icon {
    width: 48px;
    height: 48px;
    transition: filter 0.15s ease;
}

.gem-icon .gem-body {
    fill: var(--bg);
    stroke: var(--border);
    stroke-width: 1.5;
    stroke-linejoin: round;
    transition: fill 0.15s ease, stroke 0.15s ease;
}

.gem-icon .gem-facets {
    fill: none;
    stroke: var(--border);
    stroke-width: 1;
    stroke-linejoin: round;
    opacity: 0.7;
    transition: stroke 0.15s ease, opacity 0.15s ease;
}

.gem-rating input:checked ~ label .gem-icon,
.gem-rating label:hover .gem-icon,
.gem-rating label:hover ~ label .gem-icon {
    /* A soft azure glow behind the gem, on top of the fill swap below --
       reads as "lit up" rather than just recolored. */
    filter: drop-shadow(0 0 6px rgba(94, 196, 255, 0.65));
}

.gem-rating input:checked ~ label .gem-icon .gem-body,
.gem-rating label:hover .gem-icon .gem-body,
.gem-rating label:hover ~ label .gem-icon .gem-body {
    fill: var(--blue);
    stroke: var(--blue-dark);
}

.gem-rating input:checked ~ label .gem-icon .gem-facets,
.gem-rating label:hover .gem-icon .gem-facets,
.gem-rating label:hover ~ label .gem-icon .gem-facets {
    stroke: rgba(255, 255, 255, 0.65);
    opacity: 1;
}

/* Topbar menu -- a native <details>/<summary> disclosure, so opening and
   closing it needs no JS at all (only the copy-to-clipboard button inside
   it, and the outside-click-to-close convenience, do). */

.menu {
    position: relative;
}

/* The hamburger trigger -- a plain muted icon button, same family as
   every other icon-only control in the app (.icon-btn). Gold stays
   reserved for the one earned signal (the rating badge) instead of
   also marking "here's the menu" -- one accent color, used for one
   thing, is what keeps it meaning something. */
.menu-trigger {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    list-style: none;
    user-select: none;
    transition: background 0.2s var(--ease-snap), color 0.2s var(--ease-snap), border-color 0.2s var(--ease-snap);
}

.menu-trigger svg {
    width: 20px;
    height: 20px;
}

.menu-trigger::-webkit-details-marker {
    display: none;
}

.menu-trigger:hover,
.menu[open] .menu-trigger {
    background: var(--surface-hover);
    color: var(--text);
    border-color: rgba(94, 196, 255, 0.4);
}

.menu-panel {
    position: absolute;
    top: calc(100% + 0.6rem);
    right: 0;
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lg);
    padding: 0.4rem;
    min-width: 220px;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    z-index: 20;
    /* Now up to 8 items for Exec -- cap the height and let it scroll
       internally rather than risk running off the bottom of a short
       viewport the way the filters panel used to. */
    max-height: min(70vh, 420px);
    overflow-y: auto;
}

@media (max-width: 600px) {
    /* Same fixed-sheet treatment as .filters-panel above, and for the
       same reason -- a fixed-position panel anchored with only `top`
       can't reliably stay fully on-screen once there are enough items
       to matter. Pinning top+bottom guarantees it. */
    .menu-panel {
        position: fixed;
        left: 1rem;
        right: 1rem;
        top: 1rem;
        bottom: 1rem;
        min-width: 0;
        max-height: none;
    }
}

/* Explicit close affordance inside the panel itself, alongside the
   existing tap-outside-to-close behavior -- first flex child, aligned
   to the row's own end (the panel is display:flex; flex-direction:
   column, so align-self:flex-end is the column-flex equivalent of
   floating it to the right) rather than position:absolute, so it takes
   up real space and pushes the menu items below it instead of
   overlapping the first one. */
.menu-close-btn {
    align-self: flex-end;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    background: none;
    border: none;
    border-radius: 50%;
    color: var(--text-muted);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    transition: background 0.2s var(--ease-snap), color 0.2s var(--ease-snap);
}

.menu-close-btn:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.menu-item {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    padding: 0.6rem 0.75rem;
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 0.9rem;
    font-family: inherit;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
}

.menu-item:hover {
    background: var(--surface-hover);
}

/* ---------- Quick-text popup ---------- */

.icon-btn {
    /* 44px, not the visually-tighter 34px it started as -- this button
       is the whole point of the quick-text feature (texting many
       recruits fast from a phone), so it needs to meet the ~44px
       recommended touch target, not just look tidy. */
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    flex-shrink: 0;
    transition: background 0.2s var(--ease-snap), color 0.2s var(--ease-snap), border-color 0.2s var(--ease-snap);
}

.icon-btn svg {
    width: 19px;
    height: 19px;
}

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

.quick-text-dialog {
    position: relative;
    border: none;
    border-radius: var(--radius);
    padding: 1.75rem;
    width: calc(100% - 2rem);
    max-width: 440px;
    max-height: 85vh;
    overflow-y: auto;
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-lg);
}

.quick-text-dialog::backdrop {
    background: rgba(5, 7, 17, 0.75);
}

.quick-text-dialog h2 {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 1.3rem;
    margin: 0 0 1.25rem;
    padding-right: 2.75rem;
}

.dialog-close {
    position: absolute;
    top: 0.7rem;
    right: 0.7rem;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    padding: 0.25rem;
}

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

.quick-text-dialog .rating-field {
    margin-bottom: 1rem;
}

.quick-text-dialog #quick-text-result {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
}

/* Copy-paste link box (event sign-in link) */

.copy-link-box {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--blue);
    border-radius: var(--radius-sm);
    padding: 1.1rem 1.2rem;
    margin-bottom: 1.75rem;
}

.copy-link-box label {
    display: block;
    font-weight: 600;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    margin-bottom: 0.5rem;
}

.copy-link-row {
    display: flex;
    gap: 0.5rem;
}

.copy-link-row .field-input {
    flex: 1;
    min-width: 0;
}

.copy-link-row .btn {
    flex-shrink: 0;
}

/* ---------- Crest badge (topbar, login, password pages) ---------- */

.crest-badge {
    width: 60px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.crest-badge img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.crest-badge-sm {
    width: 36px;
    height: 36px;
}

.crest-badge-lg {
    width: 84px;
    height: 84px;
    margin: 0 auto 1.5rem;
}

/* ---------- Narrow-viewport pass ---------- */

@media (max-width: 560px) {
    /* The topbar has to fit brand + nav link + menu + who's-logged-in +
       log out in one bar -- drop the two text labels that are least
       essential (the wordmark next to the crest, and the name next to
       the avatar) rather than let everything wrap into a tall, messy
       multi-line bar. The crest and avatar alone still say who/what
       they are. */
    .brand-text,
    .profile-name-text,
    .nav-link-text {
        display: none;
    }

    .topbar {
        padding: 0.75rem 1rem;
        gap: 0.6rem;
    }

    .topbar nav {
        gap: 0.5rem;
    }

    .nav-link-pill {
        padding: 0.5rem;
    }

    .nav-link-pill svg {
        width: 19px;
        height: 19px;
    }

    main {
        padding: 1.5rem 1rem 2.5rem;
    }

    h1 {
        font-size: 1.5rem;
    }

    .detail-header .avatar {
        width: 64px;
        height: 64px;
        font-size: 1.4rem;
    }

    .detail-header-actions {
        width: 100%;
    }

    .detail-header-actions .btn {
        flex: 1;
    }

    .roster-row {
        flex-wrap: wrap;
        row-gap: 0.6rem;
    }

    .roster-meta {
        justify-content: flex-start;
        width: 100%;
        order: 3;
        /* The meta badges wrap to their own full-width line below the
           name/avatar row on a phone -- trying to keep everything on
           one line is what made the board unreadable at this width. */
    }

    .board-search-row input[type="search"] {
        /* Forces its own full-width line -- flex-wrap then puts Search
           and Filters together on the line below, sharing it via
           flex:1 each, instead of three separate full-width rows. */
        flex-basis: 100%;
    }

    .board-search-row > .btn,
    .board-search-row > .filters-popover {
        flex: 1;
    }

    .filters-popover summary {
        width: 100%;
        text-align: center;
    }

    .funnel-form {
        max-width: none;
    }

    .funnel-form .btn {
        align-self: stretch;
    }

    .bid-slate-actions {
        width: 100%;
        order: 4;
        /* Its own line below the rating pill (order 3) -- three action
           buttons crammed onto the name row would be unreadable this
           narrow. */
    }

    .bulk-bar {
        flex-direction: column;
        align-items: stretch;
    }

    .bulk-actions {
        /* Four controls (Text selected / stage select / Apply / Clear)
           squeezed into a wrapping row via flex:1 each is what made this
           cramped -- full-width stacked rows instead, same reasoning as
           .funnel-form going full-width above. */
        margin-left: 0;
        width: 100%;
        flex-direction: column;
        align-items: stretch;
    }

    .bulk-actions .field-select,
    .bulk-actions .btn {
        width: 100%;
        flex: none;
    }
}
