/*
 * portal.css — Layout and custom component styles shared by all portal wrappers.
 * Bootstrap handles: buttons, form-control, tables, badges, alerts, modals.
 * This file owns: sidebar/layout structure, nav, app grid, stats grid, forms
 * (card/row/group/actions), invite links, toggles, onboarding lists, toast,
 * search dropdown, join/need-invite cards, modals, responsive collapse.
 *
 * If styles aren't applying:
 *   1. Verify this file is linked in App.razor (<link rel="stylesheet" href="css/portal.css" />)
 *   2. Verify StaticWebAssetsFingerprintContent is false in MassIV.csproj
 *   3. Hard-refresh browser (Ctrl+Shift+R) — old CSS may be cached
 */

/* ===== PORTAL LAYOUT STRUCTURE ===== */

/* html/body have never had a background-color set anywhere in this app — every page
   only ever painted over them with .public-page, .main-content, etc., so the gap was
   invisible. The mobile top nav's new rounded bottom corners (.family-nav,
   border-radius: 0 0 16px 16px) cut into its own rectangle, exposing whatever sits
   behind it at the very top of the viewport — since .family-nav is position:fixed
   and out of flow, that's html/body's actual default background, not .main-content's
   themed one. Without this, the corner cutouts show the browser's plain white default
   instead of the app's neutral content color. Matches the same #eceef2 fallback used
   everywhere else in this file (.main-content, .public-page, --content-bg's default)
   so it's consistent even though it can't see a per-org --content-bg override here —
   that variable is set inline on .main-content itself, a descendant, and custom
   properties don't flow back up to ancestors like html/body. */
html, body {
    background-color: var(--content-bg, #eceef2);
}

.page {
    display: flex;
    min-height: 100vh;
}

/* --- Sidebar --- */
.sidebar {
    /* CSS variables default to the portal.css palette; inline styles on the element
       override them with per-org colors when set in FamilyLayout/PLOAdminLayout. */
    --sidebar-bg:         #e4e6ea;
    --sidebar-accent:     #3498db;
    /* Injected by SidebarStyle — white on dark backgrounds, dark on light backgrounds.
       Without this, dark sidebar colors make nav links unreadable. */
    --sidebar-text-color: #222222;
    width: 250px;
    background: var(--sidebar-bg, #e4e6ea);
    color: #333;
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    position: relative;
    /* Transition lives here (not in the media query) so the slide-out animation
       also plays on close, not just on open. */
    transition: transform 0.28s ease;
}

/* Accent color stripe across the very top of the sidebar — the only color accent
   on an otherwise neutral surface, so the eye anchors to it immediately.
   Uses var(--sidebar-accent) so per-org brand colors flow through automatically. */
.sidebar::before {
    content: '';
    display: block;
    height: 3px;
    background: linear-gradient(90deg, var(--sidebar-accent, #3498db) 0%, var(--sidebar-accent, #3498db) 100%);
    flex-shrink: 0;
}

.brand {
    padding: 20px;
    font-size: 1.4em;
    font-weight: 800;
    letter-spacing: -0.02em;
    border-bottom: 1px solid #e0e2e5;
    color: var(--sidebar-text-color, #0f0f1a);
}

.brand-subtitle {
    font-size: 0.7em;
    opacity: 0.7;
}

.nav-menu {
    display: flex;
    flex-direction: column;
    padding: 10px 0;
}

.nav-menu .nav-link {
    display: flex;
    align-items: center;
    padding: 12px 20px;
    color: var(--sidebar-text-color, #222);
    text-decoration: none;
    /* border-left: transparent reserves the 3px so padding doesn't shift on hover */
    border-left: 3px solid transparent;
    transition: background 0.18s, color 0.18s, border-color 0.18s, padding-left 0.18s, transform 150ms ease;
    gap: 10px;
}

.nav-menu .nav-link:hover {
    background: #e8f4fd;
    color: #1a6fa8;
    /* Accent bar grows in and text slides right — tactile "entering" feel */
    border-left-color: var(--sidebar-accent, #3498db);
    padding-left: 24px;
}

/* Active nav link: accent-colored left border marks the current page.
   Uses var(--sidebar-accent) so per-org brand colors flow through automatically. */
.nav-menu .nav-link.active {
    background: #e8f4fd;
    color: #1a6fa8;
    border-left: 3px solid var(--sidebar-accent, #3498db);
    font-weight: 500;
}

/* Bootstrap Icons <i> element inside nav links — sized for vector icons, not emoji */
.nav-menu .nav-link i.bi {
    font-size: 1rem;
    width: 20px;
    text-align: center;
    flex-shrink: 0;
}

/* --- Main Content Area --- */
.main-content {
    /* CSS variables default to the portal.css palette; inline styles on the element
       override them with per-org colors when set in FamilyLayout/PLOAdminLayout. */
    --content-bg:         #eceef2;
    /* Injected by MainContentStyle — white on dark page backgrounds, dark on light.
       Without this, a dark ContentPageColor makes page headings unreadable. */
    --content-text-color: #1a1a2e;
    flex: 1;
    display: flex;
    flex-direction: column;
    background: var(--content-bg, #eceef2);
    min-width: 0; /* prevent flex overflow */
}

.top-bar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 60px;
    padding: 0 30px;
    background: #fff;
    border-bottom: 1px solid #e1e4e8;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    flex-shrink: 0;
}

.top-bar-left {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.82rem;
    font-weight: 500;
    letter-spacing: 0.04em;
    color: #444;
    text-transform: uppercase;
}

/* ===== HAMBURGER BUTTON =====
   Hidden on desktop — only flex-displayed inside the mobile media query below.
   Clicks are handled by event delegation in massiv-interop.js, not inline onclick. */

/* Phase 1 — "ringing phone" shake: scale up while rotating back and forth so the
   eye immediately locks on. Runs once at page load with a 300ms head-start so the
   animation isn't swallowed by the initial render. */
@keyframes hamburger-attention {
    0%   { transform: scale(1)    rotate(0deg);   }
    8%   { transform: scale(4)    rotate(-14deg); }
    16%  { transform: scale(4)    rotate(14deg);  }
    24%  { transform: scale(4)    rotate(-11deg); }
    32%  { transform: scale(4)    rotate(9deg);   }
    40%  { transform: scale(2.5)  rotate(-5deg);  }
    52%  { transform: scale(1.2)  rotate(2deg);   }
    65%  { transform: scale(0.95) rotate(0deg);   }
    80%  { transform: scale(1.04) rotate(0deg);   }
    100% { transform: scale(1)    rotate(0deg);   }
}

/* Phase 2 — slow heartbeat pulse: fires 3 times immediately after the shake
   so the eye keeps tracking the button for ~3 more seconds before it rests. */
@keyframes hamburger-pulse {
    0%, 100% { transform: scale(1);    }
    50%      { transform: scale(1.22); }
}

/* Fades a subtle grey background in at the start of the sequence and out
   at the end. Duration 4000ms covers the full 1s shake + 3×1s pulse window. */
@keyframes hamburger-bg {
    0%   { background-color: transparent; }
    8%   { background-color: rgba(120,120,120,0.13); }
    90%  { background-color: rgba(120,120,120,0.13); }
    100% { background-color: transparent; }
}

.hamburger-btn {
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    background: none;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    padding: 5px;
    flex-shrink: 0;
    animation: hamburger-attention 1000ms ease-in-out 300ms 1 both,
               hamburger-pulse 1000ms ease-in-out 1300ms 3 both,
               hamburger-bg 4000ms ease 300ms 1 both;
    transform-origin: center;
}

/* Three CSS-drawn bars replace the bi-list font icon so each line can
   animate independently during the pulse phase. */
.hb-line {
    display: block;
    width: 22px;
    height: 2.5px;
    background: #444;
    border-radius: 2px;
    transform-origin: center;
}

/* Outer lines fan out and shrink while the middle line stretches —
   synced to the same 1000ms/3-iteration timing as hamburger-pulse
   so the spread peaks exactly when the button is at full scale. */
@keyframes hb-spread-top {
    0%, 100% { transform: translateY(0)    scaleX(1);   }
    50%      { transform: translateY(-5px) scaleX(0.6); }
}
@keyframes hb-spread-mid {
    0%, 100% { transform: scaleX(1);   }
    50%      { transform: scaleX(1.4); }
}
@keyframes hb-spread-bot {
    0%, 100% { transform: translateY(0)   scaleX(1);   }
    50%      { transform: translateY(5px) scaleX(0.6); }
}

.hb-line-1 { animation: hb-spread-top 1000ms ease-in-out 1300ms 3 both; }
.hb-line-2 { animation: hb-spread-mid 1000ms ease-in-out 1300ms 3 both; }
.hb-line-3 { animation: hb-spread-bot 1000ms ease-in-out 1300ms 3 both; }

/* ===== MOBILE DRAWER BACKDROP =====
   Sits behind the open sidebar at z-index 299. The CSS sibling combinator drives
   visibility — no JS needed to show/hide this element. When .sidebar-open is
   removed, the sibling rule no longer matches and the backdrop hides itself. */
.sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.42);
    z-index: 299;
    cursor: pointer;
}

.sidebar.sidebar-open ~ .sidebar-backdrop {
    display: block;
}

.top-bar-right {
    display: flex;
    align-items: center;
    gap: 15px;
    font-size: 0.9em;
    color: #555;
}

.top-bar-right a {
    color: #e74c3c;
    text-decoration: none;
    font-weight: 500;
}

.top-bar-right a:hover {
    text-decoration: underline;
}

.content-area {
    padding: 36px;
    flex: 1;
}

/* ===== PUBLIC LAYOUT ===== */
.public-page {
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #eceef2;
}

.public-content {
    max-width: 500px;
    width: 100%;
    padding: 20px;
}

/* ===== NEED INVITE / PUBLIC LANDING CARD ===== */
.need-invite-card {
    background: #fff;
    border-radius: 12px;
    padding: 40px;
    text-align: center;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}

.need-invite-card h1 {
    font-size: 1.8em;
    margin-bottom: 10px;
    color: #1a1a2e;
}

.need-invite-card p {
    color: #555;
    line-height: 1.6;
    margin-bottom: 15px;
}

.need-invite-card .contact-link {
    color: #3498db;
    text-decoration: none;
    font-weight: 500;
}

/* ===== DASHBOARD HEADER ===== */
.dashboard-header h1 {
    font-size: 1.6em;
    color: var(--content-text-color, #1a1a2e);
    margin-bottom: 5px;
}

.dashboard-header p {
    color: var(--content-text-color, #777);
    opacity: 0.75;
    margin-bottom: 25px;
}

/* ===== APP SELECTOR GRID =====
   Used on the GW Admin top-level dashboard (/gw) to show available apps as tiles. */
.app-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 20px;
    margin-top: 10px;
}

.app-tile {
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 25px;
    background: #fff;
    border: 2px solid #e1e4e8;
    border-radius: 12px;
    text-decoration: none;
    color: inherit;
    transition: all 0.2s ease;
    cursor: pointer;
}

.app-tile:hover {
    border-color: #3498db;
    box-shadow: 0 4px 15px rgba(52, 152, 219, 0.15);
    transform: translateY(-2px);
}

/* Greyed-out tile for apps that aren't built yet */
.app-tile-coming-soon {
    opacity: 0.5;
    cursor: default;
    pointer-events: none;
}

.app-tile-icon {
    font-size: 2.5em;
    flex-shrink: 0;
}

.app-tile-info h3 {
    margin: 0 0 5px 0;
    font-size: 1.2em;
    color: #1a1a2e;
}

.app-tile-info p {
    margin: 0 0 8px 0;
    font-size: 0.85em;
    color: #666;
    line-height: 1.4;
}

.app-status {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 12px;
    font-size: 0.75em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.app-status-active {
    background: #d4edda;
    color: #155724;
}

.app-status-planned {
    background: #e2e3e5;
    color: #6c757d;
}

/* ===== STATS GRID =====
   Used on dashboards for quick stat count cards. */
.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 20px;
    margin-top: 10px;
}

/* ===== MEMBER GRID & CARD =====
   Used on /family/my-family for guardian and student profile cards.
   Grid formula and card chrome are intentionally identical to OurFamilies'
   .family-grid / .family-card (OurFamilies.razor.css) so both pages look the same.
   Kept here (global) rather than in a scoped file so future pages can reuse them
   without touching OurFamilies. */
.member-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
    margin-top: 12px;
}

.member-card {
    background: #fff;
    border: 1px solid #e1e4e8;
    border-radius: 10px;
    padding: 18px 20px;
    transition: box-shadow 0.15s, border-color 0.15s;
}

.member-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
    border-color: #c0c4ce;
}

/* Name pill side-by-side on the card header row */
.member-card-top-row {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 8px;
    margin-bottom: 8px;
}

.member-card-name {
    font-size: 1rem;
    font-weight: 600;
    color: #1a1a2e;
}

.member-card-detail {
    font-size: 0.9rem;
    color: #555;
    margin-bottom: 2px;
}

/* Muted "Joined …" line at the bottom of each card */
.member-card-meta {
    font-size: 0.78em;
    color: #aaa;
    margin-top: 6px;
}

.stat-card {
    background: #fff;
    border: 1px solid #e1e4e8;
    border-radius: 10px;
    padding: 25px;
    text-align: center;
}

.stat-number {
    font-size: 2.2em;
    font-weight: 700;
    color: #1a1a2e;
    line-height: 1;
    margin-bottom: 5px;
}

.stat-label {
    font-size: 0.85em;
    color: #777;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* ===== MINI STAT TILES =====
   Compact horizontal widget: icon circle + number/label. Designed to look
   correct whether there's 1 tile or 6 — unlike .stats-grid, tiles do not
   stretch to fill the row; they hug their content and wrap naturally.
   Accent colors are a FIXED content-area palette, independent of org
   Primary/Accent theme colors (which only affect nav/sidebar). */

.mini-stats-row {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    margin-top: 10px;
}

.mini-stat-tile {
    display: flex;
    align-items: center;
    gap: 14px;
    background: #fff;
    border: 1px solid #e1e4e8;
    border-radius: 12px;
    padding: 14px 22px;
    min-width: 220px;
    max-width: 320px;
    text-decoration: none;
    color: inherit;
    transition: box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
}

a.mini-stat-tile:hover {
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
    transform: translateY(-2px);
    border-color: #d1d5db;
}

.mini-stat-icon {
    width: 46px;
    height: 46px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 1.3rem;
    color: #fff;
}

/* Fixed content-area accent palette — semantic, not brand-derived.
   amber = action needed (matches .pending-status-banner's existing amber)
   blue  = informational / default
   green = positive / completed
   purple = governance (matches the Elections #8e44ad accent)
   teal  = events / calendar */
.mini-stat-icon-amber  { background: #f59e0b; }
.mini-stat-icon-blue   { background: #3498db; }
.mini-stat-icon-green  { background: #27ae60; }
.mini-stat-icon-purple { background: #8e44ad; }
.mini-stat-icon-teal   { background: #16a085; }

.mini-stat-body {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.mini-stat-number {
    font-size: 1.6em;
    font-weight: 700;
    line-height: 1.15;
    color: #1a1a2e;
}

.mini-stat-label {
    font-size: 0.78em;
    color: #777;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    white-space: nowrap;
}

/* ===== SIDEBAR SECTION LABELS =====
   D48 — upgraded to match .nav-menu .nav-link's format/size: no
   text-transform/letter-spacing, full opacity, and no explicit font-size so
   this inherits the same ambient size .nav-link does (neither .nav-menu nor
   .sidebar sets a font-size, so both end up at the same inherited value).
   font-weight:600 stays — without some weight difference these read as
   plain links instead of group headers, since case/spacing/opacity/size no
   longer separate them from the links underneath. */
.nav-section-label {
    padding: 15px 20px 5px 20px;
    color: var(--sidebar-text-color, #999);
    font-weight: 600;
}

/* ===== COLLAPSIBLE NAV GROUPS =====
   <details>/<summary> handles open/close natively — the browser hides all
   non-summary children when [open] is absent, so no JS or display toggling
   is needed. The arrow is a pure CSS border-chevron that rotates on open. */
.nav-group {
    /* Reset browser margin on <details> */
    margin: 0;
}

.nav-group > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
    user-select: none;
    list-style: none; /* Firefox: hides default disclosure marker */
}

.nav-group > summary::-webkit-details-marker {
    display: none; /* Chrome / Safari */
}

.nav-group > summary:focus {
    outline: none;
}

/* Chevron: a border box whose bottom-right corner points right = collapsed.
   Uses currentColor so it respects org sidebar branding. */
.nav-group > summary::after {
    content: '';
    display: block;
    width: 6px;
    height: 6px;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform 250ms ease;
    opacity: 0.75;
    flex-shrink: 0;
    margin-right: 4px;
}

/* Rotate corner to point down when group is open */
.nav-group[open] > summary::after {
    transform: rotate(45deg);
}

/* Fade + slide links in when a group opens */
@keyframes nav-group-expand {
    from { opacity: 0; transform: translateY(-5px); }
    to   { opacity: 1; transform: translateY(0); }
}

.nav-group[open] > .nav-link {
    animation: nav-group-expand 180ms ease;
}

/* ===== REQUISITION DETAIL PANEL ===== */
.detail-label {
    font-size: 0.78em;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: #888;
    margin-bottom: 3px;
}

.detail-value {
    font-size: 0.95em;
    color: #1a1a2e;
}

/* Icon + muted uppercase label above each mini-card */
.req-mini-header {
    font-size: 0.72em;
    letter-spacing: 0.8px;
    color: #888;
    text-transform: uppercase;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 5px;
    margin-bottom: 6px;
    margin-top: 4px;
}

/* Light grey card body that visually groups each data section */
.req-mini-card {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 14px;
    margin-bottom: 12px;
}

/* Five-column stats bar for the PLO Requisitions page — overridden to 2 columns on mobile */
.req-stats-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 16px;
    margin-bottom: 20px;
}

/* Two-column panel grid — stacks to one column on mobile */
.req-detail-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 28px;
}

@media (max-width: 768px) {
    .req-detail-grid {
        grid-template-columns: 1fr !important;
    }
}

/* ===== BREADCRUMB NAV ===== */
.breadcrumb-link {
    color: #3498db;
    text-decoration: none;
    font-weight: 600;
}

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

.breadcrumb-separator {
    margin: 0 8px;
    color: #ccc;
    font-size: 1.1em;
}

/* ===== BRAND LINK ===== */
.brand-link {
    color: #1a1a2e;
    text-decoration: none;
}

.brand-link:hover {
    opacity: 0.9;
}

/* ===== ORG BRANDING =====
   Logo image in the sidebar brand area — constrained to fit without distortion. */
.brand-logo {
    max-height: 48px;
    max-width: 180px;
    object-fit: contain;
    display: block;
}

/* On desktop the plain portal text label is visible; hide the org brand swap.
   On mobile (media query below) this is reversed — org logo/shortname shows instead. */
.top-bar-org-brand {
    display: none;
}

/* Logo thumbnail inside the mobile top bar */
.top-bar-org-logo {
    max-height: 28px;
    max-width: 120px;
    object-fit: contain;
}

/* ===== BRAND PREVIEW (PLO Settings color picker) =====
   Side-by-side mini mockup: sidebar + content area.
   --preview-bg, --preview-accent, --preview-content-bg, --preview-text,
   --preview-content-text are all set inline by Blazor @oninput. */
.brand-preview {
    margin-top: 16px;
    border: 1px solid #e1e4e8;
    border-radius: 8px;
    overflow: hidden;
    width: 280px;
    display: flex;
}

.brand-preview-sidebar {
    background: var(--preview-bg, #e4e6ea);
    padding: 8px 0;
    width: 110px;
    flex-shrink: 0;
}

.brand-preview-brand {
    padding: 8px 12px;
    font-weight: 700;
    font-size: 0.85em;
    color: var(--preview-text, #0f0f1a);
    border-bottom: 1px solid rgba(0, 0, 0, 0.10);
    margin-bottom: 4px;
}

.brand-preview-nav-item {
    padding: 6px 12px;
    font-size: 0.75em;
    color: var(--preview-text, #444);
}

.brand-preview-nav-active {
    background: rgba(255, 255, 255, 0.20);
    border-left: 3px solid var(--preview-accent, #3498db);
    font-weight: 500;
}

/* Content area panel in the live preview */
.brand-preview-content {
    background: var(--preview-content-bg, #eceef2);
    padding: 10px 12px;
    flex: 1;
}

.brand-preview-heading {
    font-size: 0.78em;
    font-weight: 700;
    color: var(--preview-content-text, #1a1a2e);
    margin-bottom: 3px;
}

.brand-preview-subheading {
    font-size: 0.68em;
    color: var(--preview-content-text, #777);
    opacity: 0.75;
}

/* Logo upload preview box in PLO Settings */
.brand-logo-preview {
    width: 220px;
    height: 88px;
    border: 1px solid #e1e4e8;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f8f9fa;
    overflow: hidden;
    margin-bottom: 8px;
}

.brand-logo-preview-img {
    max-height: 76px;
    max-width: 200px;
    object-fit: contain;
}

.brand-logo-placeholder {
    color: #999;
    font-size: 0.85em;
}

/* ===== TABLE LAYOUT HELPERS ===== */
/* .row-archived and .action-cell are layout concerns Bootstrap doesn't handle */
.row-archived {
    opacity: 0.5;
}

.action-cell {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}

.table-actions {
    margin-bottom: 20px;
}

/* Recent Transactions table (/plo/finances) needs a desktop min-width so its 5
   columns don't cram together on a slightly narrow desktop window — but that same
   min-width, if set inline on the <table> element, would have out-specificity'd the
   mobile table-to-card transform's width:100% (inline styles beat any class-based
   media-query rule), forcing the table to stay desktop-width and require horizontal
   scrolling on mobile instead of collapsing into cards like every other table.
   Scoping this to min-width:769px (one pixel above the app's max-width:768px mobile
   breakpoint) means the constraint simply doesn't exist below that width, so the
   global mobile card transform is free to take over with nothing fighting it. */
@media (min-width: 769px) {
    .fin-transactions-table {
        min-width: 540px;
    }

    /* Same fix, same reasoning, for the larger 9-column transaction list table on
       /plo/finances/transactions (M20) — its inline min-width:700px had the same
       inline-beats-media-query problem as Recent Transactions' did. */
    .txn-list-table {
        min-width: 700px;
    }

    /* M26 — CSV import preview table (/plo/finances/import) had the same inline
       min-width:600px on the <table> element, which would have out-specificity'd the
       mobile card transform's width:100% the same way Recent Transactions' and
       Transaction List's did. Moved here so it only applies at desktop widths. */
    .import-preview-table {
        min-width: 600px;
    }
}

/* ===== FORMS =====
   Bootstrap provides .form-control, .form-select, and .row/.col for layout.
   These custom classes complement Bootstrap — they add card containers, section
   headers, and the two-column form-row pattern without requiring Bootstrap's
   full grid (which uses percentage-based columns instead of equal flex splits). */
.form-card {
    background: #fff;
    border: 1px solid #e1e4e8;
    border-radius: 10px;
    padding: 30px;
}

.form-section-title {
    font-size: 1.1em;
    color: #1a1a2e;
    margin: 0 0 15px 0;
    padding-bottom: 8px;
    border-bottom: 1px solid #eee;
}

.form-group {
    margin-bottom: 18px;
}

.form-group label {
    display: block;
    font-size: 0.85em;
    font-weight: 600;
    color: #555;
    margin-bottom: 5px;
}

.form-control-inline {
    padding: 5px 8px;
    border: 1px solid #d1d5db;
    border-radius: 4px;
    font-size: 0.85em;
    width: 100%;
    box-sizing: border-box;
}

.form-row {
    display: flex;
    gap: 20px;
}

.form-row .form-group {
    flex: 1;
}

.form-info {
    font-size: 0.85em;
    color: #555;
    background: #f0f7ff;
    padding: 8px 12px;
    border-radius: 6px;
    margin-top: 5px;
}

.form-card + .form-card {
    margin-top: 20px;
}

.form-actions {
    margin-top: 25px;
    display: flex;
    gap: 10px;
    align-items: center;
}

.required {
    color: #e74c3c;
}

/* Duplicate family warning card — amber border to signal "pause and read this" */
.duplicate-warning {
    border-left: 4px solid #d97706;
    background: #fffbeb;
}

/* ===== TOGGLE SWITCH (iOS-style, CSS only) ===== */
.toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 0;
    border-bottom: 1px solid #f0f0f0;
    gap: 20px;
}

.toggle-info {
    flex: 1;
}

.toggle-info strong {
    display: block;
    margin-bottom: 3px;
    color: #333;
}

.toggle-info p {
    color: #777;
    font-size: 0.85em;
    margin: 0;
}

.toggle-switch {
    position: relative;
    display: inline-block;
    width: 50px;
    height: 28px;
    flex-shrink: 0;
    cursor: pointer;
}

.toggle-switch input {
    opacity: 0;
    width: 0;
    height: 0;
    position: absolute;
}

.toggle-slider {
    position: absolute;
    cursor: pointer;
    top: 0; left: 0; right: 0; bottom: 0;
    background: #ccc;
    border-radius: 28px;
    transition: 0.25s;
}

.toggle-slider:before {
    position: absolute;
    content: "";
    height: 22px;
    width: 22px;
    left: 3px;
    bottom: 3px;
    background: white;
    border-radius: 50%;
    transition: 0.25s;
    box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}

input:checked + .toggle-slider {
    background: #3498db;
}

input:checked + .toggle-slider:before {
    transform: translateX(22px);
}

/* ===== ONBOARDING CONFIG PAGE LISTS ===== */
.onboarding-fixed-list {
    margin: 0 0 0 20px;
    padding: 0;
    color: #555;
    line-height: 1.9;
    font-size: 0.9em;
}

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

.preview-item {
    padding: 8px 12px;
    border-radius: 6px;
    margin-bottom: 6px;
    font-size: 0.9em;
}

.preview-on  { background: #d4edda; color: #155724; }
.preview-off { background: #f5f5f5; color: #999; }

.file-selected {
    font-size: 0.8em;
    color: #27ae60;
    margin-left: 10px;
}

.radio-group {
    display: flex;
    gap: 20px;
}

.radio-group label {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-weight: normal;
    cursor: pointer;
}

/* ===== MODAL / CONFIRM DIALOG =====
   Custom overlay — Bootstrap modals require JS initialization which conflicts
   with Blazor's rendering model. This CSS-only approach works with @onclick. */
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
}

.modal-card {
    background: #fff;
    border-radius: 12px;
    padding: 30px;
    max-width: 450px;
    width: 90%;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}

.modal-card h3 {
    margin: 0 0 10px 0;
    color: #1a1a2e;
}

.modal-card p {
    color: #555;
    line-height: 1.5;
    margin-bottom: 20px;
}

.modal-actions {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
}

.modal-close-btn {
    position: absolute;
    top: 14px;
    right: 14px;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 50%;
    color: #aaa;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}
.modal-close-btn:hover:not(:disabled) {
    background: #f0f0f0;
    color: #333;
}
.modal-close-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

/* ===== REMINDER CARD ===== */
.reminder-card {
    background: #fff8e1;
    border: 1px solid #ffecb3;
    border-radius: 10px;
    padding: 20px 25px;
}

.reminder-card h3 {
    margin: 0 0 10px 0;
    color: #e65100;
    font-size: 1.1em;
}

.reminder-card p {
    margin: 0 0 8px 0;
    font-size: 0.9em;
    color: #555;
    line-height: 1.5;
}

.reminder-card a {
    color: #3498db;
    font-weight: 500;
}

.reminder-card code {
    background: rgba(0,0,0,0.06);
    padding: 2px 6px;
    border-radius: 3px;
    font-size: 0.85em;
}

/* ===== SEARCHABLE DROPDOWN ===== */
.search-dropdown {
    position: relative;
}

.search-results {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: #fff;
    border: 1px solid #d1d5db;
    border-top: none;
    border-radius: 0 0 6px 6px;
    max-height: 250px;
    overflow-y: auto;
    z-index: 100;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.search-result-item {
    padding: 9px 12px;
    cursor: pointer;
    font-size: 0.9em;
    color: #333;
    border-bottom: 1px solid #f5f5f5;
}

.search-result-item:hover {
    background: #e8f4fd;
}

.search-result-code {
    color: #999;
    font-size: 0.85em;
}

.search-result-hint {
    padding: 8px 12px;
    font-size: 0.8em;
    color: #999;
    font-style: italic;
}

/* ===== JOIN ORGANIZATION CARD ===== */
.join-card {
    background: #fff;
    border-radius: 12px;
    padding: 40px;
    text-align: center;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
    max-width: 500px;
    margin: 0 auto;
}

.join-card h1 {
    font-size: 1.8em;
    margin-bottom: 10px;
    color: #1a1a2e;
}

.join-card p {
    color: #555;
    line-height: 1.6;
    margin-bottom: 15px;
}

.join-icon {
    font-size: 3em;
    margin-bottom: 15px;
}

.join-icon-success {
    color: #27ae60;
}

.join-icon-error {
    color: #e74c3c;
}

/* ===== INVITE LINKS ===== */

/* Monospace display so invite codes are clearly distinguishable */
.invite-code {
    font-family: 'Courier New', Courier, monospace;
    font-weight: 700;
    font-size: 1em;
    letter-spacing: 1px;
    background: #f0f0f5;
    padding: 3px 8px;
    border-radius: 4px;
}

/* URL + Copy button sit side-by-side */
.invite-url-cell {
    display: flex;
    align-items: center;
    gap: 8px;
}

.invite-url {
    font-size: 0.8em;
    color: #666;
    word-break: break-all;
}

/* QR code centered in the modal with a subtle border */
.qr-display {
    text-align: center;
    padding: 20px;
}

.qr-image {
    max-width: 250px;
    width: 100%;
    height: auto;
    border: 1px solid #e1e4e8;
    border-radius: 8px;
}

/* Toast notification — appears at bottom-center after clipboard copy */
.toast-message {
    position: fixed;
    bottom: 30px;
    left: 50%;
    transform: translateX(-50%);
    background: #1a1a2e;
    color: #fff;
    padding: 10px 25px;
    border-radius: 8px;
    font-size: 0.9em;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
    z-index: 2000;
    animation: toast-fade 2s ease-in-out;
}

@keyframes toast-fade {
    0%   { opacity: 0; transform: translateX(-50%) translateY(10px); }
    15%  { opacity: 1; transform: translateX(-50%) translateY(0); }
    85%  { opacity: 1; transform: translateX(-50%) translateY(0); }
    100% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
}

/* ===== GLOBAL PRESS-DOWN FEEDBACK =====
   Without this, tapping buttons and links on mobile gives no tactile signal
   that the tap registered — the element stays visually frozen until the action
   completes. :not(:disabled) prevents the scale from firing on non-interactive
   buttons (e.g. a spinner-disabled Submit button shouldn't appear pressable). */
button:not(:disabled),
.btn:not(:disabled),
a.btn:not(:disabled),
.nav-link,
.family-bottom-nav-item,
.fc-event {
    transition: transform 150ms ease;
}

button:not(:disabled):active,
.btn:not(:disabled):active,
a.btn:not(:disabled):active,
.nav-link:active,
.family-bottom-nav-item:active,
.fc-event:active {
    transform: scale(0.90);
}

/* ===== FULLCALENDAR — Bootstrap compatibility overrides ===== */
/*
 * Bootstrap resets table/td/th in ways that break FullCalendar's grid layout:
 *   - border-collapse:collapse collapses the scrollgrid borders into nothing
 *   - padding on td/th shifts cell content out of alignment with headers
 *   - box-sizing differences misalign column widths
 * All overrides are scoped to .fc so Bootstrap tables elsewhere are untouched.
 */
.fc table {
    border-collapse: separate;
    border-spacing: 0;
    box-sizing: border-box;
    width: 100%;
}

.fc td,
.fc th {
    padding: 0;
    vertical-align: top;
    box-sizing: border-box;
}

/* ===== FULLCALENDAR — MassIV design system styling =====
 * All selectors are scoped to .fc — zero impact on anything outside the calendar.
 * Where FullCalendar 6 exposes CSS custom properties we use those (no !important
 * needed). Specific selectors fill in the gaps: typography, pill shape, word-wrap,
 * list view indicator, and the golden today ring.
 */

/* --- CSS custom properties ---
   FullCalendar 6 reads these on .fc at render time. They override the default
   palette for grid borders, today highlight, event colors, and all button states.
   Using variables here means no !important fights with FullCalendar's own rules. */
.fc {
    --fc-border-color:              #e8eaed;
    --fc-today-bg-color:            #fffbf0;
    --fc-event-text-color:          #fff;
    --fc-event-border-color:        transparent;
    --fc-button-text-color:         #444;
    --fc-button-bg-color:           #fff;
    --fc-button-border-color:       #d1d5db;
    --fc-button-hover-bg-color:     #fff;
    --fc-button-hover-border-color: #3498db;
    --fc-button-active-bg-color:    #3498db;
    --fc-button-active-border-color:#3498db;
}

/* --- Outer frame ---
   Transparent outer scrollgrid border — the white container card provides the
   visible edge, so a second border here would create a doubled-line look. */
.fc .fc-scrollgrid {
    border-color: transparent;
}

/* --- Day column headers (Sun  Mon  Tue …) --- */

.fc .fc-col-header-cell {
    background: #f8f9fa;
    border-bottom: 2px solid #e8eaed;
}

/* Remove FullCalendar's default blue anchor styling from day header text */
.fc .fc-col-header-cell-cushion {
    font-weight: 600;
    font-size: 0.78em;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #888;
    text-decoration: none;
    padding: 8px 0;
}

.fc .fc-col-header-cell-cushion:hover {
    color: #888;
    text-decoration: none;
}

/* --- Day number links --- */

.fc .fc-daygrid-day-number {
    color: #555;
    font-weight: 500;
    font-size: 0.85em;
    text-decoration: none;
    padding: 4px 6px;
}

.fc .fc-daygrid-day-number:hover {
    color: #3498db;
    text-decoration: none;
}

/* --- Today cell ---
   --fc-today-bg-color handles the background fill.
   Inset box-shadow adds a golden ring without shifting adjacent cells —
   a regular border would push cell content and misalign the grid. */
.fc .fc-daygrid-day.fc-day-today {
    box-shadow: inset 0 0 0 2px #f0c040;
}

.fc .fc-day-today .fc-daygrid-day-number {
    color: #b8860b;
    font-weight: 700;
}

/* --- Day cells — give the grid some breathing room so events aren't cramped --- */

.fc .fc-daygrid-day-frame {
    min-height: 90px;
}

/* --- Event pills — base hover interaction ---
   Transition here (on the base class) so the lift animates in regardless of
   which view (month, week, list) the pill appears in — without it the transform
   snaps instantly instead of easing. */
.fc .fc-event {
    transition: transform 150ms ease, box-shadow 150ms ease;
}

.fc .fc-event:hover {
    cursor: pointer;
    transform: translateY(-3px) scale(1.05);
    box-shadow: 0 6px 16px rgba(0,0,0,0.22);
}

/* --- Event pills — month view (dayGrid) --- */

.fc .fc-daygrid-event {
    border-radius: 6px;
    border: none !important;
    font-weight: 600;
    font-size: 0.8em;
    padding: 3px 8px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
    margin-bottom: 3px;
    /* Unlock the overflow chain so word-wrap on the title can actually work.
       FullCalendar sets overflow:hidden on this element and every container
       inside it — setting only the leaf (.fc-event-title) does nothing if
       the ancestors clip it first. */
    overflow: visible;
}

/* White text always — event type colors are chosen to contrast with white */
.fc .fc-daygrid-event .fc-event-title,
.fc .fc-daygrid-event .fc-event-time {
    color: #fff;
}

/* Word-wrap: unlock every element in the DOM chain between the event anchor
   and the title so white-space:normal can actually flow the text downward.
   Without these, FullCalendar's overflow:hidden on .fc-event-main and
   .fc-event-title-container clips the title before our rule even applies. */
.fc .fc-daygrid-event .fc-event-main,
.fc .fc-daygrid-event .fc-event-main-frame,
.fc .fc-daygrid-event .fc-event-title-container {
    overflow: visible;
}

.fc .fc-daygrid-event .fc-event-title {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
}

/* Remove the colored dot — the full-width block already serves as the visual marker */
.fc .fc-daygrid-event-dot {
    display: none;
}

/* Dot-event mode (cells too narrow for a block): keep white text on the dot row */
.fc .fc-daygrid-dot-event .fc-event-title {
    color: #fff;
    font-weight: 600;
}

/* --- Event pills — week / timeGrid view --- */

.fc .fc-timegrid-event {
    border-radius: 6px;
    border: none !important;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}

.fc .fc-timegrid-event .fc-event-title,
.fc .fc-timegrid-event .fc-event-time {
    color: #fff;
    font-weight: 600;
}

/* --- Event rows — list view ---
   FullCalendar sets border-color inline on .fc-list-event-dot to the event's
   own color. Zeroing three of the four border sides leaves just a colored left
   strip — so every row gets a per-event-color indicator with no per-event CSS.
   The width and height give the element its dimensions after the original 10px
   circular border is collapsed. */
.fc .fc-list-event .fc-list-event-dot {
    width: 0 !important;
    height: 16px !important;
    border-top-width: 0 !important;
    border-right-width: 0 !important;
    border-bottom-width: 0 !important;
    border-left-width: 3px !important;
    border-left-style: solid !important;
    border-radius: 2px;
    margin-right: 8px;
    vertical-align: middle;
}

/* Dark text in list view — background is white so white text would be invisible
   (unlike month/week views where text sits on a fully colored block). */
.fc .fc-list-event-title a,
.fc .fc-list-event-time {
    color: #333;
    text-decoration: none;
}

.fc .fc-list-event:hover .fc-list-event-title a {
    color: #3498db;
}

/* Subtle blue tint on row hover so the row is visually acknowledged on hover */
.fc .fc-list-event:hover td {
    background: rgba(52, 152, 219, 0.05) !important;
}

/* --- Toolbar buttons ---
   CSS variables above handle colors. These rules supply shape and typography
   that FullCalendar 6's variables do not expose: border-radius, font-family,
   padding, and the transition. */
.fc .fc-button,
.fc .fc-button-primary {
    border-radius: 6px !important;
    font-size: 0.82em !important;
    font-weight: 500 !important;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
    box-shadow: none !important;
    text-shadow: none !important;
    padding: 5px 12px !important;
    transition: border-color 0.15s, color 0.15s, background 0.15s !important;
}

/* FullCalendar 6 has no --fc-button-hover-text-color variable, so set it directly */
.fc .fc-button-primary:not(:disabled):hover {
    color: #3498db;
}

/* FullCalendar 6 has no --fc-button-active-text-color variable, so set it directly */
.fc .fc-button-primary:not(:disabled).fc-button-active,
.fc .fc-button-primary:not(:disabled):active {
    color: #fff;
}

/* Focus ring — replaces browser default outline with a MassIV blue glow ring */
.fc .fc-button:focus,
.fc .fc-button-primary:focus {
    box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2) !important;
    outline: none !important;
}

/* Button groups: only the outer corners get rounded; inner edges meet flush.
   Without this every button in the group has full border-radius, creating gaps. */
.fc .fc-button-group .fc-button {
    border-radius: 0 !important;
}

.fc .fc-button-group .fc-button:first-child {
    border-radius: 6px 0 0 6px !important;
}

.fc .fc-button-group .fc-button:last-child {
    border-radius: 0 6px 6px 0 !important;
}

/* Prevent doubled 1px borders where adjacent group buttons meet */
.fc .fc-button-group .fc-button:not(:first-child) {
    margin-left: -1px;
}

/* --- Toolbar title --- */

.fc .fc-toolbar-title {
    font-size: 1.2em;
    font-weight: 700;
    color: #1a1a2e;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* ===== FAMILY PORTAL — Top Navigation Bar =====
 * The family portal replaces the sidebar with a horizontal top nav.
 * .family-layout on .page switches the flex axis to column so the nav
 * stacks above the main content rather than sitting beside it.
 * All selectors are prefixed .family- to prevent collisions with PLO/GW Admin.
 */

/* Column direction so the top nav bar stacks above main content.
   Higher specificity (.page.family-layout) overrides the base .page flex-direction: row.
   width: 100% ensures the column fills the full viewport on browsers that don't
   inherit width automatically from the body/html ancestors. */
.page.family-layout {
    flex-direction: column;
    width: 100%;
}

/* Guarantee the top nav spans full viewport width — some browsers let flex children
   shrink below 100% if content is narrower than the viewport. */
.page.family-layout .family-nav {
    width: 100%;
    flex-shrink: 0;
}

/* min-width: 0 prevents long content (like calendar grids) from causing flex overflow
   that would push the container wider than its parent. margin-top offsets the fixed
   top nav so content starts below it instead of being hidden underneath. */
.page.family-layout .main-content {
    width: 100%;
    min-width: 0;
    margin-top: 64px;
}

/* Bottom nav is position:fixed so left/right already anchor it, but width:100% is
   added as an explicit guarantee for fixed elements in older mobile browsers. */
.page.family-layout .family-bottom-nav {
    width: 100%;
}

/* Full-width top nav, 64px tall.
   position:fixed anchors it to the top of the viewport independent of the flex
   container's direction — the flex-direction:column approach alone is unreliable
   across browsers and cached CSS states. Same pattern as .family-bottom-nav.
   background uses --family-nav-bg so PLO Primary Color controls the nav background. */
.family-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 64px;
    padding: 0 24px;
    background: var(--family-nav-bg, #ffffff);
    border-bottom: 1px solid #e1e4e8;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
}

/* D45 follow-up — scrollToDetail (massiv-calendar.js) calls scrollIntoView({block:'start'})
   on this panel, both from a manual calendar click and the dashboard's deep-link rows. That
   lands the panel's top edge exactly at the viewport's top edge — but .family-nav is
   position:fixed and sits on top of page content, so the panel's title/badge end up
   rendered underneath it. scroll-margin-top tells the browser's native scrollIntoView to
   stop short of the true top by this much, with zero JS offset math needed. 80px is the
   64px desktop nav height plus ~16px breathing room; the mobile override below uses the
   56px mobile nav height plus the same margin. */
.cal-event-detail {
    scroll-margin-top: 80px;
}

/* Brand: 4px left accent stripe anchors the eye to org identity without a full sidebar.
   Text color uses --family-nav-text so it stays readable on dark Primary Color backgrounds. */
.family-nav-brand {
    display: flex;
    align-items: center;
    border-left: 4px solid var(--family-nav-accent, #3498db);
    padding-left: 12px;
    font-weight: 700;
    font-size: 1.05em;
    color: var(--family-nav-text, #1a1a2e);
    min-width: 140px;
    /* Prevent oversized images from escaping the brand container regardless of dimensions */
    overflow: hidden;
}

.family-nav-brand-logo {
    max-height: 36px;
    /* Wide logos without max-width overflow the brand area into the nav links on desktop */
    max-width: 160px;
    width: auto;
    object-fit: contain;
}

.family-nav-brand-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 200px;
}

/* Center nav links */
.family-nav-links {
    display: flex;
    align-items: center;
    gap: 2px;
}

/* Each link: icon stacked above label, stretches full nav height for easy tap target.
   border-radius 8px so the hover/active pill background has rounded corners.
   padding 8px 12px gives breathing room between the icon+label stack and the pill edge. */
.family-nav-link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    height: 64px;
    padding: 8px 12px;
    color: var(--family-nav-text, #333);
    text-decoration: none;
    font-weight: 500;
    border-bottom: 2px solid transparent;
    border-radius: 8px;
    transition: color 0.15s, border-color 0.15s, background-color 0.15s;
}

.family-nav-link i.bi {
    font-size: 1.25rem;
    line-height: 1;
}

.family-nav-link span {
    font-size: 0.78em;
    font-weight: 500;
}

/* Pill background on hover confirms the link is interactive without going white.
   rgba() fallback uses the default accent blue — orgs with custom colors will see
   the blue pill, not their brand color, but the link remains fully visible. */
.family-nav-link:hover {
    color: var(--family-nav-accent, #3498db);
    background: rgba(52, 152, 219, 0.10);
    text-decoration: none;
}

/* Active: a solid white "pop out" pill — scaled up, bold, dark text, drop shadow.
   Previous attempts (accent-colored border, white-overlay background) both
   depended on org theme colors and could vanish: PDB's accent is #732f2f, a dark
   red that blends into PDB's equally dark red nav background, so anything built
   from --family-nav-accent or a translucent overlay can fail on the wrong org
   palette. A solid white pill with fixed dark text reads on every nav background
   with zero reliance on org CSS variables for the active state itself. */
.family-nav-link.active {
    background: #ffffff;
    color: #1a1a2e;
    font-weight: 700;
    border-bottom-color: transparent;
    transform: scale(1.08);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}

/* Thin vertical divider separates regular nav from admin section */
.family-nav-divider {
    width: 1px;
    height: 28px;
    background: #e1e4e8;
    margin: 0 6px;
    flex-shrink: 0;
}

/* PLO Admin link: subtle outlined badge to visually distinguish it from family nav.
   color/border used to be hardcoded #666 / #d1d5db (mid-grey / light-grey) — both
   nearly invisible on dark org nav backgrounds. var(--family-nav-text) tracks the
   same readable-on-any-background color the rest of the nav already uses, so the
   border (white on dark navs, dark on light navs) stays visible either way. */
.family-nav-link-admin {
    height: auto;
    padding: 6px 12px;
    border: 1px solid var(--family-nav-text, #d1d5db);
    border-radius: 6px;
    color: var(--family-nav-text, #666);
    font-size: 0.82em;
}

.family-nav-link-admin:hover {
    border-color: var(--family-nav-accent, #3498db);
    color: var(--family-nav-accent, var(--family-nav-text, #3498db));
}

.family-nav-link-admin.active {
    border-color: var(--family-nav-accent, #3498db);
    color: var(--family-nav-accent, var(--family-nav-text, #3498db));
}

/* Right side: UserAvatar */
.family-nav-right {
    display: flex;
    align-items: center;
    min-width: 140px;
    justify-content: flex-end;
}

/* Mobile bottom nav — hidden on desktop; revealed by the mobile media query.
   background matches the top nav so both bars share the same PLO Primary Color. */
.family-bottom-nav {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 64px;
    background: var(--family-nav-bg, #ffffff);
    border-top: 1px solid #e1e4e8;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
    z-index: 400;
}

/* Each bottom tab.
   border-top: transparent default reserves the 2px so layout doesn't shift when
   active state switches it to the accent color. */
.family-bottom-nav-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    color: var(--family-nav-text, #777);
    text-decoration: none;
    font-weight: 500;
    transition: color 0.15s, background-color 0.15s;
    padding: 6px 4px;
    border-top: 2px solid transparent;
}

.family-bottom-nav-item i.bi {
    font-size: 1.3rem;
    line-height: 1;
}

.family-bottom-nav-item span {
    font-size: 0.65em;
}

/* Hover-only pill — accent-tinted, same as before. Active no longer shares this
   rule (see below): it needs a solid white pop-out, not an org-color overlay. */
.family-bottom-nav-item:hover {
    color: var(--family-nav-accent, #3498db);
    background: rgba(255, 255, 255, 0.18);
    text-decoration: none;
}

/* Active: same solid-white pop-out as .family-nav-link.active — scaled up, bold,
   dark text, drop shadow, zero reliance on org CSS variables. The border-top
   accent line is removed for the same reason the desktop version drops its
   border-bottom: on dark-on-dark org palettes (e.g. PDB's #732f2f accent on its
   own dark red nav) an accent-colored line is invisible regardless of opacity. */
.family-bottom-nav-item.active {
    background: #ffffff;
    color: #1a1a2e;
    font-weight: 700;
    border-top-color: transparent;
    transform: scale(1.08);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
    text-decoration: none;
}

/* ===== FAMILY PORTAL — Micro-animations =====
 * All rules are scoped to .family-portal (set on <main> in FamilyLayout.razor)
 * so PLO Admin and GW Admin are completely unaffected.
 */

/* Page content fade-in: opacity + slight translateY slide-up on load.
   Without this, content snaps into place; the animation gives the page a
   polished, app-like entrance that feels less jarring on mobile. */
@keyframes family-content-fadein {
    from { opacity: 0; }
    to   { opacity: 1; }
}

.family-portal .content-area {
    animation: family-content-fadein 280ms ease-out both;
}

/* massiv-content-fadein keyframe is intentionally removed — AOS handles per-element
   entrance animations on the cards and sections inside .content-area. Animating the
   wrapper itself caused a double-fade: content started at opacity:0 twice and stayed
   invisible on PLO Admin mobile after Blazor navigation until a manual browser refresh. */

/* Pending-status banner — shown on the family dashboard when the officer has set the
   family's status to Pending. Amber/warning colour signals "action needed" without
   being as alarming as red. Without visible styling, the banner blends into the page. */
.pending-status-banner {
    background: #fff8e1;
    border-left: 4px solid #f59e0b;
    border-radius: 8px;
    padding: 14px 18px;
    margin-bottom: 20px;
    color: #78350f;
}

.pending-status-banner strong {
    display: block;
    margin-bottom: 4px;
    font-size: 0.95rem;
}

.pending-status-banner p {
    margin: 0;
    font-size: 0.9rem;
    color: #92400e;
}

/* Card lift: form-cards respond to pointer hover with a gentle 3D lift.
   The transition makes it smooth regardless of mouse speed. */
.family-portal .form-card {
    transition: transform 180ms ease-out, box-shadow 180ms ease-out;
}

.family-portal .form-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.10);
}

/* Same lift for PLO Admin and GW Admin — scoped away from family-portal so
   the two hover effects don't stack on officers who see both portals. */
.main-content:not(.family-portal) .form-card {
    transition: transform 180ms ease-out, box-shadow 180ms ease-out;
}

.main-content:not(.family-portal) .form-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.09);
}

/* On press/tap the card settles back slightly — tactile press-down feedback */
.family-portal .form-card:active {
    transform: translateY(-1px);
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
}

/* Button press: scale-down on tap gives satisfying physical feedback on mobile.
   Not applied to btn-outline-secondary (Cancel/close buttons) — those are
   secondary actions and should feel less interactive to avoid accidental taps. */
.family-portal .btn:not(.btn-outline-secondary) {
    transition: transform 100ms ease-out;
}

.family-portal .btn:not(.btn-outline-secondary):active {
    transform: scale(0.97);
}

/* ===== DATE / TIME INPUT — full-field click target =====
 * Browsers only open the native picker when the user clicks the small calendar/clock
 * icon on the right edge. This invisible indicator overlay catches clicks anywhere on
 * the input so the picker opens regardless of where the user taps — critical on mobile
 * where hit-target precision is low.
 * position:relative is required so the absolutely-positioned indicator stays anchored
 * inside the input box rather than escaping to the nearest positioned ancestor.
 * The ::-webkit-calendar-picker-indicator rule is ignored by browsers that don't support
 * it (Firefox uses a different native control), so there is no cross-browser regression. */
input[type="date"],
input[type="time"] {
    position: relative;
    cursor: pointer;
}

input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    opacity: 0;
    cursor: pointer;
}

/* ===== RESPONSIVE — mobile slide-out drawer ===== */
@media (max-width: 768px) {
    /* Sidebar leaves the flex flow entirely and becomes a fixed overlay.
       Without position:fixed it would still push main-content sideways.
       overflow-y:auto lets the drawer scroll when nav links exceed viewport height —
       without it, items below the fold are clipped and unreachable by swipe. */
    .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        height: 100%;
        width: 270px;
        z-index: 300;
        transform: translateX(-100%);
        overflow-y: auto;
    }

    /* flex:1 lets the nav list expand to fill remaining sidebar space below the brand
       header; overflow-y:auto gives it an independent scroll track so short sidebars
       don't leave dead space and tall ones don't clip the last items. */
    .nav-menu {
        flex: 1;
        overflow-y: auto;
    }

    /* max-height:90vh caps modal height to the visible screen so content is never
       taller than the viewport; overflow-y:auto adds an internal scroll track for
       long-form modals instead of clipping them. */
    .modal-card {
        max-height: 90vh;
        overflow-y: auto;
    }

    /* Adding sidebar-open via JS slides the panel in. Box-shadow adds depth
       so it reads as floating above the page, not part of the layout. */
    .sidebar.sidebar-open {
        transform: translateX(0);
        box-shadow: 4px 0 24px rgba(0, 0, 0, 0.18);
    }

    /* M22 — replaced by edge-swipe-to-open + a first-load peek animation
       (massiv-interop.js), so there's no longer a visible button on mobile.
       The button, its markup, and its click listener all stay intact — this
       is a pure visual hide, scoped to this max-width:768px breakpoint only,
       so desktop/tablet behavior (already display:none by the base rule
       above) is completely unaffected. */
    .hamburger-btn {
        display: none;
    }

    /* M22 fix 2 — persistent edge-sliver affordance, now a standalone fixed
       element (PLOAdminLayout.razor) instead of a .sidebar::after pseudo.
       The pseudo-element approach (M22 fix) never actually rendered: .sidebar
       needs overflow-y:auto for its own scrollable nav menu, and overflow
       clips ANY child — including a pseudo-element — that tries to paint
       outside the parent's own box. z-index doesn't help; clipping happens
       regardless of stacking order, since it's a box-model child of
       .sidebar. Moving the sliver to its own position:fixed sibling element
       (placed right after .sidebar in the markup, before .sidebar-backdrop)
       sidesteps the clipping entirely — it's no longer inside .sidebar's box
       at all, so .sidebar's overflow rule has no power over it. left:0
       anchors it directly to the viewport edge (no more "right:-12px past a
       transformed parent" trick needed, since this element isn't nested
       inside the transformed/translated .sidebar). z-index:300 matches
       .sidebar's own value — they're never visible at the same time (see the
       sibling-combinator hide rule below) so they don't need to out-rank
       each other, just both sit above ordinary page content.
       M22 fix 3 — widened 12px→22px and tallened 56px→80px so this is a
       comfortable tap target (not just a swipe-start zone), and
       pointer-events:none removed (was previously set since the tab was
       swipe-only) now that massiv-interop.js's delegated click listener has
       a .sidebar-peek-tab branch — none would have silently swallowed every
       tap before it ever reached that listener. cursor:pointer is a no-op on
       touch but signals tappability for anyone using a mouse/trackpad at a
       narrow viewport (e.g. a resized desktop browser window). */
    .sidebar-peek-tab {
        position: fixed;
        top: 50%;
        left: 0;
        transform: translateY(-50%);
        width: 22px;
        height: 80px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: var(--sidebar-bg, #e4e6ea);
        color: var(--sidebar-text-color, #222222);
        font-size: 1rem;
        font-weight: 700;
        line-height: 1;
        opacity: 0.85;
        border-radius: 0 12px 12px 0;
        box-shadow: 2px 0 6px rgba(0, 0, 0, 0.15);
        z-index: 300;
        cursor: pointer;
        /* M25 fix 4 — replaces the JS-driven peek (massivPeekSidebarOnce), which proved
           unreliable on Blazor enhanced navigation into a section with no prior
           interactive circuit: the retry timing could never be made to reliably outlast
           circuit establishment. This glow needs no JS, no DOM-readiness check, and no
           knowledge of when Blazor connects — it's running the instant this CSS rule
           applies to the element, the same moment the tab itself becomes visible.
           M25 fix 5 — two animations layered on the same box-shadow property. Per the
           CSS animations spec, when multiple animations affect the same property, the
           last one in the list wins for as long as it's "in effect"; -intense has no
           fill, so once its 4 iterations finish at 3s it stops being in effect and
           -glow (delayed 3s, so it does nothing before then) takes over from that exact
           moment — no JS timer or coordination needed for the handoff. Both keyframe
           sets start/end on the exact same calm box-shadow value, so the handoff at 3s
           has no visible jump even though it's two separate animations swapping control. */
        animation: sidebar-peek-glow-intense 0.75s ease-in-out 4,
                   sidebar-peek-glow 1.8s ease-in-out 3s infinite;
    }

    /* M25 fix 5 — a fixed blue glow blends into orgs whose --content-bg is itself
       blue-ish or just generally cool-toned (PDB's gray page barely showed it at
       all). .sidebar-peek-tab already carries the org's own branded background via
       SidebarStyle's inline custom properties, so the glow doesn't need to be
       brand-colored too — it needs to contrast with the PAGE behind it, which is
       unknown here (CSS can't read --content-bg and do luminance math the way
       GetContrastColor does in C#). White is the simplest color that reads against
       virtually any page tone; layering a tight dark rim shadow underneath it
       covers the one case white alone can't — a very light/white org page — so the
       pulse stays visible whether the page is light, dark, or colored, with zero
       per-org branching needed.
       Rapid, bright pulses for the first 3s (4 × 0.75s) — grabs attention immediately
       on load before settling into the calm -glow loop below. Far more saturated and
       wider-spread than -glow's resting/peak values. */
    @keyframes sidebar-peek-glow-intense {
        0%, 100% {
            box-shadow: 2px 0 4px rgba(0, 0, 0, 0.18), 2px 0 6px rgba(255, 255, 255, 0.35);
        }
        50% {
            box-shadow: 3px 0 10px rgba(0, 0, 0, 0.45), 6px 0 26px rgba(255, 255, 255, 0.95);
        }
    }

    /* Base state matches the tab's own resting box-shadow so the animation has no
       visible "snap" at the loop boundary; the glow state brightens and spreads it
       further. Kept subtle — a slow breathing pulse, not a strobe. Runs forever
       after the intense phase hands off at 3s, same continuous-affordance design as
       before. Same white-halo + dark-rim pairing as -intense above, and the same
       resting-frame values, so the 3s handoff between the two animations stays
       seamless. */
    @keyframes sidebar-peek-glow {
        0%, 100% {
            box-shadow: 2px 0 4px rgba(0, 0, 0, 0.18), 2px 0 6px rgba(255, 255, 255, 0.35);
        }
        50% {
            box-shadow: 3px 0 8px rgba(0, 0, 0, 0.30), 4px 0 16px rgba(255, 255, 255, 0.75);
        }
    }

    /* Hidden once the drawer is open — same general-sibling-combinator
       technique already used for .sidebar-backdrop's visibility just above,
       since .sidebar-peek-tab is a sibling of .sidebar (not a descendant),
       this can't be a plain .sidebar-open child selector. The sliver's only
       job is hinting that a closed drawer exists; with the real sidebar
       already in view there's nothing left for it to advertise. */
    .sidebar.sidebar-open ~ .sidebar-peek-tab {
        display: none;
    }

    /* On mobile, swap the portal text label for the org logo or short name */
    .top-bar-portal-label {
        display: none;
    }

    .top-bar-org-brand {
        display: flex;
        align-items: center;
        gap: 8px;
    }

    .content-area {
        padding: 15px;
    }

    .top-bar {
        padding: 0 15px;
    }

    /* Family top nav: shrink height on mobile — links are hidden; bottom bar takes over */
    .family-nav {
        height: 56px;
    }

    /* D45 follow-up — matches the mobile nav's shorter 56px height (vs 80px/64px on
       desktop above), same ~16px breathing room. */
    .cal-event-detail {
        scroll-margin-top: 72px;
    }

    /* Mobile-only frosted-glass treatment for the top nav, matching the bottom nav's
       glass language so the two bars read as one consistent design system instead of
       a glass pill at the bottom next to a flat solid strip at the top. Values
       (color-mix percentage, blur/saturate amounts) intentionally match
       .family-bottom-nav's tuning below so both bars feel like the same material
       under the same org tint.
       Unlike .family-bottom-nav this is NOT a floating pill — no margin is added, it
       stays flush full-width against the top and both side edges, since it's an
       anchored top bar rather than something pulled away from the screen.
       M23 — bottom corners were originally rounded (0 0 16px 16px) to soften the
       transition into the page below; Andy wants a square bottom edge instead, so
       border-radius is omitted entirely (default 0) while every other glass property
       stays untouched.
       Desktop .family-nav (outside this media query) is completely untouched and
       stays solid — this block only exists inside max-width:768px. */
    .family-nav {
        background: color-mix(in srgb, var(--family-nav-bg, #ffffff) 55%, transparent);
        backdrop-filter: blur(24px) saturate(200%);
        -webkit-backdrop-filter: blur(24px) saturate(200%);
        border-bottom: 1px solid rgba(255, 255, 255, 0.3);
    }

    /* Match margin-top to the reduced nav height on mobile */
    .page.family-layout .main-content {
        margin-top: 56px;
    }

    /* Desktop nav links hidden — the fixed bottom bar replaces them on mobile */
    .family-nav-links {
        display: none;
    }

    /* Show the fixed bottom nav bar. align-items:center (not stretch) keeps every item
       pinned to a fixed vertical position regardless of content height — stretch let
       items grow/shrink slightly as the icon/label stack settled during scroll-snap,
       which read as the whole row wobbling up and down during a horizontal swipe. */
    .family-bottom-nav {
        display: flex;
        align-items: center;
    }

    /* width:100% on .page.family-layout .family-bottom-nav (set above, outside this
       media query) has higher specificity than a bare .family-bottom-nav selector, so
       it would still win and over-constrain the box once margin is added below — the
       pill would render 24px too wide and clip off the right edge of the screen.
       Re-declaring the same selector here at the same specificity, later in the
       cascade, switches it back to auto so margin/left/right size the pill correctly. */
    .page.family-layout .family-bottom-nav {
        width: auto;
    }

    /* Mobile bottom nav: floating frosted-glass pill instead of an edge-to-edge flat
       bar. margin pulls it off all three free edges so it reads as floating;
       backdrop-filter blurs whatever page content sits behind/below it for an
       iOS-style glass look.
       Round 2: the fixed neutral rgba(255,255,255,0.25) tint from the previous fix
       erased the org's brand color entirely — Andy wants PDB's dark red still visible,
       just translucent. Switched back to a translucent --family-nav-bg base, but this
       time the blur itself (24px, up from 20px) plus saturate(200%) is what creates
       separation from the page behind it, not the tint's contrast — so it stays
       readable as glass even when the pill and page share a similar hue. The linear-
       gradient layered on top is a subtle white sheen (12% at the top fading to 0% by
       40% down) so the org-colored glass still looks glossy instead of a flat color
       block; background-blend-mode:overlay lets the gradient interact with the tinted
       base instead of just sitting opaquely on top of it.
       touch-action:pan-x (not 'none') tells the browser to only ever interpret
       horizontal gestures on this element — without it, default two-axis touch
       panning applies and a swipe can drag the bar vertically, feeling like panning
       a webpage instead of swiping a row of buttons. 'none' would also block the
       horizontal scroll itself, since Blazor/the browser needs pan-x left enabled
       to generate the native scroll gesture at all.
       Round 3 (real iPhone testing): touch-action alone was not enough — iOS Safari
       has a known quirk where a touch starting on/near a position:fixed scrollable
       child can still bubble its vertical component into the page's own elastic
       rubber-band scroll. overscroll-behavior:none (both axes, not just -x) closed
       that escape hatch by fully containing scroll chaining within this element —
       but that blanket value also removed the horizontal edge-bounce as a side
       effect, since "none" suppresses overscroll on both axes rather than just the
       vertical one that actually mattered for the iOS bleed fix.
       M24: split into axis-specific values — overscroll-behavior-x:auto restores
       the native horizontal rubber-band bounce at the first/last tab (the actual
       polish this row removes), while overscroll-behavior-y:none keeps the
       vertical containment that fixed the iOS drag-bleed issue, since y was the
       axis the bleed actually traveled on. touch-action:pan-x already only ever
       permitted horizontal panning in the first place, so this isn't reopening
       the vertical gesture path Round 3 closed — it specifically targets only the
       overscroll/bounce behavior at the scroll boundary.
       -webkit-overflow-scrolling:touch restores native momentum/inertia scrolling
       on iOS WebKit, which overscroll-behavior can otherwise blunt.
       FOLLOW-UP (not changed here, per instruction): if vertical bleed persists on
       iPhone after this, check for overflow/touch-action rules on <body>/<html> that
       could still be allowing the gesture to bubble up from this fixed child. */
    .family-bottom-nav {
        margin: 0 12px 12px 12px;
        border-radius: 20px;
        background:
            linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0) 40%),
            color-mix(in srgb, var(--family-nav-bg, #ffffff) 55%, transparent);
        background-blend-mode: overlay, normal;
        backdrop-filter: blur(24px) saturate(200%);
        -webkit-backdrop-filter: blur(24px) saturate(200%);
        border: 1px solid rgba(255, 255, 255, 0.3);
        box-shadow: 0 8px 28px rgba(0, 0, 0, 0.20);
        overflow-x: auto;
        touch-action: pan-x;
        scroll-snap-type: x mandatory;
        overscroll-behavior-x: auto;
        overscroll-behavior-y: none;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        /* Extra layer of native scroll easing on top of scroll-snap so momentum
           settles smoothly instead of stopping abruptly at each snap point. */
        scroll-behavior: smooth;
    }

    /* Hides the WebKit scrollbar track (Chrome/Safari) — scrollbar-width:none above
       already hides it in Firefox. Without both, a thin scrollbar would poke out past
       the rounded corners and break the "pill" illusion. */
    .family-bottom-nav::-webkit-scrollbar {
        display: none;
    }

    /* flex:0 0 auto + a min-width sized to ~1/3.6 of the pill's inner width (instead of
       a clean /4) means a sliver of the next tab visibly peeks in from the right edge
       at rest — a simple CSS-only affordance that signals "there's more here, swipe"
       to a first-time user, who otherwise has no way to know tabs exist off-screen.
       24px subtracted matches the pill's 12px+12px side margins so the math reflects
       its actual on-screen width. margin creates a visible gap between capsules so
       each one reads as a separate floating object rather than a continuous strip. */
    .family-bottom-nav-item {
        flex: 0 0 auto;
        min-width: calc((100vw - 24px) / 3.6);
        /* Round 3: the previous 64px height + 16px radius + 8px vertical margin read
           as oversized blobs rather than refined pills. height dropped to 48px (the
           bar itself stays 64px tall — align-items:center on .family-bottom-nav
           vertically centers the shorter capsule, so no vertical margin is needed
           here at all; using margin instead of height to shrink it would have left
           the item's own box still 64px tall and overflowing past the pill's rounded
           corners). Horizontal-only margin tightens the gap between capsules so the
           row reads as a compact, dense cluster instead of spaced-out blocks. */
        height: 48px;
        margin: 0 2px;
        padding: 5px 9px;
        scroll-snap-align: center;
        /* Glass-capsule look for the inactive state — a subtle fill, translucent
           border, and inset top highlight so each tab reads as its own glossy,
           elevated button instead of flat text sitting on the blurred strip.
           .active below overrides background/border/box-shadow with the existing
           solid-white pop-out, which must stay clearly more prominent than these;
           its scale(1.08) transform is relative, so it automatically scales down
           proportionally along with this smaller base size with no separate change. */
        background: rgba(255, 255, 255, 0.10);
        border: 1px solid rgba(255, 255, 255, 0.18);
        border-radius: 12px;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
    }

    .family-bottom-nav-item i.bi {
        font-size: 1.2rem;
    }

    /* Bottom padding so page content is never hidden behind the bottom bar */
    .family-portal .content-area {
        padding-bottom: 80px;
    }

    /* Mobile typography: slightly larger base size and tighter card rounding
       feel warmer and more readable on small screens */
    .family-portal {
        font-size: 17px;
        line-height: 1.65;
    }

    .family-portal .form-card {
        border-radius: 14px;
    }

    /* Mobile calendar: compact cells and truncated event titles */
    .fc .fc-daygrid-day {
        min-height: 52px;
    }

    .fc .fc-daygrid-event {
        font-size: 0.7em !important;
        padding: 1px 4px !important;
    }

    /* On mobile the cell is narrow — truncate rather than wrap so grid stays compact */
    .fc .fc-daygrid-event .fc-event-title {
        white-space: nowrap !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
    }

    .fc .fc-toolbar-title {
        font-size: 0.92em !important;
    }

    .fc .fc-button,
    .fc .fc-button-primary {
        padding: 4px 8px !important;
    }

    /* Global mobile table → card stack.
       Applies to every table inside the content area on mobile.
       No wrapper div or per-page changes needed — works automatically on all current
       and future tables. The data-label attribute on each <td> supplies the column
       label shown before each value via ::before pseudo-element. */
    .content-area table {
        display: block;
        width: 100%;
    }

    .content-area thead {
        display: none;
    }

    .content-area tbody {
        display: block;
        width: 100%;
    }

    .content-area tbody tr {
        display: block;
        border: 1px solid #e1e4e8;
        border-radius: 10px;
        margin-bottom: 12px;
        padding: 12px;
        background: #fff;
        box-shadow: 0 1px 4px rgba(0,0,0,0.06);
    }

    /* color:#1a1a2e makes the value text itself the dark, primary-weight focal
       point — matching .detail-value's color elsewhere in this file. Columns that
       intentionally want a muted value already set their own color explicitly on
       td[data-label="X"] (Email, Onboarding/Joined, Date Needed/Submitted, Rank,
       etc.) — those selectors are more specific than this bare td rule and keep
       winning, so this only affects columns that weren't already styled. */
    .content-area tbody td {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        padding: 6px 0;
        border: none !important;
        font-size: 0.88em;
        gap: 8px;
        word-break: break-word;
        color: #1a1a2e;
    }

    /* Two earlier passes darkened this from #888 → #555 → #333 chasing "low
       contrast," but that was the wrong direction: this app's existing label/value
       convention (.detail-label/.detail-value, .mini-stat-label/.stat-label,
       .req-mini-header) always keeps labels MUTED and values DARK — that contrast
       between the two is the actual hierarchy device, not the label's absolute
       darkness. #333 made the label nearly as dark as the value text above, so
       everything read as the same weight of text with no separation at all.
       #6b7280 is a properly accessible muted gray (~4.6:1 against white, clears
       WCAG AA) — clearly readable on its own, but now visibly lighter than the
       newly-dark #1a1a2e value text beside/below it, restoring actual hierarchy. */
    .content-area tbody td::before {
        content: attr(data-label);
        font-weight: 600;
        color: #6b7280;
        font-size: 0.78em;
        text-transform: uppercase;
        letter-spacing: 0.4px;
        flex-shrink: 0;
        min-width: 80px;
        padding-top: 1px;
    }

    /* Cells with no data-label (action columns) — right-align and hide the ::before */
    .content-area tbody td:not([data-label]) {
        justify-content: flex-end;
    }

    .content-area tbody td:not([data-label])::before {
        display: none;
    }

    /* URL cell stacks label above value and breaks the URL so it wraps cleanly */
    .content-area tbody td.invite-url-cell {
        flex-direction: column;
        align-items: flex-start;
    }

    .content-area .invite-url {
        word-break: break-all;
        font-size: 0.78em;
    }

    /* FullCalendar must retain its default table layout — the global display:block
       above would flatten the calendar grid. These resets restore normal table rendering. */
    .content-area .fc table  { display: table; width: 100%; }
    .content-area .fc thead  { display: table-header-group; }
    .content-area .fc tbody  { display: table-row-group; }
    .content-area .fc tr     { display: table-row; }
    .content-area .fc td,
    .content-area .fc th     { display: table-cell; border: revert; }
    .content-area .fc tbody td::before { display: none; }

    /* ===== EVENT TYPES TABLE — mobile card polish =====
       Scoped by data-label values unique to Event Types (Color, Name, Type, Active).
       The global table→card rules already handle the base stacking; these rules
       layer on top to fix the "receipt" look without touching the markup. */

    /* Swatch is self-explanatory — the "COLOR" label above it is pure noise */
    .content-area tbody td[data-label="Color"]::before {
        display: none;
    }

    /* Swatch + name flow inline as a visual card header.
       Name label suppressed because the swatch already anchors the eye;
       "NAME:" between swatch and text is redundant clutter at narrow widths. */
    .content-area tbody td[data-label="Color"],
    .content-area tbody td[data-label="Name"] {
        display: inline-flex;
        align-items: center;
        vertical-align: middle;
        border: none !important;
    }

    .content-area tbody td[data-label="Color"] {
        width: auto;
        padding: 6px 8px 6px 0;
    }

    .content-area tbody td[data-label="Name"] {
        width: calc(100% - 48px);
        padding: 6px 0;
        font-weight: 500;
    }

    .content-area tbody td[data-label="Name"]::before {
        display: none;
    }

    /* Type and Active badges on the same line — both are short; stacking wastes vertical space */
    .content-area tbody td[data-label="Type"],
    .content-area tbody td[data-label="Active"] {
        display: inline-flex;
        align-items: center;
        width: auto;
        padding: 4px 12px 4px 0;
        vertical-align: middle;
        border: none !important;
    }

    /* Remove the global 80px min-width so compact badge labels stay truly inline */
    .content-area tbody td[data-label="Type"]::before,
    .content-area tbody td[data-label="Active"]::before {
        min-width: 0;
        margin-right: 4px;
    }

    /* System type rows render an empty Actions cell — hide it entirely so it
       doesn't float as an orphaned label with nothing below it. */
    .content-area tbody td[data-label="Actions"]:not(:has(button, a)) {
        display: none;
    }

    /* Thin rule visually separates data fields from action controls */
    .content-area tbody td[data-label="Actions"] {
        border-top: 1px solid #e8eaed !important;
        margin-top: 10px;
        padding-top: 10px;
    }

    /* Buttons are self-explanatory — "ACTIONS" label is visual clutter */
    .content-area tbody td[data-label="Actions"]::before {
        display: none;
    }

    /* Side-by-side equal-width buttons — both reachable with a thumb,
       layout stays balanced whether there is one button or two. */
    .content-area tbody td[data-label="Actions"] .action-cell {
        flex-direction: row;
        gap: 8px;
        width: 100%;
    }

    .content-area tbody td[data-label="Actions"] .action-cell .btn {
        flex: 1;
        justify-content: center;
    }

    /* ===== EVENTS LIST TABLE — mobile card layout =====
       Date and Time columns are unique to the events table — no scoping
       conflicts with other tables. The sibling selector "Date ~ Type" isolates
       event-row overrides from the EventTypes table which also has data-label="Type". */

    /* Date and Time flow on one line — labels suppressed, values self-explanatory */
    .content-area tbody td[data-label="Date"],
    .content-area tbody td[data-label="Time"] {
        display: inline-flex;
        align-items: center;
        width: auto;
        vertical-align: middle;
        border: none !important;
        font-size: 0.8em;
        color: #666;
    }

    .content-area tbody td[data-label="Date"] {
        padding: 4px 6px 0 0;
    }

    .content-area tbody td[data-label="Date"]::before {
        display: none;
    }

    .content-area tbody td[data-label="Time"] {
        padding: 4px 0 0 0;
    }

    /* Bullet replaces the "TIME:" label — keeps the date/time line compact */
    .content-area tbody td[data-label="Time"]::before {
        content: "·";
        min-width: 0;
        margin-right: 4px;
        font-weight: 400;
        font-size: 1.1em;
        color: #ccc;
        text-transform: none;
        letter-spacing: 0;
    }

    /* Title is the card headline — large enough to anchor the eye immediately */
    .content-area tbody td[data-label="Title"] {
        display: block;
        font-size: 1.05rem;
        font-weight: 600;
        color: #1a1a2e;
        padding: 4px 0 6px 0;
        border: none !important;
    }

    .content-area tbody td[data-label="Title"]::before {
        display: none;
    }

    /* Colored dot + type name are self-labeling — "TYPE:" adds no value.
       Sibling selector scopes this to event rows only (EventTypes has no Date cell). */
    .content-area tbody td[data-label="Date"] ~ td[data-label="Type"]::before {
        display: none;
    }

    /* Location and Signups: compact — remove the 80px min-width that pushes values right */
    .content-area tbody td[data-label="Location"]::before,
    .content-area tbody td[data-label="Signups"]::before {
        min-width: 0;
        margin-right: 6px;
    }

    /* Edit button right-aligned — event rows only via Date sibling */
    .content-area tbody td[data-label="Date"] ~ td[data-label="Actions"] {
        justify-content: flex-end;
    }

    /* ===== FEATURED DOCUMENT BANNER — mobile stack layout =====
       Horizontal layout (star + info + buttons in a row) truncates long titles and
       pushes buttons off-screen on narrow widths. Column direction stacks them vertically. */
    .doc-featured-banner {
        flex-direction: column;
        align-items: flex-start;
    }

    /* Inline nowrap was removed from markup; this rule closes the door on future regressions */
    .doc-featured-banner .doc-banner-title {
        white-space: normal;
        word-break: break-word;
    }

    /* Buttons fill the full banner width so none are clipped or overflow to the right */
    .doc-featured-banner .doc-banner-buttons {
        width: 100%;
        justify-content: flex-start;
        flex-wrap: wrap;
    }

    /* ===== DOCUMENT TABLE — mobile card layout =====
       Category and Visibility are unique to this table — sibling scoping not needed.
       Title cell inherits the Events table's Title rules (display:block, label suppressed). */

    /* Category and Visibility badges side by side — both are short; stacking wastes space */
    .content-area tbody td[data-label="Category"],
    .content-area tbody td[data-label="Visibility"] {
        display: inline-flex;
        align-items: center;
        width: auto;
        vertical-align: middle;
        border: none !important;
        padding: 4px 12px 4px 0;
    }

    /* Remove the 80px min-width so short badge labels stay truly inline */
    .content-area tbody td[data-label="Category"]::before,
    .content-area tbody td[data-label="Visibility"]::before {
        min-width: 0;
        margin-right: 4px;
    }

    /* Uploaded date: small and muted — less prominent than the document title */
    .content-area tbody td[data-label="Uploaded"] {
        font-size: 0.8em;
        color: #888;
    }

    .content-area tbody td[data-label="Uploaded"]::before {
        min-width: 0;
        margin-right: 6px;
    }

    /* Delete (trash icon only) stays at content width; Open + Edit share remaining space.
       Scoped via Category sibling so EventTypes' Actions layout is not affected. */
    .content-area tbody td[data-label="Category"] ~ td[data-label="Actions"] .action-cell .btn:last-child {
        flex: none;
        width: auto;
    }

    /* ===== OFFICER POSITIONS TABLE — mobile card layout =====
       Rank and Position labels are unique to this table — no sibling scoping needed.
       Approver and Actions inherit the global flex label/value and Actions rules. */

    /* Rank pill + position title sit on one line — stacking would lose the hierarchy signal */
    .content-area tbody td[data-label="Rank"],
    .content-area tbody td[data-label="Position"] {
        display: inline-block;
        border: none !important;
        vertical-align: middle;
        width: auto;
    }

    /* Rank rendered as a compact muted pill — communicates priority without competing with the name */
    .content-area tbody td[data-label="Rank"] {
        font-size: 0.78em;
        color: #888;
        background: #f1f3f5;
        border-radius: 4px;
        padding: 1px 6px;
        margin-right: 8px;
    }

    /* The pill itself identifies rank — a "RANK:" text prefix would be redundant */
    .content-area tbody td[data-label="Rank"]::before,
    .content-area tbody td[data-label="Position"]::before {
        display: none;
    }

    /* Position name is the visual anchor of the card */
    .content-area tbody td[data-label="Position"] {
        font-weight: 600;
        font-size: 1rem;
        padding: 4px 0;
    }

    /* ===== OFFICERS LIST TABLE — mobile card layout =====
       Name conflicts with EventTypes; Position conflicts with OfficerPositions.
       Both are scoped via tr:has(td[data-label="Email"]) — Email is unique to this table. */

    /* Officer name is the card headline — overrides the EventTypes inline-flex Name rule */
    .content-area tbody tr:has(td[data-label="Email"]) td[data-label="Name"] {
        display: block;
        font-size: 1.05rem;
        font-weight: 700;
        color: #1a1a2e;
        padding-bottom: 2px;
        border: none !important;
    }

    .content-area tbody tr:has(td[data-label="Email"]) td[data-label="Name"]::before {
        display: none;
    }

    /* Email sits below the name as muted supporting detail — no label needed */
    .content-area tbody td[data-label="Email"] {
        display: block;
        font-size: 0.85em;
        color: #666;
        padding-top: 0;
        padding-bottom: 8px;
        border: none !important;
    }

    .content-area tbody td[data-label="Email"]::before {
        display: none;
    }

    /* Position dropdown overrides OfficerPositions' inline-block rule for this table */
    .content-area tbody tr:has(td[data-label="Email"]) td[data-label="Position"] {
        display: block;
        width: 100%;
        padding-bottom: 8px;
    }

    /* Onboarding and Joined are secondary context — legible but not competing with name */
    .content-area tbody td[data-label="Onboarding"],
    .content-area tbody td[data-label="Joined"] {
        font-size: 0.8em;
        color: #999;
    }

    .content-area tbody td[data-label="Onboarding"]::before,
    .content-area tbody td[data-label="Joined"]::before {
        font-size: 0.7em;
        letter-spacing: 0.3px;
    }

    /* Status shows transient save feedback — no label prefix, collapses when empty */
    .content-area tbody td[data-label="Status"]::before {
        display: none;
    }

    .content-area tbody td[data-label="Status"]:not(:has(*)) {
        display: none;
    }

    /* ===== REQUISITION HISTORY TABLE — mobile card layout =====
       Title conflicts with Events; Status display:inline-flex scoped via Req sibling
       so OfficerList's Status collapse rule is unaffected. */

    /* REQ number is the card identity tag — monospace mimics the desktop column style */
    .content-area tbody td[data-label="Req"] {
        display: block;
        font-family: monospace;
        font-size: 0.8em;
        font-weight: 600;
        color: #2980b9;
        padding-bottom: 2px;
    }

    .content-area tbody td[data-label="Req"]::before {
        display: none;
    }

    /* Title overrides the Events Title rule — scoped so Events cards are unaffected */
    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Title"] {
        display: block;
        font-weight: 700;
        font-size: 1rem;
        color: #1a1a2e;
        padding-top: 0;
        padding-bottom: 6px;
        border: none !important;
    }

    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Title"]::before {
        display: none;
    }

    /* Urgency, Cost, and Status inline on one line — Cost sits between them in the DOM
       so all three must be inline-flex; Cost value then acts as a separator between badges */
    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Urgency"],
    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Cost"],
    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Status"] {
        display: inline-flex;
        align-items: center;
        width: auto;
        border: none !important;
        vertical-align: middle;
        padding: 2px 10px 4px 0;
    }

    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Urgency"]::before,
    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Cost"]::before,
    .content-area tbody tr:has(td[data-label="Req"]) td[data-label="Status"]::before {
        display: none;
    }

    /* Cost value stands out between the two badges */
    .content-area tbody td[data-label="Cost"] {
        font-weight: 600;
    }

    /* Date Needed and Submitted are secondary metadata — visible but not prominent */
    .content-area tbody td[data-label="Date Needed"],
    .content-area tbody td[data-label="Submitted"] {
        font-size: 0.8em;
        color: #999;
    }

    .content-area tbody td[data-label="Date Needed"]::before,
    .content-area tbody td[data-label="Submitted"]::before {
        min-width: 0;
        margin-right: 6px;
        font-size: 0.7em;
    }

    /* ===== RECENT TRANSACTIONS TABLE (/plo/finances) — mobile card layout =====
       Date, Type, and Category reuse the existing global [data-label="Date"]/
       [data-label="Type"]/[data-label="Category"] rules above (defined for the
       Events and Document tables) with zero extra CSS — those rules are already
       unscoped by table, so Type/Category automatically become compact inline
       badges and Date's "DATE:" label is already suppressed via the existing
       Date~Type sibling rule. Amount is the one column name unique to this table,
       so it gets its own small bold treatment here, matching the desktop column's
       font-weight:600 emphasis — mirrors the same unscoped pattern already used
       for Requisition History's Cost column just above.
       justify-content:flex-start overrides the base td rule's space-between, which
       was pushing the "AMOUNT" label to the card's left edge and the dollar value
       all the way to the right edge — far apart instead of sitting together. */
    .content-area tbody td[data-label="Amount"] {
        font-weight: 600;
        justify-content: flex-start;
    }

    /* The Description <td> carries inline white-space:nowrap/text-overflow:ellipsis/
       max-width:200px for its desktop table-cell truncation — inline styles beat any
       external class or media-query rule without !important, so the global mobile
       word-break:break-word never actually took effect and the value cut off
       mid-word with no visible ellipsis. !important here is required specifically
       because the conflicting rule is inline, not just higher-specificity CSS —
       same justification as the Requisition History detail grid above.
       justify-content:flex-start is the same fix applied to Amount above: once the
       value no longer wraps onto a new line by default, the base td rule's
       space-between pushes it all the way to the card's right edge, away from the
       "DESCRIPTION" label, instead of sitting right after it. */
    .content-area tbody td[data-label="Description"] {
        white-space: normal !important;
        overflow: visible !important;
        text-overflow: clip !important;
        max-width: none !important;
        justify-content: flex-start;
    }

    /* ===== TRANSACTION LIST TABLE (/plo/finances/transactions) — mobile card layout =====
       Date, Type, Amount, Category, and Description all reuse existing unscoped
       rules above with zero extra CSS — this table shares those exact column names
       with the Recent Transactions table and others. Balance is the one column name
       unique to this table, so it needs its own rule: same bold-value +
       justify-content:flex-start treatment as Amount, for the same reason (the base
       td rule's space-between would otherwise push the balance to the card's right
       edge, away from its "BALANCE" label). Ref #, Receipt, and the unlabeled Void
       action column need no extra CSS — Ref # and Receipt are short single-line
       values that fit the global label/value layout as-is, and the Void column has
       no data-label at all (matching its empty desktop header), so it already gets
       the existing global td:not([data-label]) treatment (right-aligned, no label)
       used for action-only columns elsewhere in this file. */
    .content-area tbody td[data-label="Balance"] {
        font-weight: 600;
        justify-content: flex-start;
    }

    /* Amount and Balance both carry inline text-align:right for their desktop
       table-cell alignment — harmless on desktop (a real table column), but on
       mobile the value is a flex child, not a table cell, so this had no
       layout-positioning effect EXCEPT on the value's own inline text box, which
       still visually nudges it away from the flush-left alignment every other
       field in the card uses. Inline styles beat any external rule without
       !important (same issue already addressed for Description's white-space
       above) — scoped to .txn-list-table specifically so this doesn't reach into
       any other table that might reuse the Amount/Balance column names. */
    .content-area table.txn-list-table tbody td[data-label="Amount"],
    .content-area table.txn-list-table tbody td[data-label="Balance"] {
        text-align: left !important;
    }

    /* ===== TRANSACTION LIST TABLE — card readability pass =====
       Scoped to .txn-list-table (the class on this specific table's <table>
       element) throughout, so none of this reaches the Recent Transactions card
       on /plo/finances (M18) or any other table sharing the global mobile card
       transform — same scoping convention as the text-align fix just above.

       Tighter field padding: the unscoped global td rule's padding:6px 0 has
       never actually applied here, because every td in this table also carries
       an inline padding:10px 14px for its desktop table-cell sizing, and inline
       styles beat non-!important external rules — so fields were rendering at
       the desktop 10px/14px spacing, not the intended 6px, reading as too much
       air between fields. !important forces the tighter, intentionally compact
       mobile spacing here. */
    .content-area table.txn-list-table tbody td {
        padding-top: 3px !important;
        padding-bottom: 3px !important;
    }

    /* Date+Type already render on one line together via existing global rules
       (both become display:inline-flex), which already makes them read as a
       pair — but nothing set that pair apart from the data fields below, so
       every field in the card read with equal weight as one flat list. Bumping
       Date's weight/size gives the pair a "headline" feel next to Type's already
       bold, colored badge. */
    .content-area table.txn-list-table tbody td[data-label="Date"] {
        font-weight: 600;
        font-size: 0.95em !important;
    }

    /* Amount is the first full-width block-level field after the inline
       Date+Type pair (Date/Type are inline-flex; Amount/Balance/Description/etc.
       stay block-level flex rows each on their own line) — giving it extra
       top space plus an inset top shadow (not a real border, which would fight
       the base td rule's border:none!important) draws a clean divider under the
       header pair regardless of exactly how wide Date+Type render. */
    .content-area table.txn-list-table tbody td[data-label="Amount"] {
        margin-top: 6px;
        padding-top: 10px !important;
        box-shadow: inset 0 1px 0 #f0f0f0;
    }

    /* D47 addendum 3 — Ref # and Receipt relied on nothing but the global
       td/td::before label+value rules above, whose justify-content:space-between
       pushes the value all the way to the card's right edge, away from its own
       label — the same root cause already fixed for Date Needed/Submitted on the
       Requisitions card and Amount/Balance just above. flex-start pairs the value
       immediately next to the label instead of leaving a wide gap that read as
       "not paired with this label." Receipt additionally gets flex-wrap:wrap:
       its value can be a short "Open"/"Attach Receipt" button (fits fine inline
       next to the label) or the wider inline upload control (file input +
       Upload/Cancel buttons, min-width:160px) — wrap lets the short states stay
       inline while the upload control gracefully drops to its own line below the
       label instead of overflowing or squeezing. The Void button's <td> has no
       data-label and is untouched by this rule. */
    .content-area table.txn-list-table tbody td[data-label="Ref #"] {
        justify-content: flex-start;
        align-items: center;
        gap: 8px;
    }

    .content-area table.txn-list-table tbody td[data-label="Receipt"] {
        justify-content: flex-start;
        align-items: center;
        gap: 8px;
        flex-wrap: wrap;
    }

    /* ===== PLO REQUISITIONS LIST TABLE (/plo/requisitions) — card polish pass =====
       The shared tr:has(td[data-label="Req"]) rules above already give this table
       (and the Sponsor portal's requisition-history table, which reuses the exact
       same column data-labels) baseline mobile-card behavior. Everything here is
       scoped through .req-list-table — the class added to THIS table's <table>
       element specifically — so none of it reaches that other table, matching the
       same per-table scoping convention used for .txn-list-table above.
       Unlike Transactions, no inline text-align:right exists anywhere in this
       table's markup, so there's nothing to override there — but Date Needed and
       Submitted hit the same underlying root cause as Amount/Balance did: their
       ::before label is intentionally left visible (just shrunk) rather than
       hidden, which means each of those td's still has two flex children, and the
       base td rule's justify-content:space-between (never overridden for these
       two) shoves the value to the card's right edge, away from its own label —
       reading exactly like the "small all-caps right-aligned text" complaint. */
    .content-area table.req-list-table tbody td[data-label="Date Needed"],
    .content-area table.req-list-table tbody td[data-label="Submitted"] {
        justify-content: flex-start;
    }

    /* Tighter field padding, matching the density already established for
       Transactions — no inline padding exists on these td's to fight, so this
       wins on specificity alone without needing !important. */
    .content-area table.req-list-table tbody td {
        padding-top: 3px;
        padding-bottom: 3px;
    }

    /* M19 fix 2 — Req # and Urgency badge sit side-by-side as a single header
       row instead of stacking. M19's flex-column + order approach could
       reorder fields but couldn't put two of them on the same visual line —
       each order'd item is still its own row in a column flex container. CSS
       Grid can place Req and Urgency into the same grid row at different
       columns while every other field still spans the full row width below
       them. Scoped to .req-list-table and only inside this max-width:768px
       media query, so desktop (where tr isn't a grid container at all) and
       the Sponsor portal's requisition-history table (same data-labels, but
       not this class) are both untouched. */
    .content-area table.req-list-table tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        column-gap: 10px;
        align-items: center;
        grid-template-areas:
            "req        urgency"
            "title      title"
            "cost       cost"
            "dateneeded dateneeded"
            "submitted  submitted"
            "status     status";
    }

    /* Maps each field to its slot in the template above. Req's 1fr column
       stretches to fill the row (matching how it always spanned full width);
       Urgency's auto column sizes to the badge's own content so it hugs the
       right side of the header row instead of stretching. data-label
       attributes and td order in the Razor markup are completely untouched —
       only where each td is painted inside the grid changes. */
    .content-area table.req-list-table tbody td[data-label="Req"]         { grid-area: req; }
    .content-area table.req-list-table tbody td[data-label="Urgency"]     { grid-area: urgency; }
    .content-area table.req-list-table tbody td[data-label="Title"]       { grid-area: title; }
    .content-area table.req-list-table tbody td[data-label="Cost"]        { grid-area: cost; }
    .content-area table.req-list-table tbody td[data-label="Date Needed"] { grid-area: dateneeded; }
    .content-area table.req-list-table tbody td[data-label="Submitted"]   { grid-area: submitted; }
    .content-area table.req-list-table tbody td[data-label="Status"]      { grid-area: status; }

    /* Req and Urgency now share one row, so they need matching compact
       vertical padding instead of Req's old standalone bottom-pairing
       padding — align-items:center on the grid (above) handles vertical
       alignment between the small monospace Req text and the Urgency badge. */
    .content-area table.req-list-table tbody td[data-label="Req"],
    .content-area table.req-list-table tbody td[data-label="Urgency"] {
        padding-top: 2px;
        padding-bottom: 2px;
    }

    /* Title is still the first field after the Req+Urgency header row — same
       divider treatment as before (an inset top shadow, not a real border,
       which would fight the base td rule's border:none!important), now
       marking the boundary under the combined header row instead of under
       Urgency alone. */
    .content-area table.req-list-table tbody td[data-label="Title"] {
        margin-top: 4px;
        padding-top: 8px;
        padding-bottom: 8px;
        box-shadow: inset 0 1px 0 #f0f0f0;
    }

    /* Status still sits alone at the bottom — breathing room keeps it from
       reading as flush against Submitted right above it. */
    .content-area table.req-list-table tbody td[data-label="Status"] {
        margin-top: 6px;
    }

    /* Status and Urgency are both badges, but Status is the more important one
       to register at a glance — slightly larger/bolder so it visually
       outweighs Urgency, even sitting right next to it in the header row. */
    .content-area table.req-list-table tbody td[data-label="Status"] .badge {
        font-size: 0.78em;
        padding: 4px 10px;
    }

    .content-area table.req-list-table tbody td[data-label="Urgency"] .badge {
        font-size: 0.7em;
        opacity: 0.92;
    }

    /* Cost is bumped past the shared font-weight:600 (set globally above) and
       up in size to make it the clear focal point of its own line, the same
       role Amount plays on the Transactions card. */
    .content-area table.req-list-table tbody td[data-label="Cost"] {
        font-size: 1em;
        font-weight: 700;
    }

    /* ===== ELECTIONS LIST TABLE (/plo/elections) — mobile card layout (M21) =====
       Same root cause as M18/M20/M19: zero data-label attributes meant the
       global mobile table-to-card transform had nothing to label these
       values, producing stacked right-aligned text with no labels. Scoped
       entirely through .elec-list-table (the class added to THIS table's
       <table> element) so none of this reaches any other table on the shared
       transform. Uses CSS Grid (not flex-column+order) for the same reason
       established in M19 fix 2 — only Grid can place two fields on the same
       visual line, here pairing Type+Created and Status+Votes. */
    .content-area table.elec-list-table tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        column-gap: 10px;
        align-items: center;
        grid-template-areas:
            "title  title"
            "type   created"
            "status votes";
    }

    .content-area table.elec-list-table tbody td[data-label="Title"]   { grid-area: title; }
    .content-area table.elec-list-table tbody td[data-label="Type"]    { grid-area: type; }
    .content-area table.elec-list-table tbody td[data-label="Created"] { grid-area: created; }
    .content-area table.elec-list-table tbody td[data-label="Status"]  { grid-area: status; }
    .content-area table.elec-list-table tbody td[data-label="Votes"]   { grid-area: votes; }

    /* Title is the card's header/identity row — the primary thing an officer
       scans for, same role Req#+Title plays on the Requisitions card. The td
       carries an inline font-weight:500 for its desktop table-cell styling;
       inline styles beat any external rule without !important, so without
       this override Title would render lighter than intended despite the
       existing unscoped global Title rule (display:block, 1.05rem) already
       giving it size and its own full-width line. */
    .content-area table.elec-list-table tbody td[data-label="Title"] {
        font-weight: 700 !important;
    }

    /* Type + Created pair up as muted secondary metadata below the header —
       their inline color:#666 already keeps them visually quiet, so no color
       override is needed here, just spacing. The inset top shadow (not a
       real border, which would fight the base td rule's border:none!important)
       marks the boundary under the Title header row, same technique used for
       Title's divider on the Requisitions card. */
    .content-area table.elec-list-table tbody td[data-label="Type"] {
        margin-top: 4px;
        padding-top: 8px;
        box-shadow: inset 0 1px 0 #f0f0f0;
    }

    /* Status is the most actionable field at a glance (Draft/Open/Closed) — a
       larger, bolder badge gives it the visual weight the task calls for, the
       same role Status played over Urgency in the M19 requisitions fix.
       StatusBadgeStyle() only sets background/color inline, so no !important
       fight here. */
    .content-area table.elec-list-table tbody td[data-label="Status"] .badge {
        font-size: 0.82em;
        font-weight: 700;
        padding: 4px 12px;
    }

    /* Votes sits inline next to Status as a quiet supporting number —
       left-aligned to match every other field on the card instead of the
       inline text-align:right carried over from the desktop column (the
       matching inline text-align:right on the <th> only affects desktop;
       thead is hidden on mobile via display:none, so it never needs
       touching). justify-content:flex-start is the same fix already applied
       to Date Needed/Submitted on the Requisitions card and Amount/Balance
       on the Transactions card — the base td rule's space-between would
       otherwise shove the value away from its "VOTES" label. */
    .content-area table.elec-list-table tbody td[data-label="Votes"] {
        text-align: left !important;
        justify-content: flex-start;
    }

    /* ===== CSV IMPORT PREVIEW TABLE (/plo/finances/import) — mobile card layout (M26) =====
       Same root cause as M18/M20/M21: zero data-label attributes meant the global
       mobile table-to-card transform had nothing to label these values. Scoped
       entirely through .import-preview-table so none of this reaches Transactions,
       Recent Transactions, Requisitions, or Elections. This is the first table in
       the series with a leading checkbox column — Import? doesn't pair with a text
       label the way other fields do, so it's paired with Date in a header row
       instead (same CSS Grid technique as Req#+Urgency and Type+Created), since
       "is this row included" is the first thing the Treasurer needs to see/control.
       Status and Type badges sit side by side on their own row near the top of the
       card — the single most important things to scan per row, the same priority
       Status got in the M19 requisitions and M21 elections fixes. Description
       stays a simple stacked label+value row (only needs an override for the same
       inline-truncation-style problem Transactions' Description had).
       M26 fix — a 3rd grid column was added specifically so the Status/Type
       pairing could share a row without disturbing the checkbox+date row above.
       Auto-sized grid columns size to the WIDEST single-column item assigned to
       them across every row in the grid, not per-row — putting Status (a wide
       badge like "Possible Duplicate") into the same lone auto column the
       checkbox uses would have inflated that column for the checkbox row too,
       pushing Date away from the checkbox and breaking the established header
       pairing. Spanning Status across columns 1+2 (an auto column plus a flexible
       1fr column) instead means only the checkbox's own intrinsic width drives
       column 1's auto size, while column 2 — flexible — absorbs whatever extra
       width Status's badge needs, leaving column 1 (and the checkbox+date row)
       untouched. Type occupies column 3 alone, giving the two badges a natural
       side-by-side split close to even. */
    .content-area table.import-preview-table tbody tr {
        display: grid;
        grid-template-columns: auto 1fr 1fr;
        column-gap: 10px;
        align-items: center;
        grid-template-areas:
            "checkbox date   date"
            "status    status type"
            "amount    amount amount"
            "description description description";
    }

    .content-area table.import-preview-table tbody td[data-label="Import?"]     { grid-area: checkbox; }
    .content-area table.import-preview-table tbody td[data-label="Date"]        { grid-area: date; }
    .content-area table.import-preview-table tbody td[data-label="Status"]      { grid-area: status; }
    .content-area table.import-preview-table tbody td[data-label="Amount"]      { grid-area: amount; }
    .content-area table.import-preview-table tbody td[data-label="Type"]        { grid-area: type; }
    .content-area table.import-preview-table tbody td[data-label="Description"] { grid-area: description; }

    /* Checkbox needs no text label — its presence is self-explanatory, same
       reasoning as Color/Req# suppressing their own labels elsewhere. Sized up
       from the browser default so it's a comfortable tap target next to the date. */
    .content-area table.import-preview-table tbody td[data-label="Import?"] {
        display: flex;
        align-items: center;
        justify-content: flex-start;
        border: none !important;
        padding: 4px 8px 4px 0;
    }

    .content-area table.import-preview-table tbody td[data-label="Import?"]::before {
        display: none;
    }

    .content-area table.import-preview-table tbody td[data-label="Import?"] input[type="checkbox"] {
        width: 20px;
        height: 20px;
        cursor: pointer;
    }

    /* Date sits beside the checkbox as the card's header row — bumped to a bold
       headline weight (the existing unscoped global Date rule keeps it small/muted,
       which reads fine as a secondary field elsewhere but is too quiet to anchor a
       header row here). */
    .content-area table.import-preview-table tbody td[data-label="Date"] {
        font-weight: 700;
        font-size: 1rem;
        color: #1a1a2e;
        padding: 4px 0;
    }

    /* Status — the single most actionable field per row, now sharing a row with
       Type instead of sitting full-width below it (M26 fix). Switched from block
       to flex so the badge hugs the left edge of its (column 1+2) area instead of
       stretching/wrapping at block width. The previous inset-shadow top divider
       is dropped: it used to span the row's full width because Status WAS the
       full row; now that the row only covers columns 1-2 (Type occupies column 3
       separately), a divider on Status alone would draw a partial-width line that
       stops short of Type's column — reading as a stray underline rather than a
       clean section break — so it's removed rather than kept half-broken. The
       badge itself (.import-badge, defined in FinancialImport.razor's own <style>
       block) is still sized up here so it reads as the card's focal point. */
    .content-area table.import-preview-table tbody td[data-label="Status"] {
        display: flex;
        align-items: center;
        border: none !important;
        margin-top: 4px;
        padding: 4px 0;
    }

    .content-area table.import-preview-table tbody td[data-label="Status"]::before {
        display: none;
    }

    .content-area table.import-preview-table tbody td[data-label="Status"] .import-badge {
        font-size: 0.85em;
        padding: 4px 12px;
    }

    /* Amount — same inline text-align:right-fights-the-flex-row problem already
       fixed for Transactions' Amount/Balance; left-aligns the value and pairs it
       with its label instead of shoving it to the card's right edge. gap:4px (vs.
       the base td rule's 8px) pulls the dollar value in closer to the "AMOUNT"
       label, per Andy's request that the two sit visually close together rather
       than far apart. */
    .content-area table.import-preview-table tbody td[data-label="Amount"] {
        text-align: left !important;
        justify-content: flex-start;
        border: none !important;
        padding-top: 4px;
        padding-bottom: 4px;
        gap: 4px;
    }

    /* Type — now shares a row with Status (column 3) instead of its own full-width
       row below. The existing unscoped global Type rule already gives it a
       compact inline-flex badge+label sized to fit comfortably in that column;
       only spacing needs adjustment here. */
    .content-area table.import-preview-table tbody td[data-label="Type"] {
        padding-top: 4px;
        padding-bottom: 4px;
    }

    /* Description — same inline white-space:nowrap/text-overflow:ellipsis/
       max-width:220px desktop truncation problem Transactions' Description had;
       inline styles beat any external rule without !important. Secondary field,
       so it stays the plain label+value row everything defaults to — only
       flex-start is needed to pair the value next to its label instead of the
       base rule's space-between shoving it to the card's right edge. */
    .content-area table.import-preview-table tbody td[data-label="Description"] {
        white-space: normal !important;
        overflow: visible !important;
        text-overflow: clip !important;
        max-width: none !important;
        justify-content: flex-start;
        border: none !important;
        padding-top: 4px;
        padding-bottom: 4px;
    }

    /* ===== REQUISITION HISTORY DETAIL PANEL — mobile stack =====
       Inline styles on the grid divs have higher specificity than class rules,
       so !important is required to override grid-template-columns on mobile. */

    /* Two-column desktop layout collapses to single column */
    .req-history-detail {
        grid-template-columns: 1fr !important;
        gap: 16px !important;
    }

    /* Inner field pairs (Budget/Cost, Vendor/Contact) also collapse */
    .req-history-detail-grid {
        grid-template-columns: 1fr !important;
    }

    /* Cancel + Close stack vertically — full-width buttons are easier thumb targets */
    .req-history-detail + div {
        flex-direction: column !important;
        align-items: stretch !important;
        gap: 8px;
        padding: 0 16px 16px !important;
    }

    .req-history-detail + div .btn {
        justify-content: center;
    }

    /* ===== PLO REQUISITIONS STATS BAR — mobile wrap =====
       5-column grid overflows off-screen at narrow widths; 2 columns lets cards wrap
       cleanly. With 5 items and 2 columns, Total Value naturally lands alone on row 3. */
    .req-stats-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    /* Stack form rows vertically on mobile — flex side-by-side is too cramped on narrow screens */
    .form-row {
        flex-direction: column;
        gap: 0;
    }

    /* Restore normal bottom margin when stacked so fields don't run together */
    .form-row .form-group {
        margin-bottom: 18px;
    }

    /* ===== FAMILY EVENTS — mobile overrides =====
       These mirror the desktop classes defined below but collapse the flex row
       to a vertical stack so event info is readable on narrow screens. */
    .family-event-row {
        flex-wrap: wrap;
        gap: 6px;
        padding: 12px 16px;
    }

    .family-event-time {
        min-width: unset;
        width: 100%;
        font-size: 0.82em;
        color: #888;
        white-space: nowrap;
    }

    .event-type-dot {
        display: none;
    }

    .family-event-title {
        font-size: 1rem;
        font-weight: 600;
        width: 100%;
        flex: unset;
    }

    .family-event-location {
        width: 100%;
        text-align: left !important;
        font-size: 0.82em;
        color: #888;
        word-break: break-word;
        white-space: normal;
    }
}

/* ===== FAMILY EVENTS — event list page =====
   These classes replace what was previously an inline <style> block in FamilyEvents.razor.
   Blazor InteractiveServerRenderMode(prerender:false) delays component style injection
   until after the first render, causing flex containers to briefly (or permanently) collapse
   before styles arrive. Loading these via portal.css (a static asset) ensures they are
   present unconditionally before any component renders. */

.events-section-label {
    font-size: 0.78em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--content-text-color, #888);
    margin: 0 0 12px 0;
}

/* padding:0 overrides form-card's default 30px; overflow:hidden clips row edges to card corners. */
.events-list-card {
    padding: 0;
    overflow: hidden;
}

.events-day-header {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 10px;
    padding: 10px 20px;
    background: #f8f9fa;
    border-bottom: 1px solid #e8eaed;
}

.events-day-name {
    font-weight: 700;
    color: #1a1a2e;
    font-size: 0.95em;
}

.events-day-date {
    font-size: 0.85em;
    color: #3498db;
    font-weight: 600;
}

.family-event-row {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 13px 20px;
    border-bottom: 1px solid #f0f0f0;
}

.family-event-time {
    min-width: 140px;
    color: #666;
    font-size: 0.9em;
    white-space: nowrap;
}

.event-type-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
}

/* flex:1 lets the title fill remaining space between the dot and location/signup. */
.family-event-title {
    flex: 1;
    font-weight: 500;
    color: #1a1a2e;
}

.event-capacity-label {
    font-size: 0.8em;
    color: #888;
    margin-left: 8px;
}

.family-event-location {
    color: #888;
    font-size: 0.85em;
    text-align: right;
}

.event-signup-status {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}

.badge-waitlist {
    font-size: 0.8em;
    color: #856404;
    background: #fff3cd;
    border-radius: 12px;
    padding: 3px 8px;
    white-space: nowrap;
}

.badge-signed-up {
    font-size: 0.8em;
    color: #155724;
    background: #d4edda;
    border-radius: 12px;
    padding: 3px 8px;
    white-space: nowrap;
}

.event-signup-btn {
    flex-shrink: 0;
}

/* ===== CARD ELEVATION (GLOBAL) =====
   Every white card in the app shares this elevation treatment — a subtle
   shadow + softened corners. This is the single biggest lever for moving the
   app's feel from "flat admin form" to "modern app." Placed at the end of the
   file so this combined rule wins over earlier per-class border-radius
   declarations via CSS cascade order (same specificity = last rule wins).
   .elec-stat-tile is included so Elections stat rows get the same treatment;
   its border-radius may be capped at 8px by the inline <style> in those
   components, but the box-shadow applies regardless. */
.form-card,
.stat-card,
.member-card,
.app-tile,
.mini-stat-tile,
.elec-stat-tile {
    border-radius: 14px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07);
}
