/* =============================================================================
   BCC Alumni Tracker System - Design system
   A single stylesheet using CSS variables for theming (light + dark).
   ========================================================================== */

/* --- Design tokens (light theme) ----------------------------------------- */
:root {
    --primary: #16A34A;
    --primary-dark: #15803D;
    --primary-light: #DCFCE7;
    --primary-contrast: #FFFFFF;

    --background: #F5FAF7;
    --surface: #FFFFFF;
    --surface-alt: #EDF6F0;
    --text-primary: #0B1F14;
    --text-secondary: #5C7568;
    --border: #DDEBE2;
    --border-subtle: rgba(11, 31, 20, 0.07);

    --danger: #DC2626;
    --danger-light: #FEE2E2;
    --warning: #D97706;
    --warning-light: #FEF3C7;
    --info: #2563EB;
    --info-light: #DBEAFE;
    --success: #16A34A;
    --success-light: #DCFCE7;

    --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
    --shadow: 0 1px 3px rgba(15, 23, 42, 0.1), 0 1px 2px rgba(15, 23, 42, 0.06);
    --shadow-lg: 0 10px 25px rgba(15, 23, 42, 0.12);

    --radius-sm: 6px;
    --radius: 10px;
    --radius-lg: 16px;

    --sidebar-width: 260px;
    --topbar-height: 64px;

    --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    --transition: 160ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* --- Dark theme (deep emerald) ---------------------------------------------
   The default look: a rich forest-green surface set rather than a neutral
   slate, so the brand colour carries through the whole interface. */
:root[data-theme="dark"] {
    --primary: #22C55E;
    --primary-dark: #16A34A;
    --primary-light: #14532D;
    --primary-contrast: #04160B;

    --background: #061A0F;
    --surface: #0B2418;
    --surface-alt: #123021;
    --text-primary: #F0FDF4;
    --text-secondary: #8FB6A0;
    --border: #16382A;
    /* Full-length structural lines (sidebar edge, top bar) read much heavier
       than short card borders, so they get a softer colour. */
    --border-subtle: rgba(255, 255, 255, 0.045);

    --danger-light: #3A1A1A;
    --warning-light: #38290C;
    --info-light: #12283F;
    --success-light: #123D25;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55);
}

/* --- Reset / base ---------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html { font-size: 16px; -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font-family: var(--font);
    background: var(--background);
    color: var(--text-primary);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    transition: background var(--transition), color var(--transition);
}

a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }

/* --- Scrollbars ------------------------------------------------------------
   Slim, theme-aware scrollbars with a soft track and a brand-green thumb on
   hover. Firefox uses the two-value `scrollbar-color`; WebKit/Chromium uses
   the pseudo-elements below. */
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 999px;
    /* Transparent border + padding-box clip leaves breathing room around the
       thumb so it reads slimmer than the track. */
    border: 3px solid transparent;
    background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--primary); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }

h1, h2, h3, h4 { margin: 0 0 0.5rem; font-weight: 600; line-height: 1.25; }
p { margin: 0 0 1rem; }

img { max-width: 100%; display: block; }

/* --- Buttons --------------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.6rem 1.1rem;
    font-size: 0.9rem;
    font-weight: 500;
    font-family: inherit;
    border: 1px solid transparent;
    border-radius: var(--radius);
    cursor: pointer;
    transition: background var(--transition), border-color var(--transition), transform var(--transition), box-shadow var(--transition);
    white-space: nowrap;
    text-decoration: none;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }

.btn-primary { background: var(--primary); color: var(--primary-contrast); }
.btn-primary:hover { background: var(--primary-dark); }

.btn-secondary { background: var(--surface); color: var(--text-primary); border-color: var(--border); }
.btn-secondary:hover { background: var(--surface-alt); }

.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { filter: brightness(0.93); }

.btn-ghost { background: transparent; color: var(--text-secondary); }
.btn-ghost:hover { background: var(--surface-alt); color: var(--text-primary); }

.btn-sm { padding: 0.4rem 0.75rem; font-size: 0.8rem; }
.btn-block { width: 100%; }
.btn-icon { padding: 0.5rem; width: 40px; height: 40px; }

/* --- Forms ----------------------------------------------------------------- */
.form-group { margin-bottom: 1.1rem; }
.form-label { display: block; margin-bottom: 0.4rem; font-size: 0.85rem; font-weight: 500; color: var(--text-primary); }
/* Sub-label for the levels nested inside a grouped field (e.g. the PSGC address block). */
.form-label-sm { font-size: 0.78rem; font-weight: 400; color: var(--text-secondary); }

.form-control, .form-select, textarea.form-control {
    width: 100%;
    padding: 0.65rem 0.85rem;
    font-size: 0.9rem;
    font-family: inherit;
    color: var(--text-primary);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color var(--transition), box-shadow var(--transition);
}
.form-control:focus, .form-select:focus, textarea.form-control:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-light);
}
.form-control::placeholder { color: var(--text-secondary); opacity: 0.7; }
textarea.form-control { resize: vertical; min-height: 96px; }

.form-error { display: block; margin-top: 0.3rem; font-size: 0.8rem; color: var(--danger); }

/* Inline checkbox + label, e.g. the Terms and Conditions agreement. */
.form-check { display: flex; align-items: flex-start; gap: 0.55rem; font-size: 0.85rem; line-height: 1.45; color: var(--text-secondary); cursor: pointer; }
.form-check input[type="checkbox"],
.form-check input[type="radio"] { margin-top: 0.15rem; width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--primary); cursor: pointer; }

/* Groups related fields in long forms so they stay scannable. */
.form-section {
    margin: 0.35rem 0 0.85rem;
    padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--border);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-secondary);
}
.form-section:not(:first-child) { margin-top: 1.35rem; }
.form-control.is-invalid, .form-select.is-invalid { border-color: var(--danger); }
.form-hint { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.3rem; }

/* Required marker on a label. Aria-hidden in effect: the input carries its own
   `required`, so this is the visual cue only and must not be read as content. */
.req { color: var(--danger); font-weight: 700; margin-left: 0.1rem; }

.input-group { position: relative; }
.input-group .form-control { padding-right: 2.75rem; }
.input-group-icon {
    position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%);
    background: none; border: none; color: var(--text-secondary); cursor: pointer;
    display: flex; align-items: center; padding: 0.35rem;
}

/* --- Cards ----------------------------------------------------------------- */
.card {
    /* Grid and flex items default to min-width:auto, which refuses to shrink
       below the intrinsic width of their content. A card holding a wide table
       would therefore stretch past its column and drag the whole page into
       sideways scrolling, with .table-wrap's overflow never getting a chance to
       engage. Allowing the card to shrink puts the scrolling back inside it. */
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}
/* flex-wrap is load-bearing, not decoration: a header is a title block beside
   its controls and neither side gives way on its own — the buttons are nowrap
   and a header select carries a min-width — so on one unwrappable line the
   title block was crushed to a word per row while the control still overflowed
   the card (Reference Data's "Form Options" was the worst of them). Wrapping
   drops the controls to their own line instead, and min-width: 0 lets the title
   block use the full width once they have gone. */
.card-header { padding: 1.1rem 1.35rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.card-header > * { min-width: 0; }
.card-title { font-size: 1rem; font-weight: 600; margin: 0; }
.card-body { padding: 1.35rem; }

/* Two or more controls in a card header. A class rather than an inline
   `display:flex`, because the phone rules below have to override the widths and
   a style attribute cannot be reached by a media query. */
.card-header-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.card-header-actions > .form-select { flex: 0 1 190px; }

/* On a phone the two sides never share a line, so stack them outright rather
   than relying on the wrap: a title that fits by two characters would otherwise
   leave its button hanging off the right edge. align-items stays at the start
   so a lone badge or button keeps its natural width — stretching would blow a
   count badge across the whole card — while a control group is widened
   deliberately, and its select given the leftover room instead of a min-width
   wider than the screen. */
@media (max-width: 640px) {
    .card-header { flex-direction: column; align-items: flex-start; gap: 0.75rem; padding: 1rem 1.1rem; }
    .card-header > * { max-width: 100%; }
    .card-header-actions { width: 100%; }
    .card-header-actions > .form-select { flex: 1 1 10rem; min-width: 0; }
    .card-body { padding: 1.1rem; }
}

/* --- Badges ---------------------------------------------------------------- */
.badge {
    display: inline-flex; align-items: center; gap: 0.35rem;
    padding: 0.2rem 0.6rem; font-size: 0.75rem; font-weight: 500;
    border-radius: 999px; line-height: 1.4;
}
.badge-success { background: var(--success-light); color: var(--success); }
.badge-warning { background: var(--warning-light); color: var(--warning); }
.badge-danger { background: var(--danger-light); color: var(--danger); }
.badge-info { background: var(--info-light); color: var(--info); }
.badge-neutral { background: var(--surface-alt); color: var(--text-secondary); }
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; display: inline-block; flex-shrink: 0; }

/* --- Empty state (reusable) ------------------------------------------------ */
.empty-state { text-align: center; padding: 3.5rem 1.5rem; }
.empty-state-icon { width: 62px; height: 62px; margin: 0 auto 1.1rem; border-radius: 50%; display: grid; place-items: center; background: var(--surface-alt); color: var(--primary); }
.empty-state-icon svg { width: 28px; height: 28px; }
.empty-state h3 { font-size: 1.05rem; margin: 0 0 0.35rem; }
.empty-state p { color: var(--text-secondary); font-size: 0.9rem; margin: 0 auto; max-width: 360px; }

/* --- Avatars --------------------------------------------------------------- */
/* A photo when one is set, otherwise initials on a soft tint. */
.avatar { border-radius: 50%; background: var(--surface-alt); display: inline-grid; place-items: center; color: var(--primary); font-weight: 700; overflow: hidden; flex-shrink: 0; text-transform: uppercase; line-height: 1; }
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-sm { width: 38px; height: 38px; font-size: 0.82rem; }
.avatar-lg { width: 76px; height: 76px; font-size: 1.55rem; box-shadow: 0 0 0 1px var(--border); }

/* Photo picker on the self-service update form. */
.photo-field { display: flex; align-items: center; gap: 1rem; }
.photo-field-info { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; }

/* Custom file input: the native control is hidden and a styled button + the
   chosen filename stand in for it. The label wraps the input so a click (or
   keyboard activation) still opens the file dialog. */
.file-picker { display: inline-flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; cursor: pointer; }
.file-picker-input { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); border: 0; }
.file-picker-btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.55rem 0.95rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text-primary); font-size: 0.85rem; font-weight: 600; transition: border-color var(--transition), color var(--transition), background var(--transition); }
.file-picker-btn svg { width: 16px; height: 16px; }
.file-picker:hover .file-picker-btn { border-color: var(--primary); color: var(--primary); background: var(--surface-alt); }
.file-picker:focus-within .file-picker-btn { border-color: var(--primary); color: var(--primary); box-shadow: 0 0 0 3px var(--primary-light); }
.file-picker-name { font-size: 0.82rem; color: var(--text-secondary); word-break: break-word; }

/* --- Announcements --------------------------------------------------------- */
/* Alumni feed reads as a single column; the admin manager packs cards into a
   responsive grid that wraps down to one column on narrow screens. */
.announcement-feed { display: grid; gap: 1.1rem; max-width: 720px; }
.announcement-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); }

/* Card with a colour-coded accent rail. The rail colour is driven by --accent
   so admin cards can tint it per status while the alumni feed stays primary. */
.announcement-card { position: relative; overflow: hidden; transition: box-shadow var(--transition), transform var(--transition); }
.announcement-card::before { content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 4px; background: var(--accent, var(--primary)); }
.announcement-card:hover { box-shadow: var(--shadow); transform: translateY(-2px); }
.announcement-item { display: flex; gap: 1.1rem; padding: 1.35rem 1.5rem; }

/* Rounded icon tile that leads each card. */
.announcement-avatar { flex-shrink: 0; width: 44px; height: 44px; border-radius: 13px; display: grid; place-items: center; background: var(--primary-light); color: var(--primary); }
.announcement-avatar svg { width: 22px; height: 22px; }

.announcement-main { flex: 1; min-width: 0; }
.announcement-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; margin-bottom: 0.5rem; }
.announcement-head .badge { flex-shrink: 0; }
.announcement-title { font-size: 1.12rem; font-weight: 600; margin: 0; line-height: 1.35; }
.announcement-date { min-width: 0; font-size: 0.76rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
.announcement-body { font-size: 0.94rem; line-height: 1.7; color: var(--text-primary); margin: 0; }
.announcement-excerpt { font-size: 0.9rem; line-height: 1.6; color: var(--text-secondary); margin: 0.15rem 0 0; }
.announcement-foot { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.9rem; padding-top: 0.85rem; border-top: 1px solid var(--border); font-size: 0.8rem; color: var(--text-secondary); }
.announcement-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); }

/* Action buttons render as neutral outlined chips; the semantic colour (send /
   archive / delete) surfaces only on hover so the row stays calm at rest. */
.announcement-actions .btn { gap: 0.35rem; border: 1px solid var(--border); background: var(--surface); color: var(--text-secondary); }
.announcement-actions .btn svg { width: 14px; height: 14px; }
.announcement-actions .btn:hover { background: var(--surface-alt); color: var(--text-primary); }
.announcement-actions .btn-publish:hover { color: var(--primary); border-color: var(--primary); }
.announcement-actions .btn-archive:hover { color: var(--warning); border-color: var(--warning); }
.announcement-actions .btn-delete:hover { color: var(--danger); border-color: var(--danger); }

/* --- Alerts ---------------------------------------------------------------- */
.alert { padding: 0.8rem 1rem; border-radius: var(--radius); font-size: 0.88rem; margin-bottom: 1rem; border: 1px solid transparent; }
.alert-success { background: var(--success-light); color: var(--success); }
.alert-danger { background: var(--danger-light); color: var(--danger); }
.alert-info { background: var(--info-light); color: var(--info); }
/* Completes the set — the other three existed, so a state that is neither a
   success nor an error had nowhere to go. */
.alert-warning { background: var(--warning-light); color: var(--warning); }

/* --- Auth layout ----------------------------------------------------------- */
.auth-wrapper { min-height: 100vh; display: grid; grid-template-columns: 1fr 1fr; }

/* Emerald gradient panel with a soft glow toward the top-left. */
.auth-brand {
    position: relative;
    color: #fff;
    padding: 3.5rem;
    display: flex;
    flex-direction: column;
    justify-content: center;
    overflow: hidden;
    background:
        /* Glow concentrated toward the top-left. */
        radial-gradient(95% 75% at 20% 14%, rgba(34, 197, 94, 0.48) 0%, rgba(22, 163, 74, 0.14) 45%, transparent 72%),
        /* Vignette so the lower edge falls into near-black. */
        radial-gradient(120% 90% at 70% 100%, rgba(2, 12, 7, 0.65) 0%, transparent 60%),
        linear-gradient(165deg, #0A4F2E 0%, #05301C 45%, #02100A 100%);
}
.auth-brand > * { position: relative; z-index: 1; }
/* Soft green glow in the lower-right for depth. */
.auth-brand::after {
    content: ""; position: absolute; z-index: 0; pointer-events: none;
    width: 340px; height: 340px; right: -130px; bottom: -110px; border-radius: 50%;
    background: radial-gradient(circle, rgba(34, 197, 94, 0.20), transparent 70%);
}
.auth-brand h1 { color: #fff; font-size: 2.1rem; line-height: 1.2; max-width: 460px; }
.auth-brand p { color: rgba(240, 253, 244, 0.78); max-width: 420px; }

.auth-brand-logo { display: flex; align-items: center; gap: 1rem; margin-bottom: 2.5rem; font-size: 1.6rem; font-weight: 700; }
.auth-logo-mark {
    width: 84px; height: 84px; border-radius: 20px;
    background: #fff; display: grid; place-items: center; overflow: hidden;
    font-size: 2.4rem; flex-shrink: 0;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25), 0 12px 40px rgba(34, 197, 94, 0.45);
}
.auth-logo-mark img { width: 100%; height: 100%; object-fit: cover; }

.auth-features { list-style: none; margin: 2.25rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.9rem; }
.auth-features li { display: flex; align-items: center; gap: 0.9rem; font-size: 1rem; color: rgba(240, 253, 244, 0.94); }
.auth-feature-icon {
    width: 42px; height: 42px; flex-shrink: 0; border-radius: 12px; display: grid; place-items: center;
    background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.13);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.auth-features svg { width: 20px; height: 20px; color: #86EFAC; }

.auth-brand .auth-brand-footer {
    position: absolute; left: 3.5rem; bottom: 2.25rem; z-index: 2;
    font-size: 0.8rem; color: rgba(240, 253, 244, 0.5);
}

/* Form side */
.auth-panel { position: relative; display: flex; align-items: center; justify-content: center; padding: 2rem; background: var(--background); }

/* "Powered by <logo> ACRONYM" badge pinned to the foot of the sign-in panel. */
.auth-powered { position: absolute; bottom: 1.25rem; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 0.45rem; font-size: 0.8rem; color: var(--text-secondary); }
.auth-powered-icon { display: inline-flex; color: var(--primary); }
.auth-powered-icon svg { width: 15px; height: 15px; }
.auth-powered-name { font-weight: 700; letter-spacing: 0.02em; color: var(--text-primary); }
.auth-card {
    width: 100%; max-width: 430px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 2.25rem;
    box-shadow: var(--shadow-lg);
}
.auth-card h2 { font-size: 1.6rem; }
.auth-card .subtitle { color: var(--text-secondary); margin-bottom: 1.75rem; }
.auth-footer { margin-top: 1.5rem; text-align: center; font-size: 0.88rem; color: var(--text-secondary); }

/* Logo at the top of the sign-in card. Hidden on desktop (the brand panel
   already shows the logo there) and revealed on mobile where that panel is not. */
.auth-card-brand { display: none; flex-direction: column; align-items: center; gap: 0.6rem; margin-bottom: 1.85rem; }
.auth-card-logo {
    width: 84px; height: 84px; border-radius: 22px; background: #fff;
    display: grid; place-items: center; overflow: hidden; font-size: 2.4rem;
    box-shadow: 0 0 0 1px var(--border), 0 12px 34px rgba(34, 197, 94, 0.30);
}
.auth-card-logo img { width: 100%; height: 100%; object-fit: cover; }
.auth-card-name { font-size: 1.1rem; font-weight: 700; letter-spacing: 0.02em; color: var(--text-primary); }

@media (max-width: 860px) {
    .auth-wrapper { grid-template-columns: 1fr; }
    .auth-brand { display: none; }
    .auth-card { border: none; box-shadow: none; background: transparent; padding: 0; }
    /* Reveal the logo — it becomes the page's only branding on mobile. */
    .auth-card-brand { display: flex; margin-top: 0.5rem; margin-bottom: 2rem; }
    .auth-card-logo { width: 92px; height: 92px; }
}

/* --- Public shell (emailed links, no sign-in) ------------------------------ */
.public-shell { min-height: 100vh; display: flex; flex-direction: column; background: var(--background); }
.public-header { border-bottom: 1px solid var(--border-subtle); background: var(--surface); padding: 0.9rem 1.5rem; }
.public-brand { display: flex; align-items: center; gap: 0.6rem; font-weight: 700; font-size: 1rem; }
.public-brand img { width: 30px; height: 30px; border-radius: 8px; object-fit: cover; }
.public-brand .logo-mark { width: 30px; height: 30px; border-radius: 8px; background: var(--primary); color: #fff; display: grid; place-items: center; }
.public-content { flex: 1; width: 100%; max-width: 680px; margin: 0 auto; padding: 2rem 1.25rem; }
.public-footer { text-align: center; padding: 1.25rem; font-size: 0.8rem; color: var(--text-secondary); }

/* "Developed by" build credit, shared by the sign-in badge and the public
   footer. It sits beside the institution's own line rather than replacing it:
   the site belongs to the school, the code to whoever wrote it — so the two are
   separated by a dot instead of run together.

   nowrap keeps "Developed by Lx" whole while still allowing a break before it,
   which is what a narrow phone footer needs. The link inherits its colour so
   the one rule works on the public footer's themed surface and on the sign-in
   badge alike, and is underlined rather than recoloured for the same reason. */
.footer-credit { white-space: nowrap; }
.footer-credit::before { content: "\00b7"; margin: 0 0.45rem; opacity: 0.6; }
/* The </> mark. Left inline with a nudged baseline rather than made a flex item:
   the credit is a sentence, and flex would put a gap between "Developed by" and
   the link on top of the space already between the words. It inherits the
   muted colour instead of taking the accent the "Powered by" bolt beside it
   uses — one accent on that line is the point of it being an accent. */
.footer-credit-icon { width: 13px; height: 13px; vertical-align: -2px; margin-right: 0.15rem; opacity: 0.75; }
.footer-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; opacity: 0.85; }
.footer-credit a:hover,
.footer-credit a:focus-visible { opacity: 1; color: var(--primary); }

/* Key event details on the public RSVP page. */
.event-facts { display: flex; flex-direction: column; gap: 0.85rem; margin-top: 1rem; }
.event-facts > div { display: flex; align-items: flex-start; gap: 0.7rem; font-size: 0.92rem; }
.event-facts svg { width: 19px; height: 19px; flex-shrink: 0; color: var(--primary); margin-top: 2px; }

/* --- App shell ------------------------------------------------------------- */
.app { min-height: 100vh; }

.sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; width: var(--sidebar-width);
    background: var(--surface); border-right: 1px solid var(--border-subtle);
    display: flex; flex-direction: column; z-index: 40;
    transition: transform var(--transition);
}
.sidebar-brand { height: var(--topbar-height); display: flex; align-items: center; gap: 0.6rem; padding: 0 1.35rem; font-weight: 700; font-size: 1.05rem; border-bottom: 1px solid var(--border-subtle); }
.sidebar-brand .logo-mark { width: 32px; height: 32px; border-radius: 8px; background: var(--primary); color: #fff; display: grid; place-items: center; font-size: 1rem; }
.sidebar-nav { flex: 1; overflow-y: auto; padding: 1rem 0.75rem; }
.nav-section-title { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-secondary); padding: 0.75rem 0.75rem 0.35rem; }
.nav-item {
    display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0.75rem;
    border-radius: var(--radius); color: var(--text-secondary); font-size: 0.9rem; font-weight: 500;
    transition: background var(--transition), color var(--transition); margin-bottom: 0.15rem;
}
.nav-item:hover { background: var(--surface-alt); color: var(--text-primary); text-decoration: none; }
.nav-item.active { background: var(--primary-light); color: var(--primary-dark); }
:root[data-theme="dark"] .nav-item.active { color: var(--primary); }
.nav-item svg { width: 20px; height: 20px; flex-shrink: 0; }

.main { margin-left: var(--sidebar-width); min-height: 100vh; display: flex; flex-direction: column; }

.topbar {
    height: var(--topbar-height); position: sticky; top: 0; z-index: 30;
    background: var(--surface); border-bottom: 1px solid var(--border-subtle);
    display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0 1.5rem;
}
.topbar-left { display: flex; align-items: center; gap: 0.75rem; }
/* Mobile-only context. On desktop the fixed sidebar already highlights the
   current section, and keeping the title here would reserve layout space
   beside the search box — shifting it sideways on every page, since the
   title's width varies. Revealed only once the page heading scrolls away. */
.topbar-title {
    display: none;
    font-size: 1.05rem;
    font-weight: 600;
    opacity: 0;
    transform: translateY(4px);
    transition: opacity var(--transition), transform var(--transition);
    pointer-events: none;
}
.topbar-title.is-visible { opacity: 1; transform: none; pointer-events: auto; }

@media (max-width: 992px) {
    .topbar-title { display: inline; }
}
.topbar-actions { display: flex; align-items: center; gap: 0.5rem; }

/* --- Global search --------------------------------------------------------- */
.topbar-search { position: relative; width: min(380px, 34vw); }
.topbar-search input {
    width: 100%;
    height: 38px;
    padding: 0 2.25rem 0 2.4rem;
    font-size: 0.88rem;
    font-family: inherit;
    color: var(--text-primary);
    background: var(--surface-alt);
    border: 1px solid transparent;
    border-radius: 999px;
    transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
}
.topbar-search input::placeholder { color: var(--text-secondary); }
.topbar-search input:focus {
    outline: none;
    background: var(--surface);
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-light);
}
/* Hide the browser's native clear button so the layout stays stable. */
.topbar-search input::-webkit-search-cancel-button { display: none; }

.topbar-search-icon {
    position: absolute; left: 0.85rem; top: 50%; transform: translateY(-50%);
    width: 16px; height: 16px; color: var(--text-secondary); pointer-events: none;
}
.topbar-search-key {
    position: absolute; right: 0.6rem; top: 50%; transform: translateY(-50%);
    font-size: 0.7rem; font-family: inherit; color: var(--text-secondary);
    border: 1px solid var(--border); border-radius: 5px; padding: 0.05rem 0.35rem;
    pointer-events: none;
}
.topbar-search input:focus ~ .topbar-search-key { display: none; }

.search-results {
    position: absolute; top: calc(100% + 8px); left: 0; right: 0;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-lg);
    padding: 0.35rem; z-index: 60; max-height: 380px; overflow-y: auto;
}
.search-result {
    display: flex; flex-direction: column; gap: 0.1rem;
    padding: 0.55rem 0.7rem; border-radius: var(--radius-sm);
    color: var(--text-primary); text-decoration: none;
}
.search-result:hover, .search-result:focus { background: var(--surface-alt); text-decoration: none; }
.search-result strong { font-size: 0.87rem; font-weight: 600; }
.search-result span { font-size: 0.78rem; color: var(--text-secondary); }
.search-empty { padding: 0.9rem 0.7rem; font-size: 0.85rem; color: var(--text-secondary); text-align: center; }

/* --- Type-ahead (suggest.js) ----------------------------------------------
   Wraps a single input inside its .form-group, so it must add no height of its
   own or every field it mounts on would shift. .is-above is set by the module
   when the panel would be clipped by a scrolling ancestor — the alumni modal. */
.suggest { position: relative; display: block; }
.suggest-panel {
    position: absolute; top: calc(100% + 4px); left: 0; right: 0;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-lg);
    padding: 0.3rem; z-index: 70; max-height: 232px; overflow-y: auto;
}
.suggest-panel.is-above { top: auto; bottom: calc(100% + 4px); }
.suggest-item {
    display: block; width: 100%; text-align: left; border: 0; background: none;
    padding: 0.5rem 0.65rem; border-radius: var(--radius-sm);
    font: inherit; font-size: 0.87rem; color: var(--text-primary); cursor: pointer;
}
.suggest-item:hover, .suggest-item.is-active { background: var(--surface-alt); }

@media (max-width: 992px) { .topbar-search { display: none; } }

/* min-width:0 for the same reason as .card: .content is a flex item of .main. */
.content { flex: 1; min-width: 0; padding: 1.75rem; max-width: 1400px; width: 100%; }

/* --- Menu / avatar --------------------------------------------------------- */
.avatar { width: 38px; height: 38px; border-radius: 50%; background: var(--primary); color: #fff; display: grid; place-items: center; font-weight: 600; font-size: 0.85rem; }
.dropdown { position: relative; }
.dropdown-menu {
    position: absolute; right: 0; top: calc(100% + 8px); min-width: 200px;
    /* Never wider than the viewport, so the menu can't spill off-screen. */
    max-width: calc(100vw - 1.5rem);
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow-lg); padding: 0.4rem; opacity: 0; visibility: hidden; transform: translateY(-6px);
    transition: opacity var(--transition), transform var(--transition), visibility var(--transition); z-index: 50;
}
/* Left-align the menu under the trigger's left edge, for triggers that sit on
   the left/middle of a row (right:0 would push the menu off the left edge). */
.dropdown-menu-start { right: auto; left: 0; }

/* A page-header menu opens off the screen on a phone, and the header's own
   wrapping is what does it: on a wide screen the trigger sits at the right edge
   where right:0 is correct, but once the header wraps, the actions start a new
   line at the LEFT and right:0 hangs the entire menu off the left edge — every
   item in Reports' Export menu was cut in half. max-width does not save it,
   because the menu is not too wide, it is in the wrong place.

   Anchoring to the header rather than to the trigger avoids having to guess
   which edge the button ended up nearest, which changes with the number of
   actions and the length of the title: pinned to both edges the menu is on
   screen wherever the button lands, and it reads as a phone menu rather than a
   desktop popover that happens to fit. .dropdown has to give up its
   position:relative for that — an absolute box resolves against the nearest
   positioned ancestor — which is also why this is scoped to .page-header and
   not to dropdowns generally: the events list puts one inside a card, where the
   trigger really is at the right edge and the default is already right. */
@media (max-width: 640px) {
    .page-header { position: relative; }
    .page-header .dropdown { position: static; }
    .page-header .dropdown-menu { left: 0; right: 0; max-width: none; }
}

/* Inline "+ New" toggle for the Course/Department fields on the alumni form. */
.ref-toggle { float: right; background: none; border: none; padding: 0; cursor: pointer;
    color: var(--primary); font-size: 0.72rem; font-weight: 600; font-family: inherit; }
.ref-toggle:hover { text-decoration: underline; }
.dropdown.open .dropdown-menu { opacity: 1; visibility: visible; transform: translateY(0); }
.dropdown-item { display: flex; align-items: center; gap: 0.6rem; padding: 0.55rem 0.7rem; border-radius: var(--radius-sm); font-size: 0.88rem; color: var(--text-primary); width: 100%; background: none; border: none; cursor: pointer; text-align: left; font-family: inherit; }
.dropdown-item:hover { background: var(--surface-alt); text-decoration: none; }
.notif-menu { width: 320px; max-height: 420px; overflow-y: auto; }
.dropdown-divider { height: 1px; background: var(--border); margin: 0.35rem 0; }

/* --- Theme switcher "island" ---------------------------------------------- */
/* A segmented pill whose highlight slides to the active mode. Buttons are a
   fixed 34px wide so the indicator can translate by whole-button steps. */
.theme-island { position: relative; display: inline-flex; background: var(--surface-alt); border-radius: 999px; padding: 3px; }
.theme-island button {
    position: relative; z-index: 1; border: none; background: none; cursor: pointer;
    width: 34px; height: 30px; display: grid; place-items: center;
    color: var(--text-secondary); border-radius: 999px; transition: color 0.25s ease;
}
.theme-island button.active { color: var(--primary); }
.theme-island button:hover { color: var(--text-primary); }
.theme-island svg { width: 16px; height: 16px; }

.theme-island-indicator {
    position: absolute; top: 3px; left: 3px; width: 34px; height: 30px;
    border-radius: 999px; background: var(--surface); box-shadow: var(--shadow-sm);
    transition: transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.theme-island[data-active-theme="light"]  .theme-island-indicator { transform: translateX(0); }
.theme-island[data-active-theme="dark"]   .theme-island-indicator { transform: translateX(34px); }
.theme-island[data-active-theme="system"] .theme-island-indicator { transform: translateX(68px); }

/* Dynamic Island-style alert shown when the theme is switched. Always a dark
   pill with light text (the signature "island" look), regardless of theme. */
.theme-island-alert {
    position: fixed; top: 14px; left: 50%; z-index: 1200;
    display: inline-flex; align-items: center; gap: 0.5rem;
    padding: 0.5rem 1.1rem; border-radius: 999px;
    background: #0b0b0f; color: #fff;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
    font-size: 0.85rem; font-weight: 600; white-space: nowrap;
    pointer-events: none;
    opacity: 0; transform: translateX(-50%) translateY(-18px) scale(0.85);
    transition: opacity 0.25s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.theme-island-alert.show {
    opacity: 1; transform: translateX(-50%) translateY(0) scale(1);
}
.theme-island-alert svg { width: 16px; height: 16px; }

@media (prefers-reduced-motion: reduce) {
    .theme-island-alert { transition: opacity 0.2s ease; }
}

/* --- Stat cards ------------------------------------------------------------ */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
.stat-card { display: flex; align-items: center; gap: 1rem; padding: 1.25rem; }
.stat-icon { width: 48px; height: 48px; border-radius: var(--radius); display: grid; place-items: center; flex-shrink: 0; }
.stat-icon svg { width: 24px; height: 24px; }
.stat-value { font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
.stat-label { font-size: 0.82rem; color: var(--text-secondary); }

/* --- Progress bar ---------------------------------------------------------- */
.progress {
    height: 6px;
    border-radius: 999px;
    background: var(--surface-alt);
    overflow: hidden;
}
.progress-bar {
    height: 100%;
    border-radius: 999px;
    background: var(--primary);
    transition: width 400ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* --- List cards (surveys once used these; events do now) ------------------ */
.list-card .card-body { display: flex; flex-direction: column; height: 100%; }
.list-card-title { margin: 0 0 0.3rem; font-size: 1.05rem; line-height: 1.35; }
.list-card-desc {
    color: var(--text-secondary);
    font-size: 0.85rem;
    margin: 0 0 0.85rem;
    /* Keep every card the same height regardless of description length. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-height: 2.4em;
}
.list-meta {
    display: flex; align-items: center; gap: 0.4rem;
    font-size: 0.8rem; color: var(--text-secondary);
}
.list-progress {
    margin-top: 0.85rem;
    display: flex; flex-direction: column; gap: 0.35rem;
    font-size: 0.78rem; color: var(--text-secondary);
}
/* Push the actions to the bottom so buttons line up across the row. */
.list-card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: auto; padding-top: 1.1rem; }

/* --- Event cards (extend list-card with a status accent rail) ------------ */
/* The rail colour is driven by --accent, set per status on each card. */
.event-card { position: relative; overflow: hidden; transition: box-shadow var(--transition), transform var(--transition); }
.event-card::before { content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 4px; background: var(--accent, var(--primary)); }
.event-card:hover { box-shadow: var(--shadow); transform: translateY(-2px); }
.event-card .list-meta span { display: inline-flex; align-items: center; gap: 0.5rem; }
.event-card .list-meta svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--accent, var(--primary)); }
/* Registration tally sits in a soft tray to separate it from the meta above. */
.event-card .list-progress { background: var(--surface-alt); border-radius: var(--radius); padding: 0.7rem 0.85rem; }

/* Calendar-style date tile that anchors each event card and gives every card a
   pop of brand colour — the month strip stays green regardless of status. */
.event-card-head { display: flex; gap: 0.95rem; margin-bottom: 0.9rem; }
.event-head-main { flex: 1; min-width: 0; }
.event-head-main .list-card-title { margin: 0.45rem 0 0.2rem; }
.event-head-main .list-card-desc { margin: 0; min-height: 0; }
.event-date-tile { flex-shrink: 0; width: 54px; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); background: var(--surface); text-align: center; line-height: 1; box-shadow: var(--shadow-sm); }
.event-date-tile-month { background: var(--primary); color: #fff; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.28rem 0; }
.event-date-tile-day { font-size: 1.45rem; font-weight: 700; color: var(--text-primary); padding: 0.32rem 0 0.05rem; }
.event-date-tile-year { font-size: 0.6rem; color: var(--text-secondary); padding-bottom: 0.32rem; }

/* Section heading that groups related report cards. */
.section-heading { font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary); margin: 0 0 0.85rem; }

/* --- CSV import modal ------------------------------------------------------ */
.import-template { display: flex; gap: 0.85rem; align-items: flex-start; padding: 1rem; margin-bottom: 1.1rem;
    background: var(--surface-alt); border: 1px solid var(--border); border-radius: var(--radius); }
.import-template-icon { flex-shrink: 0; width: 38px; height: 38px; border-radius: 50%;
    display: grid; place-items: center; background: var(--primary-light); color: var(--primary); }

.file-drop { display: block; border: 2px dashed var(--border); border-radius: var(--radius);
    padding: 1.6rem 1rem; cursor: pointer; transition: border-color var(--transition), background var(--transition); }
.file-drop:hover { border-color: var(--primary); background: var(--surface-alt); }
.file-drop.dragover { border-color: var(--primary); background: var(--primary-light); }
.file-drop-idle { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; text-align: center; color: var(--text-secondary); }
.file-drop-idle svg { color: var(--primary); }
.file-drop-text { font-size: 0.9rem; }
.file-drop-text strong { color: var(--text-primary); }
.file-drop-hint { font-size: 0.78rem; opacity: 0.8; }
.file-drop-selected { display: flex; align-items: center; gap: 0.75rem; text-align: left; }
.file-drop-badge { flex-shrink: 0; width: 38px; height: 38px; border-radius: var(--radius-sm);
    display: grid; place-items: center; background: var(--primary-light); color: var(--primary); }
.file-drop-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-drop-size { font-size: 0.78rem; color: var(--text-secondary); }

.import-columns summary { cursor: pointer; color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.4rem; }
.import-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.import-chip { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.74rem; padding: 0.2rem 0.5rem;
    border-radius: var(--radius-sm); background: var(--surface-alt); color: var(--text-secondary); border: 1px solid var(--border); }
.import-chip-required { background: var(--primary-light); color: var(--primary); border-color: transparent; font-weight: 600; }

/* Skipped rows from the last upload. Warning-toned rather than danger: the
   import succeeded, and a duplicate that was skipped is the importer working. */
.import-result { margin-top: 1.1rem; padding: 0.85rem 1rem; border-radius: var(--radius);
    background: var(--surface-alt); border: 1px solid var(--border); border-left: 3px solid var(--warning); }
.import-result strong { display: block; font-size: 0.88rem; margin-bottom: 0.4rem; }
.import-result ul { margin: 0; padding-left: 1.1rem; max-height: 190px; overflow-y: auto;
    font-size: 0.82rem; color: var(--text-secondary); }
.import-result li { margin-bottom: 0.2rem; }

/* --- Grid utilities -------------------------------------------------------- */
.grid { display: grid; gap: 1.25rem; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 1024px) { .grid-cols-3 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px) { .grid-cols-2, .grid-cols-3 { grid-template-columns: 1fr; } }

/* Uneven two-column page splits. These live here rather than in a style
   attribute on the element: an inline grid-template-columns cannot be
   overridden by a media query, so pages using one stayed two columns wide even
   on a phone, squeezing form fields into unusable slivers. */
.grid-split     { grid-template-columns: 1.1fr 1fr; align-items: start; }
.grid-split-alt { grid-template-columns: 1fr 1.2fr; align-items: start; }
@media (max-width: 860px) {
    .grid-split, .grid-split-alt { grid-template-columns: 1fr; }
}

/* --- Tables ---------------------------------------------------------------- */
.table-wrap { overflow-x: auto; border-radius: var(--radius-lg); }
.table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.table th, .table td { text-align: left; padding: 0.8rem 1rem; border-bottom: 1px solid var(--border); white-space: nowrap; }
.table th { font-weight: 600; color: var(--text-secondary); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; background: var(--surface-alt); }
.table tbody tr:hover { background: var(--surface-alt); }
.table td.wrap { white-space: normal; }

/* --- Row actions -----------------------------------------------------------
   Actions in a table row are peers, so they get one weight: neutral outlined
   chips, with the semantic colour surfacing only on hover. Mixing filled,
   outlined and bare buttons made Edit shout and left Archive looking as
   destructive as a permanent delete. Generalised from .announcement-actions,
   which already worked this way.

   The intent classes carry the meaning: is-primary for the safe main action,
   is-warning for something reversible (archive), is-danger for what cannot be
   undone. That distinction is the point — archiving is not deleting. */
.row-actions { display: inline-flex; align-items: center; gap: 0.35rem; }
/* Transparent rather than filled: the chips sit inside a row that already has
   its own background and a hover background of its own, and a filled chip would
   fight both. It also means one rule works in either theme. */
.row-actions .btn {
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text-secondary);
    padding: 0.32rem 0.7rem;
    font-size: 0.78rem;
    gap: 0.3rem;
}
.row-actions .btn:hover { color: var(--text-primary); border-color: var(--text-secondary); background: transparent; }
.row-actions .btn svg { width: 14px; height: 14px; }
.row-actions .is-primary:hover { color: var(--primary); border-color: var(--primary); }
.row-actions .is-warning:hover { color: var(--warning); border-color: var(--warning); }
.row-actions .is-danger:hover  { color: var(--danger);  border-color: var(--danger); }
/* Square chips for the reorder arrows, so they read as one control pair. */
.row-actions .is-step { padding: 0.32rem 0.5rem; min-width: 26px; }
/* Destructive text outside a row-actions group (e.g. "Remove photo"), so the
   colour is a class rather than a repeated inline style. */
.btn-danger-text { color: var(--danger); }
.btn-danger-text:hover { color: var(--danger); background: var(--danger-light); }

/* --- Dynamic Island alerts --------------------------------------------------
   A pill that drops from the top centre, springs open to reveal its content,
   then collapses back. Intentionally dark in both themes, like the original. */
.island-host {
    position: fixed;
    top: 14px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 200;
    display: flex;
    justify-content: center;
    pointer-events: none;
}

.island {
    --island-accent: #30D158;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    height: 46px;
    width: 46px;                 /* collapsed pill; JS animates to content width */
    padding: 0;
    background: #0A0A0C;
    color: #F5F5F7;
    border-radius: 999px;
    /* Inset ring separates the pill from dark backgrounds without affecting
       layout width (a real border would change the measured size). */
    box-shadow:
        0 10px 30px rgba(0, 0, 0, 0.38),
        0 2px 8px rgba(0, 0, 0, 0.28),
        inset 0 0 0 1px rgba(255, 255, 255, 0.09);
    overflow: hidden;
    white-space: nowrap;
    pointer-events: auto;
    cursor: pointer;
    opacity: 0;
    transform: translateY(-26px) scale(0.82);
    /* Springy expansion, quick fade. */
    transition:
        width 560ms cubic-bezier(0.32, 1.48, 0.46, 1),
        transform 560ms cubic-bezier(0.32, 1.48, 0.46, 1),
        opacity 240ms ease;
    will-change: width, transform, opacity;
}
.island.is-open { opacity: 1; transform: translateY(0) scale(1); }
.island.is-closing { opacity: 0; transform: translateY(-18px) scale(0.88); }

.island-icon {
    flex: 0 0 34px;
    width: 34px;
    height: 34px;
    margin-left: 6px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--island-accent);
    color: #08130C;
    transform: scale(0.6);
    transition: transform 420ms cubic-bezier(0.32, 1.48, 0.46, 1) 60ms;
}
.island.is-open .island-icon { transform: scale(1); }
.island-icon svg { width: 18px; height: 18px; }

.island-body {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-right: 18px;
    min-width: 0;
    max-width: min(64vw, 340px);
    opacity: 0;
    transform: translateX(-8px);
    transition: opacity 260ms ease 140ms, transform 300ms ease 140ms;
}
.island.is-open .island-body { opacity: 1; transform: none; }

.island-title {
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
}
.island-text {
    font-size: 0.76rem;
    line-height: 1.3;
    color: rgba(245, 245, 247, 0.6);
    overflow: hidden;
    text-overflow: ellipsis;
}

.island-success { --island-accent: #30D158; }
.island-error   { --island-accent: #FF453A; }
.island-info    { --island-accent: #0A84FF; }
.island-error .island-icon,
.island-info .island-icon { color: #FFFFFF; }

@media (max-width: 480px) {
    .island-host { top: 10px; }
    .island-body { max-width: 62vw; }
}

/* Respect users who prefer reduced motion: show things immediately rather
   than animating them in. */
@media (prefers-reduced-motion: reduce) {
    .island, .island-body, .island-icon,
    .modal-backdrop, .modal, .dropdown-menu, .topbar-title, .sidebar {
        transition-duration: 1ms !important;
        animation-duration: 1ms !important;
    }
}

/* --- Modals ---------------------------------------------------------------- */
.modal-backdrop { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.55); display: flex; align-items: center; justify-content: center; padding: 1rem; z-index: 90; opacity: 0; visibility: hidden; transition: opacity var(--transition), visibility var(--transition); }
.modal-backdrop.open { opacity: 1; visibility: visible; }
/* Set while any modal is up, so the page behind cannot be scrolled out from
   under it. modal.js adds the scrollbar's width back as padding, since hiding
   the overflow removes the bar and would otherwise shift the page sideways. A
   long modal still scrolls on its own: .modal is capped at 90vh with its own
   overflow-y. */
body.modal-open { overflow: hidden; }
.modal { background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); width: 100%; max-width: 540px; max-height: 90vh; overflow-y: auto; transform: translateY(10px); transition: transform var(--transition); }
.modal-backdrop.open .modal { transform: translateY(0); }
.modal-header { padding: 1.1rem 1.35rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.modal-body { padding: 1.35rem; }
.modal-footer { padding: 1.1rem 1.35rem; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 0.6rem; }

/* --- QR code panel ---------------------------------------------------------
   Shared by both scan modals (one alumni's link, and the public join link) via
   partials/qr-modal.php, so the two cannot drift apart.

   The whole design problem here is that a QR code has to stay black-on-white to
   scan — it is the one element that cannot follow the theme. Left bare on a
   dark surface it reads as a hole punched in the dialog. So the code is given
   an explicit white "plate" with its own padding and border: the white then
   looks like paper deliberately placed on the surface, which is also what it
   becomes when printed. The plate's colours are fixed rather than themed — in
   light mode the border is what separates white paper from a white dialog, and
   in dark mode the plate already contrasts on its own. The padding is the part
   that matters for scanning: it is the quiet zone the code needs, on top of the
   margin the generator draws. */
.qr-panel { display: flex; flex-direction: column; align-items: center; gap: 0.9rem; text-align: center; }
.qr-intro { color: var(--text-secondary); font-size: 0.85rem; line-height: 1.55; margin: 0; max-width: 34ch; }
.qr-plate {
    background: #FFFFFF;
    border: 1px solid #E2E8F0;
    border-radius: var(--radius);
    padding: 0.85rem;
    box-shadow: var(--shadow-sm);
    /* Squares only: the plate never grows past the image it frames. */
    line-height: 0;
}
.qr-plate img { width: 232px; max-width: 100%; height: auto; display: block; }
/* The URL under the code is a fallback for a camera that will not focus, so it
   has to be readable and hand-typable: monospace, and broken anywhere rather
   than overflowing the dialog. .dialog-url is the same treatment for the link
   Modal.alert({ link }) shows when the clipboard was blocked — the update token
   is 64 hex characters, which as prose has no break opportunity at all and so
   runs straight out of a 440px box. */
.qr-url,
.dialog-url {
    display: block;
    width: 100%;
    background: var(--surface-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.5rem 0.65rem;
    color: var(--text-secondary);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.72rem;
    line-height: 1.5;
    overflow-wrap: anywhere;
    margin: 0;
}
/* The link inside is followable, which is the way out of a blocked clipboard:
   opening it puts the URL in the address bar, where the browser's own copy
   works. So it has to read as a link — coloured, and underlined on hover — while
   keeping the monospace box's job of being copyable by eye, which is why it is
   not underlined at rest: a permanent underline through a wrapped 90-character
   token buries the characters being transcribed. */
.dialog-url a { color: var(--primary); text-decoration: none; }
.dialog-url a:hover,
.dialog-url a:focus-visible { text-decoration: underline; }
/* A state note inside a dropdown, not an item: it explains the items under it
   rather than being clickable itself. */
.dropdown-note {
    margin: 0; padding: 0.5rem 0.75rem;
    background: var(--warning-light); color: var(--warning);
    font-size: 0.73rem; line-height: 1.45;
}
/* Says the code is still good but the door is shut, so a coordinator does not
   print a poster that lands every scanner on a notice. Warning-coloured rather
   than red: nothing is broken and nothing was lost. */
.qr-closed {
    display: flex; align-items: flex-start; gap: 0.4rem; text-align: left; margin: 0;
    background: var(--warning-light); color: var(--warning);
    border-radius: var(--radius-sm); padding: 0.5rem 0.6rem;
    font-size: 0.73rem; line-height: 1.5;
}
.qr-closed svg { flex-shrink: 0; margin-top: 0.15rem; }
/* Sets the expectation that "Download PNG" saves a card rather than the bare
   code on screen — otherwise the extra branding looks like a surprise. */
.qr-note { display: flex; align-items: flex-start; gap: 0.4rem; color: var(--text-secondary); font-size: 0.73rem; line-height: 1.5; text-align: left; margin: 0; }
.qr-note svg { flex-shrink: 0; margin-top: 0.15rem; }
/* Who or what the code opens — the one line that differs between the two
   modals, so it is styled as a badge rather than buried in the paragraph. */
.qr-subject { display: inline-flex; align-items: center; gap: 0.4rem; background: var(--primary-light); color: var(--primary-dark); border-radius: 999px; padding: 0.3rem 0.75rem; font-size: 0.78rem; font-weight: 600; max-width: 100%; }
.qr-subject span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
:root[data-theme="dark"] .qr-subject { color: var(--text-primary); }
@media (max-width: 480px) {
    .qr-plate img { width: 190px; }
}

/* --- Confirm / prompt dialogs ---------------------------------------------- */
.dialog-backdrop { z-index: 120; } /* Above ordinary modals it may be raised from. */
.dialog-icon {
    width: 42px; height: 42px; flex-shrink: 0;
    border-radius: 50%; display: grid; place-items: center;
}
.dialog-icon svg { width: 20px; height: 20px; }
.dialog-icon-danger  { background: var(--danger-light);  color: var(--danger); }
.dialog-icon-warning { background: var(--warning-light); color: var(--warning); }
.dialog-icon-primary { background: var(--primary-light); color: var(--primary); }
:root[data-theme="dark"] .dialog-icon-primary { color: var(--primary); }

/* --- Misc helpers ---------------------------------------------------------- */
.page-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.page-title { font-size: 1.4rem; font-weight: 700; margin: 0; }
.page-subtitle { color: var(--text-secondary); font-size: 0.9rem; }

/* The identity block of a record header: back arrow, avatar, name, meta line.
   The flex-basis is what makes the header wrap instead of crushing. With
   `flex: 1` the basis is 0, so on a phone the block shrinks to a sliver beside
   the status badge and Actions menu rather than pushing them onto the next
   line — .page-header already has flex-wrap, but wrapping never triggers
   because the block is willing to be 40px wide. The meta line's
   overflow-wrap: anywhere then breaks an email one character per row.
   A real basis means the two cannot share a line on a narrow screen, so the
   actions drop below; min-width: 0 still lets the block shrink once it has the
   line to itself, so a long email wraps at sensible points instead of pushing
   the page sideways. */
.page-identity { display: flex; align-items: center; gap: 0.75rem; flex: 1 1 20rem; min-width: 0; }
.text-muted { color: var(--text-secondary); }
.text-success { color: var(--success); }
.text-danger { color: var(--danger); }
.text-center { text-align: center; }
.mt-1 { margin-top: 0.5rem; } .mt-2 { margin-top: 1rem; } .mt-3 { margin-top: 1.5rem; }
.mb-0 { margin-bottom: 0; } .mb-2 { margin-bottom: 1rem; }
.flex { display: flex; } .items-center { align-items: center; } .justify-between { justify-content: space-between; } .gap-2 { gap: 0.75rem; }
.hidden { display: none !important; }
/* Archived reference rows stay listed but read as retired, not current. */
.is-archived td { opacity: 0.55; }
.spinner { width: 18px; height: 18px; border: 2px solid rgba(255,255,255,0.4); border-top-color: #fff; border-radius: 50%; animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* --- Mobile sidebar -------------------------------------------------------- */
.sidebar-backdrop { position: fixed; inset: 0; background: rgba(15,23,42,0.5); z-index: 39; opacity: 0; visibility: hidden; transition: opacity var(--transition); }
.menu-toggle { display: none; }

@media (max-width: 992px) {
    .sidebar { transform: translateX(-100%); }
    .sidebar.open { transform: translateX(0); box-shadow: var(--shadow-lg); }
    .sidebar-backdrop.open { opacity: 1; visibility: visible; }
    .main { margin-left: 0; }
    .menu-toggle { display: grid; }
    .content { padding: 1.25rem; }
}

/* The account name and role are the widest thing in the bar. They have to go
   before the layout gets tight, which happens well above phone sizes: between
   roughly 993px and 1100px the sidebar is still pinned open AND the search box
   is still shown, leaving the bar short of room. The avatar continues to
   identify the account, and the full name and email remain in the menu. */
@media (max-width: 1100px) {
    .topbar-user-meta { display: none; }
}

/* --- Phone-sized top bar ---------------------------------------------------
   The bar holds a menu button, page title, notifications, a three-way theme
   switch and the account button. At full size those add up to more than a
   phone is wide, which pushed the account button off screen and left the whole
   page scrolling sideways. Below, the widest pieces give way: the account name
   and role drop out (the avatar still identifies the account, and the name is
   repeated inside the menu), the title truncates instead of pushing, and the
   spacing tightens. Every control stays reachable. */
@media (max-width: 640px) {
    .topbar { padding: 0 0.85rem; gap: 0.5rem; }
    .topbar-actions { gap: 0.15rem; }

    /* Let the title shrink and ellipsis rather than force the bar wider. */
    .topbar-left { min-width: 0; flex: 1; }
    .topbar-title {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 0.98rem;
    }

    .topbar-actions .dropdown > .btn-ghost { padding: 0.3rem; gap: 0 !important; }

    /* Logo/favicon rows: the preview plus both buttons are wider than a phone,
       so let the buttons drop below the preview instead of overflowing. */
    .asset-picker { flex-wrap: wrap; }

    /* Top bar menus hang from their button via right:0, which aligns them to
       the button rather than to the screen. The notifications button sits in
       the middle of the bar, so its 320px panel ran off the left edge. Pinning
       these to the viewport keeps them on screen whatever the button's
       position, and the max-width stops them outgrowing a narrow phone. */
    .topbar-actions .dropdown-menu {
        position: fixed;
        top: calc(var(--topbar-height) + 6px);
        right: 0.75rem;
        left: auto;
    }

    /* The notifications panel is the wide one. Pinning both edges makes it span
       the screen exactly, rather than deriving a width from 100vw — which
       counts the scrollbar and so overshot the usable width on narrow phones.
       The account menu keeps its natural width; it already fits. */
    .topbar-actions [data-notifications] .dropdown-menu {
        left: 0.75rem;
        width: auto;
    }
}
