/* ================================================================================================
   DESIGN SYSTEM
   Modernization pass over the existing studiojaed.com-derived palette (white ground, near-black
   ink, the brand's brick red used as a sparing accent) - the brand identity is kept as-is (it was
   deliberately matched to the real firm's site earlier in this project), but the execution around
   it is tightened: a real type/spacing scale, subtle elevation instead of flat 1px borders
   everywhere, consistent status-badge language, and new components (project cards, richer clash
   test cards, empty states) built ON these same tokens rather than one-off styles. Restrained on
   purpose - a construction-coordination tool, not a marketing site: no gradients, no neon, no
   decorative motion.
   ================================================================================================ */
:root {
    /* Brand (unchanged values, see header comment) */
    --brand-red-bright: #E2364A;
    --brand-red-deep: #A51724;
    --brand-red-action: #C41230;
    --brand-red-secondary: #7A121C;
    --brand-red-tint: #FBEAEC;

    /* Company-pill palette (see .company-pill-0..7 below, and companyColorClass in app.js) -
       deliberately its own set of hues, distinct from the semantic status colors (--status-*
       below / .badge-status--*) so a company tag can never be misread as a review-status color.
       8 is enough headroom for a real project's actual number of trades/consultants without
       colors repeating in practice. */
    --company-pill-0-bg: #EDE9FE; --company-pill-0-ink: #6D28D9; /* violet */
    --company-pill-1-bg: #CCFBF1; --company-pill-1-ink: #0F766E; /* teal */
    --company-pill-2-bg: #DBEAFE; --company-pill-2-ink: #1D4ED8; /* blue */
    --company-pill-3-bg: #FCE7F3; --company-pill-3-ink: #BE185D; /* pink */
    --company-pill-4-bg: #E0E7FF; --company-pill-4-ink: #4338CA; /* indigo */
    --company-pill-5-bg: #FFEDD5; --company-pill-5-ink: #C2410C; /* orange */
    --company-pill-6-bg: #FAE8FF; --company-pill-6-ink: #A21CAF; /* fuchsia */
    --company-pill-7-bg: #E2E8F0; --company-pill-7-ink: #334155; /* slate */

    /* Wash used by the stat/project "glass" cards (see DASHBOARD section) - a light neutral grey
       here, since a red-tinted wash read as odd in light mode; dark mode keeps the red-maroon
       version (redefined below) since that one wasn't flagged and still fits the darker ground. */
    --card-glass-tint: var(--card-bg-inner);

    /* Surfaces & ink */
    --bg: #F7F7F8;
    --card-bg: #FFFFFF;
    --card-bg-inner: #F3F4F6;
    --text-primary: #1A1A1A;
    --text-secondary: #6B7280;
    --text-heading: #101114;
    --track-bg: #E5E7EB;
    --border: #E5E7EB;
    --border-strong: #D1D5DB;
    --heatmap-bg: #0B0F1A; /* a fixed "night view" panel by design - not overridden for dark mode */

    /* Left icon nav rail - intentionally fixed dark in BOTH themes (not redefined below), same
       reasoning as --heatmap-bg: it's meant to read as a constant dark anchor for navigation,
       like the rail in the Dribbble task-management reference this was modeled on, not a surface
       that should flip white in light mode. */
    --rail-bg: #1B1D22;
    --rail-icon: #8B92A0;

    /* Status language - used everywhere via .badge-status, never one-off colors per page */
    --status-green-bg: #DCFCE7; --status-green-ink: #15803D;
    --status-red-bg: #FEE2E2;   --status-red-ink: #B91C1C;
    --status-amber-bg: #FEF3C7; --status-amber-ink: #92400E;
    --status-blue-bg: #DBEAFE;  --status-blue-ink: #1D4ED8;
    /* Was #41FDFE (a near-white neon cyan) - unreadable as text on the equally pale --status-cyan-bg
       below. Swapped for a saturated teal that keeps the "cyan" identity but actually passes contrast
       as ink, matching how every other status-*-ink here is a solid AA-legible color on its own pale
       bg (see --status-blue-ink/--status-green-ink etc.) - this one was the sole exception. */
    --status-cyan-bg: #CFFAFE;  --status-cyan-ink: #0E7490;
    --status-gray-bg: #F1F5F9;  --status-gray-ink: #475569;
    /* Vivid orange/yellow, distinct from the muted --status-amber-ink (that one's tuned as
       readable text-on-pale-fill; this one's meant to read as a bright highlight color, same job
       --status-cyan-ink already does) - added for the clash gallery's In Progress review stage. */
    --status-orange-bg: #FEF3C7; --status-orange-ink: #F59E0B;
    /* Two more for the clash lifecycle's Deferred/Closed stages (see ClashReviewStatus
       server-side) - purple and slate, distinct from every other stage's color. */
    --status-purple-bg: #F3E8FF; --status-purple-ink: #7E22CE;
    --status-slate-bg: #E2E8F0;  --status-slate-ink: #334155;

    /* Coordinator-set clash priority (see ClashPriority server-side) - a separate palette from
       the status language above so priority and status never look like the same kind of thing. */
    --priority-low-bg: #F1F5F9;    --priority-low-ink: #64748B;
    --priority-medium-bg: #DBEAFE; --priority-medium-ink: #1D4ED8;
    --priority-high-bg: #FEF3C7;   --priority-high-ink: #B45309;
    --priority-critical-bg: #FEE2E2; --priority-critical-ink: #B91C1C;

    /* Type scale */
    /* Phase 1 design-system pass: one new tier above 2xl, for the genuine "Display" role
       (.text-display below) - a rare hero number, not a page title (.page-title stays --text-2xl). */
    --text-3xl: 32px;
    --text-2xl: 22px;
    --text-xl: 17px;
    --text-lg: 14px;
    --text-md: 13px;
    --text-sm: 12px;
    --text-xs: 11px;
    --text-2xs: 10px;

    /* Spacing scale (4px base) */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-8: 32px;

    /* Elevation & shape */
    /* Phase 1 design-system pass: the smallest tier several components independently invented
       their own value for (.hint code, progress bars, ...) - see the pass's own audit notes. */
    --radius-xs: 3px;
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 14px;
    --radius-pill: 999px;
    --shadow-sm: 0 1px 2px rgba(16, 17, 20, 0.05), 0 1px 1px rgba(16, 17, 20, 0.03);
    --shadow-md: 0 4px 14px rgba(16, 17, 20, 0.08), 0 1px 3px rgba(16, 17, 20, 0.04);
    --shadow-lg: 0 20px 60px rgba(16, 17, 20, 0.20);

    color-scheme: light; /* native form controls/scrollbars render light-appropriate */
}

/* ================================================================================================
   DARK MODE
   Same token set redefined - every component below already reads these variables instead of a
   literal color, so it themes automatically with no per-component dark CSS. Two blocks on purpose:
   the media-query one follows the OS setting for anyone who's never touched the toggle (see
   #theme-toggle-btn in app.js); the [data-theme="dark"] one is what that toggle actually flips, and
   wins over the OS setting either direction because of the plain :root it's compared against.
   Brand red (--brand-red-bright/action/secondary) is left as-is in both themes - already vivid
   enough to read on a dark surface; only --brand-red-tint/--brand-red-deep (a pale pink fill + a
   dark ink meant to sit on top of it) need dark-appropriate counterparts. --heatmap-bg and
   .trade-view-report-frame's white are deliberately NOT overridden - see their own comments.
   ================================================================================================ */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #18191D;
        --card-bg: #212328;
        --card-bg-inner: #2A2D33;
        --text-primary: #E8E9EB;
        --text-secondary: #9AA0AC;
        --text-heading: #FFFFFF;
        --track-bg: #363940;
        --border: #2F323A;
        --border-strong: #40444D;
        --brand-red-tint: #3A1820;
        --brand-red-deep: #FF7E90;
        --card-glass-tint: #3A1820;
        --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35), 0 1px 1px rgba(0, 0, 0, 0.25);
        --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.3);
        --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.55);
        color-scheme: dark;
        --company-pill-0-bg: #2E1B4D; --company-pill-0-ink: #C4B5FD;
        --company-pill-1-bg: #0F2E2A; --company-pill-1-ink: #5EEAD4;
        --company-pill-2-bg: #1E2A47; --company-pill-2-ink: #93C5FD;
        --company-pill-3-bg: #3A1830; --company-pill-3-ink: #F9A8D4;
        --company-pill-4-bg: #241F42; --company-pill-4-ink: #A5B4FC;
        --company-pill-5-bg: #3A2412; --company-pill-5-ink: #FDBA74;
        --company-pill-6-bg: #341530; --company-pill-6-ink: #F0ABFC;
        --company-pill-7-bg: #262E3B; --company-pill-7-ink: #CBD5E1;
        /* Phase 1 design-system pass: the status/priority language was the one token category left
           un-themed - a pale pastel bg (e.g. --status-green-bg's light mint) sits with much lower
           contrast against a dark #18191D ground than it does against white. Same "deep saturated
           bg + bright ink" recipe already used for the company-pill palette above, one hue per
           existing status/priority meaning - never a new color, never a renamed one. */
        --status-green-bg: #14311F;  --status-green-ink: #4ADE80;
        --status-red-bg: #3A1517;    --status-red-ink: #FCA5A5;
        --status-amber-bg: #3A2D0E;  --status-amber-ink: #FBBF24;
        --status-blue-bg: #1E2A47;   --status-blue-ink: #93C5FD;
        --status-cyan-bg: #0E3A3D;   --status-cyan-ink: #67E8F9;
        --status-gray-bg: #262B33;   --status-gray-ink: #CBD5E1;
        --status-orange-bg: #3A2D0E; --status-orange-ink: #FDBA74;
        --status-purple-bg: #2E1B4D; --status-purple-ink: #C4B5FD;
        --status-slate-bg: #262E3B;  --status-slate-ink: #CBD5E1;
        --priority-low-bg: #262B33;      --priority-low-ink: #94A3B8;
        --priority-medium-bg: #1E2A47;   --priority-medium-ink: #93C5FD;
        --priority-high-bg: #3A2D0E;     --priority-high-ink: #FBBF24;
        --priority-critical-bg: #3A1517; --priority-critical-ink: #FCA5A5;
    }
}
:root[data-theme="dark"] {
    --bg: #18191D;
    --card-bg: #212328;
    --card-bg-inner: #2A2D33;
    --text-primary: #E8E9EB;
    --text-secondary: #9AA0AC;
    --text-heading: #FFFFFF;
    --track-bg: #363940;
    --border: #2F323A;
    --border-strong: #40444D;
    --card-glass-tint: #3A1820;
    --brand-red-tint: #3A1820;
    --brand-red-deep: #FF7E90;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35), 0 1px 1px rgba(0, 0, 0, 0.25);
    --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.55);
    color-scheme: dark;
    --company-pill-0-bg: #2E1B4D; --company-pill-0-ink: #C4B5FD;
    --company-pill-1-bg: #0F2E2A; --company-pill-1-ink: #5EEAD4;
    --company-pill-2-bg: #1E2A47; --company-pill-2-ink: #93C5FD;
    --company-pill-3-bg: #3A1830; --company-pill-3-ink: #F9A8D4;
    --company-pill-4-bg: #241F42; --company-pill-4-ink: #A5B4FC;
    --company-pill-5-bg: #3A2412; --company-pill-5-ink: #FDBA74;
    --company-pill-6-bg: #341530; --company-pill-6-ink: #F0ABFC;
    --company-pill-7-bg: #262E3B; --company-pill-7-ink: #CBD5E1;
    /* See the @media block above's own comment - kept identical here, same reasoning as every
       other token this file already duplicates across both dark-mode blocks. */
    --status-green-bg: #14311F;  --status-green-ink: #4ADE80;
    --status-red-bg: #3A1517;    --status-red-ink: #FCA5A5;
    --status-amber-bg: #3A2D0E;  --status-amber-ink: #FBBF24;
    --status-blue-bg: #1E2A47;   --status-blue-ink: #93C5FD;
    --status-cyan-bg: #0E3A3D;   --status-cyan-ink: #67E8F9;
    --status-gray-bg: #262B33;   --status-gray-ink: #CBD5E1;
    --status-orange-bg: #3A2D0E; --status-orange-ink: #FDBA74;
    --status-purple-bg: #2E1B4D; --status-purple-ink: #C4B5FD;
    --status-slate-bg: #262E3B;  --status-slate-ink: #CBD5E1;
    --priority-low-bg: #262B33;      --priority-low-ink: #94A3B8;
    --priority-medium-bg: #1E2A47;   --priority-medium-ink: #93C5FD;
    --priority-high-bg: #3A2D0E;     --priority-high-ink: #FBBF24;
    --priority-critical-bg: #3A1517; --priority-critical-ink: #FCA5A5;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    font-family: "Poppins", "Segoe UI", system-ui, sans-serif;
    background: var(--bg);
    color: var(--text-primary);
    font-size: var(--text-md);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

/* ---------- Typography utilities ---------- */
.page-title { font-size: var(--text-2xl); font-weight: 700; color: var(--text-heading); margin: 0; letter-spacing: -0.01em; }
.section-title {
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary);
    margin: 0 0 var(--space-3) 0;
}
.text-meta { font-size: var(--text-2xs); color: var(--text-secondary); }
.text-muted { color: var(--text-secondary); }

/* One per tab-panel (see index.html) - the actual textual "you are here" confirmation the nav rail's
   icon-only buttons can't provide on their own (only a hover tooltip names them). Kept to a title
   alone, no subtitle/description row - the breadcrumb below the header already carries the current
   project, and every tab's own first card explains itself; a second explanatory line here would
   just be duplicate vertical space on a page whose whole job is showing real coordination data
   without scrolling. */
.page-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); }
.page-header .page-title { margin: 0; }

/* ================================================================================================
   APP SHELL - left icon nav rail (full height, sticky) + a right column holding the header,
   tab content, and footer. See index.html's #app-shell comment for why the rail's buttons are
   still plain .nav-item/#sidebar elements under the hood.
   ================================================================================================ */
#app-shell { display: flex; align-items: stretch; }

/* Phase 2: widened from a 72px icon-only rail into a real labeled sidebar (brand + Org/Project
   switcher + grouped nav) - align-items:stretch (was center) so those full-width children can
   actually fill it; each one still controls its own internal padding rather than relying on the
   rail's own padding for horizontal spacing, since the switcher/nav-group-labels/nav-items all
   want slightly different edge-to-edge treatments. */
/* overflow-y is deliberately NOT set here (found during live testing - see #sidebar's own comment
   for why: a non-visible overflow-y forces the browser to also treat overflow-x as auto instead of
   visible, per the CSS overflow spec's own "one axis visible, one not -> the visible one computes
   as auto too" rule - that silently clipped the project switcher popover below, which is 320px
   wide and meant to overflow past this rail's own 244px into the content area). Any vertical
   scrolling this rail's contents need is scoped to #sidebar alone instead, a sibling of the
   switcher/popover, not a container they sit inside. */
#nav-rail {
    width: 244px;
    flex-shrink: 0;
    background: var(--rail-bg);
    position: sticky;
    top: 0;
    height: 100vh;
    align-self: flex-start;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-3);
    box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15);
    z-index: 30;
}

.rail-brand { padding: 0 var(--space-1) var(--space-2); }
/* Width-based, not height-based - the Collision Suite wordmark is a wide horizontal lockup
   (~11:1), not the old roughly-square mark this rule was originally sized for. Fixed to fit
   within .rail-brand's own content width (rail 244px - its own padding - this padding) with
   room to spare, rather than height:Npx, which would blow past the rail's edge at this aspect
   ratio. .rail-logo-icon (the cropped icon mark, no wordmark) takes over at the narrow collapsed
   rail below - see its own rule in the 560px breakpoint. */
.rail-logo { width: 190px; height: auto; display: block; }
.rail-logo-icon { display: none; }

#shell-main {
    flex: 1;
    min-width: 0; /* lets flex children shrink below their content's natural width instead of overflowing */
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* ================================================================================================
   HEADER
   ================================================================================================ */
header {
    background: var(--card-bg);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 20;
}

/* Phase 2: brand moved into the rail (see .rail-brand) - this row now holds only .header-utility
   (notifications/account/login), still anchored to the header's true right edge via that element's
   own margin-left:auto rather than floating centered. */
.header-row {
    display: flex;
    align-items: center;
    gap: var(--space-6);
    padding: var(--space-3) var(--space-6);
}

.brand {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    text-decoration: none;
}

/* Light/dark logo swap - still used by the login modal's two <img class="login-logo logo-light/
   dark"> tags (see login.html markup), one hidden via these rules depending on theme, same three-
   state pattern as the color tokens above (system preference, then the explicit toggle overriding
   it either direction). The rail's own brand mark (.rail-logo) deliberately does NOT use this
   swap - see its own comment in index.html for why. */
img.logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .logo-light { display: none; }
    :root:not([data-theme="light"]) .logo-dark { display: block; }
}
:root[data-theme="dark"] .logo-light { display: none; }
:root[data-theme="dark"] .logo-dark { display: block; }

.header-utility {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-left: auto;
    flex-shrink: 0;
}

/* Labeled sidebar nav (lives in #nav-rail, its own full-height column - see the APP SHELL section
   above). Admin-only items sit under their own "Administration" label (see .nav-group-label /
   #nav-admin-divider) instead of a bare divider line. min-height:0 + overflow-y:auto scoped HERE
   (not on #nav-rail itself - see that rule's own comment on why) so a viewport too short to fit
   every nav item scrolls just this list, without clipping the switcher popover above it, which
   needs to overflow past the rail's own width, not be contained by it. */
#sidebar {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    min-height: 0;
    overflow-y: auto;
}

/* Phase 2: full-width, icon + label rows (was a centered 44x44 icon-only square). Specificity
   (#nav-rail .nav-item = an id + a class) beats the bare .nav-item rules (NAVIGATION ITEMS,
   further below - written for an even earlier horizontal-row iteration) regardless of source
   order, so this doesn't need !important. Fixed rail-appropriate colors throughout (--rail-icon,
   literal white/rgba), not theme tokens - the rail's own background is intentionally dark in both
   themes (see --rail-bg's comment), so a theme-adaptive color like --text-secondary would go
   dark-on-dark and vanish in light mode. */
#nav-rail .nav-item {
    width: 100%;
    height: auto;
    padding: 9px 10px;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 10px;
    border-left: none;
    border-radius: var(--radius-md);
    color: var(--rail-icon);
    font-size: var(--text-sm);
    font-weight: 500;
    text-align: left;
}
#nav-rail .nav-item svg { flex-shrink: 0; }
#nav-rail .nav-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#nav-rail .nav-item:hover { background: rgba(255, 255, 255, 0.08); color: #FFFFFF; }
#nav-rail .nav-item.active { background: var(--brand-red-action); color: #FFFFFF; font-weight: 600; }
/* Author-origin display:flex above always beats the browser's built-in [hidden] default regardless
   of specificity - without this, applyRoleGating's navAdmin.hidden/navViewer.hidden (app.js) never
   actually hid the Admin/3D Viewer items for a non-Admin role; they just kept rendering as normal
   flex items. Only ever surfaced when actually testing a non-Admin account, since an Admin session
   is supposed to see both anyway. */
#nav-rail .nav-item[hidden] { display: none; }

/* The "Workspace"/"Administration" section labels above each nav group - #nav-admin-divider is the
   exact same element applyRoleGating already shows/hides (see its own JS comment), just rendering
   as a real label now instead of a thin divider line, so the role boundary carries information
   instead of only implying one. */
.nav-group-label {
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.35);
    padding: var(--space-3) var(--space-2) 4px;
}
.nav-group-label:first-child { padding-top: 4px; }
/* Same display:flex-vs-[hidden] cascade note as #nav-rail .nav-item[hidden] above. */
.nav-group-label[hidden] { display: none; }

/* Pinned to the bottom of the rail's own full-height flex column (#nav-rail is display:flex;
   flex-direction:column, sitting outside #sidebar's own nav so it's a direct flex child of the
   rail, not competing with #sidebar's tab buttons for space) - out of the way of the actual
   navigation above it, but still always reachable.
   Kept compact/circular (unlike the full-width nav-item rows above it) - the same "icon inside a
   ring" look it had as a pill button in the original header design, so it still reads as a
   distinct control (a setting/preference) rather than another tab to click; align-self:flex-start
   overrides #sidebar-adjacent stretch behavior now that #nav-rail's own children default to
   filling the rail's width.
   NOTE the doubled-up #nav-rail #theme-toggle-btn selector (two IDs, not one) - #nav-rail
   .nav-item's own `border-left: none` above is an ID+class selector, which OUTRANKS a plain
   #theme-toggle-btn (a single ID) on that one property specifically - the border rendered on 3
   sides but not the left, which is exactly what a per-property (not per-rule) cascade loss looks
   like. Two IDs beats one ID+class outright. */
#nav-rail #theme-toggle-btn {
    width: 40px; height: 40px; align-self: flex-start; justify-content: center;
    margin-top: auto; border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 50%;
}
#nav-rail #theme-toggle-btn:hover { border-color: rgba(255, 255, 255, 0.4); }

/* ---------- Current user badge / logout ---------- */
.current-user-badge {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-xs);
    color: var(--text-secondary);
}
/* Author-origin display:flex above always beats the browser's built-in [hidden] default regardless
   of specificity - this explicit override is required, not decorative. Same pattern as .login-overlay. */
.current-user-badge[hidden] { display: none; }
#current-user-name { font-weight: 700; color: var(--text-primary); }

.role-pill {
    background: var(--card-bg-inner);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    padding: 3px 10px;
    font-size: var(--text-2xs);
    font-weight: 600;
    color: var(--text-primary);
}

/* Opens the My Account modal - just the avatar + name, kept visually part of the same
   .current-user-badge cluster as the role pill/Log Out next to it, not its own separate control. */
.my-account-trigger {
    display: flex; align-items: center; gap: 6px;
    background: none; border: none; padding: 0; cursor: pointer;
    font: inherit; color: var(--text-secondary);
}
.my-account-trigger:hover #current-user-name { text-decoration: underline; }
.header-avatar {
    width: 22px; height: 22px; border-radius: 50%; overflow: hidden; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    background: var(--card-bg-inner); border: 1px solid var(--border); color: var(--text-secondary);
}
.header-avatar img { width: 100%; height: 100%; object-fit: cover; }

/* max-height/overflow-y - this card grew past a typical viewport's height once the password and
   notification-preferences sections were added below the original profile form; .login-card
   itself has no scroll handling (fine for the shorter login/forgot-password cards that reuse it),
   so without this the bottom of the card - and, worse, the close button up top once you've
   scrolled the page under it - become unreachable. Scoped to .account-card specifically rather
   than widening .login-card's own rule, so the shorter modals that share that class are untouched. */
.account-card { width: 360px; text-align: left; max-height: 85vh; overflow-y: auto; }
.account-card h2 { margin: 4px 0 var(--space-4) 0; font-size: var(--text-md); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-heading); }
.account-card input:disabled { background: var(--card-bg-inner); color: var(--text-secondary); cursor: not-allowed; }
/* width/resize only - border/padding/background/color already come from the shared
   "select, input[...], textarea" rule earlier in this file. */
.account-card textarea { width: 100%; resize: vertical; margin-bottom: 2px; }
#account-bio-count { text-align: right; margin: 0 0 var(--space-3) 0; }
/* Divider between the profile form and the password-change form below it - see the Change
   Password section in index.html. */
.account-section-rule { border: none; border-top: 1px solid var(--border); margin: var(--space-4) 0; }
.notification-pref-row {
    display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs);
    color: var(--text-primary); padding: 6px 0; cursor: pointer;
}
/* .login-card's own bare `input` rule (width:100%; margin-bottom:...) is meant for the text
   fields elsewhere in this same card - with no [type] qualifier it also caught these checkboxes,
   stretching each one to the row's full width and pushing its label text down/off to the side.
   Checkboxes need their natural size back, not the text-field treatment. */
.notification-pref-row input[type="checkbox"] { width: auto; margin-bottom: 0; flex-shrink: 0; }

.account-avatar-row { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); }
.account-avatar {
    width: 64px; height: 64px; border-radius: 50%; overflow: hidden; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    background: var(--card-bg-inner); border: 1px solid var(--border); color: var(--text-secondary);
}
.account-avatar img { width: 100%; height: 100%; object-fit: cover; }
.account-avatar-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.account-avatar-actions label.btn-secondary { display: inline-block; cursor: pointer; padding: 8px 14px; }

.link-btn {
    background: none;
    border: none;
    color: var(--brand-red-bright);
    font-size: var(--text-xs);
    font-weight: 600;
    cursor: pointer;
    padding: 0;
}
.link-btn:hover { text-decoration: underline; }
.delete-btn { color: var(--status-red-ink); }

.login-btn {
    background: transparent;
    border: 1px solid var(--text-primary);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    padding: 8px 16px;
    font-size: var(--text-2xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
}
.login-btn:hover { border-color: var(--brand-red-deep); color: var(--brand-red-deep); }

/* ---------- Notifications (see Services/NotificationService.cs) ---------- */
.notifications-wrap { position: relative; }
.notifications-btn {
    background: transparent; border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
    color: var(--text-secondary); width: 34px; height: 34px;
    display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer;
    position: relative;
}
.notifications-btn:hover { border-color: var(--brand-red-bright); color: var(--brand-red-bright); }
.notifications-badge {
    position: absolute; top: -4px; right: -4px;
    background: var(--brand-red-action); color: #fff;
    border-radius: var(--radius-pill); min-width: 16px; height: 16px; padding: 0 4px;
    font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
    border: 2px solid var(--bg);
}
.notifications-panel {
    position: absolute; top: calc(100% + 8px); right: 0; z-index: 50;
    width: 340px; max-height: 420px; display: flex; flex-direction: column;
    background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
}
/* Author-origin display:flex above always beats the browser's built-in [hidden] default regardless
   of specificity - same required-not-decorative override as .current-user-badge/.lightbox-details. */
.notifications-panel[hidden] { display: none; }
.notifications-panel-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: var(--space-3) var(--space-3) var(--space-2); border-bottom: 1px solid var(--border);
}
.notifications-panel-header h3 { margin: 0; font-size: var(--text-sm); }
.notifications-panel-actions { display: flex; align-items: center; gap: var(--space-2); }
.notifications-panel-actions-sep { width: 1px; height: 12px; background: var(--border); }
.notifications-list { overflow-y: auto; padding: var(--space-2); }

/* ---------- Project switcher (rail) - Organization -> Project browser (see
   #project-switcher-wrap in index.html, app.js's "Header project switcher popover" section, name
   kept even though it lives in the rail now - Phase 2 moved it here so "who am I working for,
   which project" is visible on every tab, not just ones that happen to restate it - see the
   Phase 0 UX audit's own findings on both counts). Same position:relative anchor + absolutely-
   positioned panel toggled via [hidden] as the Notifications popover; the POPOVER itself
   (.project-switcher-panel below) is unchanged from before the move - still a plain light card
   regardless of the rail's own fixed-dark background, same as how it always looked. Only the
   TRIGGER button is restyled here, with fixed rail-appropriate colors (not theme tokens) for the
   same reason #nav-rail .nav-item uses them - see that rule's own comment. */
.project-switcher-wrap { position: relative; display: block; margin-bottom: var(--space-2); }
.project-switcher-btn {
    width: 100%; background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.14); padding: 9px 10px;
    border-radius: var(--radius-md); cursor: pointer; font: inherit;
    display: flex; align-items: center; justify-content: space-between; gap: 6px;
    text-align: left; transition: background-color 0.12s ease, border-color 0.12s ease;
}
.project-switcher-btn:hover { background: rgba(255, 255, 255, 0.09); border-color: rgba(255, 255, 255, 0.22); }
.project-switcher-btn:focus-visible { outline: 2px solid var(--brand-red-bright); outline-offset: 1px; }
.project-switcher-btn svg { color: rgba(255, 255, 255, 0.45); flex-shrink: 0; }
.project-switcher-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
#header-current-org {
    font-weight: 600; color: rgba(255, 255, 255, 0.5); font-size: var(--text-2xs);
    letter-spacing: 0.02em; text-transform: uppercase;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#header-current-project {
    font-weight: 700; color: #FFFFFF; font-size: var(--text-sm);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.project-switcher-panel {
    position: absolute; top: calc(100% + 6px); left: 0; z-index: 50;
    width: 320px; max-height: 440px; overflow-y: auto;
    background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg); padding: var(--space-3);
}
/* Author-origin display:block (the default for a bare <div>) above always beats the browser's
   built-in [hidden] default regardless of specificity - same required-not-decorative override as
   .notifications-panel[hidden] right above. */
.project-switcher-panel[hidden] { display: none; }

.switcher-section-label {
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-secondary); margin: var(--space-3) 0 var(--space-1);
}
.switcher-section-label:first-child { margin-top: 0; }

.switcher-org-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
#switcher-current-org-name { font-size: var(--text-md); font-weight: 700; color: var(--text-heading); }

.switcher-search { width: 100%; margin-bottom: var(--space-2); }

.switcher-project-list { display: flex; flex-direction: column; gap: 2px; }

.switcher-project-row {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2); border-radius: var(--radius-sm); cursor: pointer;
}
.switcher-project-row:hover, .switcher-project-row:focus-visible { background: var(--card-bg-inner); }
.switcher-project-row:focus-visible { outline: 2px solid var(--brand-red-bright); outline-offset: -2px; }
/* The currently-open project gets the same brand-tint wash as an unread notification
   (.notification-item.is-unread) - "this is the one" reads at a glance, same visual vocabulary. */
.switcher-project-row.is-current { background: var(--brand-red-tint); }
.switcher-project-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--border-strong); }
.switcher-project-row.is-current .switcher-project-dot { background: var(--brand-red-bright); }
.switcher-project-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.switcher-project-name {
    font-size: var(--text-sm); font-weight: 600; color: var(--text-primary);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.switcher-project-meta { font-size: var(--text-2xs); color: var(--text-secondary); }

.switcher-org-list-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
    padding: var(--space-2); border-radius: var(--radius-sm); cursor: pointer;
}
.switcher-org-list-row:hover, .switcher-org-list-row:focus-visible { background: var(--card-bg-inner); }
.switcher-org-list-row:focus-visible { outline: 2px solid var(--brand-red-bright); outline-offset: -2px; }
.switcher-org-list-row.is-current { background: var(--brand-red-tint); }
.switcher-org-list-name { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); }

/* Tighter padding than the base .empty-state (see just below the DASHBOARD section) - that one's
   sized for a full-width card, not a 320px popover. */
.switcher-empty-state { padding: var(--space-4) var(--space-2); }
.switcher-empty-state .empty-title { font-size: var(--text-sm); }
.switcher-empty-state .empty-body { font-size: var(--text-xs); }
/* A row (not a single <button> anymore - see renderNotifications in app.js) holding the open-and-
   navigate button plus its own delete button side by side - a <button> can't nest another
   <button>, so this became the click/hover/unread-tint target instead, with .notification-open
   inheriting that same look for the part that's actually clickable-to-open. */
.notification-item {
    display: flex; align-items: center; gap: 4px; border-radius: var(--radius-sm);
}
.notification-item:hover { background: var(--card-bg-inner); }
.notification-item.is-unread { background: var(--brand-red-tint); }
.notification-item.is-unread:hover { background: var(--brand-red-tint); filter: brightness(0.97); }
.notification-open {
    display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; text-align: left;
    background: none; border: none; padding: var(--space-2); cursor: pointer; font-family: inherit;
}
.notification-message { font-size: var(--text-xs); color: var(--text-primary); }
.notification-when { font-size: var(--text-2xs); color: var(--text-secondary); }
.notification-delete {
    flex-shrink: 0; width: 22px; height: 22px; margin-right: 4px; border-radius: 50%;
    background: none; border: none; color: var(--text-secondary); cursor: pointer;
    display: flex; align-items: center; justify-content: center; font-size: 11px;
}
.notification-delete:hover { background: var(--brand-red-tint); color: var(--brand-red-deep); }

/* ================================================================================================
   NAVIGATION ITEMS
   ================================================================================================ */
.nav-item {
    background: transparent;
    border: none;
    border-left: 3px solid transparent; /* reserved even when inactive, so .active doesn't shift layout */
    border-radius: var(--radius-sm);
    padding: 9px 14px;
    font-size: var(--text-md);
    font-weight: 500;
    color: var(--text-secondary); /* darkish grey when unselected - only the active tab goes bold/red */
    text-align: left;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.nav-item:hover { background: var(--card-bg-inner); }
.nav-item.active {
    background: var(--brand-red-tint);
    color: var(--brand-red-deep);
    font-weight: 700;
    border-left-color: var(--brand-red-action);
}
/* A thin vertical rule ahead of the first Admin-only nav item - "everything after this line is
   Admin-only", without needing a label that would just repeat what role-gating already enforces. */
.nav-admin-divider {
    width: 1px;
    align-self: stretch;
    background: var(--border-strong);
    margin: 4px 6px;
}

/* ================================================================================================
   LAYOUT / TABS
   ================================================================================================ */
/* flex:1 so #layout (and the footer after it) fills whatever height #shell-main's own min-height
   leaves, instead of the old fixed "100vh minus a guessed header height" - robust regardless of
   how tall the header/rail actually render. */
#layout { flex: 1; }

#content {
    max-width: 1280px;
    margin: 0 auto;
    padding: var(--space-6);
}

.tab-panel { display: none; }
.tab-panel.active { display: block; }

/* ---------- Cards ---------- */
.card {
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: var(--space-5);
    margin-bottom: var(--space-4);
}

/* Plain .card now - each stat inside is its own clickable <button> (.mini-stat-link below), not
   the whole card, so drilling into a specific stat (Overdue vs Awaiting Response, say) can set its
   own filter - see goToMyWork in app.js. */
.my-clashes-card {
    transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
/* display:block beats the native [hidden] UA rule with no override present - same recurring
   gotcha as .meeting-add-item-row/#create-meeting-form/.gallery-history-panel. Without this, the
   card never actually hides (starts hidden in index.html, then JS toggles it based on whether the
   signed-in user has anything assigned - see refreshMyClashesCard in app.js) - it was visually
   showing with its static placeholder content the whole time, CSS having silently overridden both
   the initial hidden attribute and every later hidden=true/false JS was setting. */
.my-clashes-card[hidden] { display: none; }
.mini-stat-row { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-2); }
.mini-stat { flex: 1 1 140px; }
/* A stat that's also a drill-down button (My Work card) - resets the UA button defaults, keeps
   the same visual weight as a plain (non-clickable) .mini-stat like Coordination Health uses, and
   only hints "clickable" on hover/focus so the row doesn't look like six buttons at rest. */
.mini-stat-link {
    display: block; width: 100%; text-align: left; font-family: inherit; cursor: pointer;
    background: none; border: none; border-radius: var(--radius-sm); padding: 4px;
    margin: -4px; transition: background-color 0.12s ease;
}
.mini-stat-link:hover, .mini-stat-link:focus-visible { background: var(--card-bg-inner); }
/* tabular-nums - same reasoning as .bar-count/.text-kpi elsewhere (Phase 9): six of these sit in
   a row, and a proportionally-spaced "1" is narrower than a "12" or "128", drifting the whole row
   left/right as any one value changes on refresh. */
.mini-stat-n { font-size: 26px; font-weight: 700; color: var(--text-primary); line-height: 1.15; font-variant-numeric: tabular-nums; }
.mini-stat-n.accent { color: var(--brand-red-bright); }
.mini-stat-l { font-size: var(--text-xs); color: var(--text-secondary); margin-top: 2px; }

/* Coordination Health's Good/Attention/At Risk pill - see updateCoordinationHealth in app.js /
   DashboardStateService.ComputeHealthStatus. Reuses the same amber-for-attention/red-for-at-risk
   language as the aging badge/overdue chip elsewhere, green only for the all-clear case. */
.health-pill {
    display: inline-block; border-radius: var(--radius-pill); padding: 3px 12px;
    font-size: var(--text-xs); font-weight: 700;
}
.health-pill.health-good { background: var(--status-green-bg); color: var(--status-green-ink); }
.health-pill.health-attention { background: var(--status-amber-bg); color: var(--status-amber-ink); }
.health-pill.health-atrisk { background: var(--status-red-bg); color: var(--status-red-ink); }
.health-why { font-size: var(--text-xs); color: var(--text-secondary); margin: var(--space-2) 0 0; }

.card h3 {
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary);
    margin: 0 0 var(--space-3) 0;
}

/* ================================================================================================
   PHASE 1 DESIGN SYSTEM ADDITIONS
   New, additive component/type vocabulary from the UI/UX modernization program's Phase 1. These
   are deliberately NOT applied to existing markup yet - Phase 0's audit found working components
   (.card h3, .section-eyebrow, .stat-card, .project-card, ...) that already render this exact
   visual language correctly; rewriting them here would risk the one thing this program explicitly
   rules out ("do not destroy the existing application"). Instead this establishes the CANONICAL
   version of each pattern the audit found duplicated, for Phase 2 onward to build new components
   against (and, opportunistically, to consolidate old ones into, once a page is already being
   touched for its own reasons - not as a separate sweep).
   ================================================================================================ */

/* Card variants - modifiers on the existing .card, same "base + --modifier" convention this file
   already uses for .badge-status--green etc., not a replacement for .card. The audit's own finding
   ("not every section needs a card... stop putting every component inside a white rectangle")
   is .panel below, not a card variant at all. */

/* A stat/KPI tile - same box as .card, just the larger radius .stat-card/.project-card already
   use today (kept as its own value here rather than promoted into --radius-lg's meaning, since a
   metric tile being SLIGHTLY more rounded than a content card is a deliberate, small distinction,
   not a universal rule). Static by default - pair with .card--interactive below if the tile is
   also a real drill-down, so "does this tile do something" has one consistent visual answer
   instead of the mix the Dashboard audit found (some KPI tiles are buttons, most aren't, none look
   different). */
.card--metric { border-radius: var(--radius-lg); }

/* Any card that IS a real click target. One hover/focus recipe used everywhere a card is
   interactive, instead of some interactive cards elevating on hover and others not. */
.card--interactive {
    cursor: pointer;
    transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.card--interactive:hover, .card--interactive:focus-visible {
    box-shadow: var(--shadow-md);
    border-color: var(--border-strong);
}
@media (prefers-reduced-motion: reduce) {
    .card--interactive { transition: none; }
}

/* Deliberately NOT a card - no border, no shadow, a quiet tinted surface for grouping content that
   doesn't need a card's visual weight (a de-emphasized or secondary section, not primary
   information/metrics/alerts - those still want .card or .card--metric). This is the direct answer
   to the audit's "9+ components each reimplement a bordered white box" finding: some of those
   sections should not have been boxes at all. */
.panel {
    background: var(--card-bg-inner);
    border-radius: var(--radius-md);
    padding: var(--space-4);
}
.panel--tight { padding: var(--space-3); }

/* Named type roles - reach for one of these by name instead of picking a raw --text-* size and
   re-deriving the weight/letter-spacing/color that goes with it. .text-eyebrow is the canonical
   version of the small-caps label style the audit found hand-copied 6+ times (.card h3 above,
   .section-eyebrow, and others) - those existing rules are left exactly as they render today; this
   is what a NEW component should use going forward. */
.text-eyebrow {
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
/* A short secondary line under a heading/value - same role .hint already plays, named for use
   outside form contexts too (.hint's own name reads as "form field help text" specifically). */
.text-caption {
    font-size: var(--text-xs);
    color: var(--text-secondary);
    line-height: 1.5;
}
/* A prominent number meant to be scanned/compared at a glance (KPI tiles, metric values) -
   tabular-nums so a column of these lines up digit-for-digit instead of proportionally-spaced
   numerals drifting side to side as their value changes on refresh. */
.text-kpi {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    color: var(--text-primary);
    line-height: 1.1;
}
/* The one tier above --text-2xl (22px) - a genuine "Display" role for the rare moment something
   should read as the single most important number/word on the page (a hero metric, not a page
   title - .page-title already owns that job at --text-2xl). Used sparingly by design. */
.text-display {
    font-size: var(--text-3xl);
    font-weight: 700;
    color: var(--text-heading);
    letter-spacing: -0.01em;
    line-height: 1.1;
}

.card-header-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    margin-bottom: var(--space-3);
    gap: var(--space-3);
}
.card-header-row h3 { margin: 0; }

/* A <button class="card-header-row collapsible-toggle"> - .card-header-row's own flex/spacing
   rules above already do the layout, this just strips default button chrome and adds the
   collapse behavior itself. See app.js for the click handler and Recent Activity in index.html
   for the one place this is used today. */
.collapsible-toggle {
    width: 100%;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    color: inherit;
    text-align: left;
}
.collapsible-toggle .chevron-icon {
    color: var(--text-secondary);
    flex-shrink: 0;
    transition: transform 0.15s ease;
}
.collapsible-toggle[aria-expanded="false"] .chevron-icon { transform: rotate(-90deg); }
.collapsible-toggle[aria-expanded="false"] + .scroll-box { display: none; }

.hint { font-size: var(--text-xs); color: var(--text-secondary); line-height: 1.5; }
.hint code { background: var(--card-bg-inner); padding: 1px 5px; border-radius: var(--radius-xs); font-size: var(--text-2xs); }

.field-label {
    display: block;
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--text-primary);
    margin: var(--space-3) 0 4px 0;
}

.badge {
    background: var(--brand-red-action);
    color: white;
    font-size: var(--text-2xs);
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    border-radius: var(--radius-sm);
    padding: 4px 9px;
}

/* Generalized status badge - subtle tinted background + matching ink, never a solid color block.
   .badge-status--<variant> covers every status word used across the app (Active/Open/Resolved/
   Pending/Overdue/Completed/Reviewed/Under Review/...) so a "resolved" chip looks the same on the
   Dashboard, a clash test card, and the gallery. */
.badge-status {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: 0.02em;
    border-radius: var(--radius-pill);
    padding: 3px 10px;
    white-space: nowrap;
}
.badge-status--green  { background: var(--status-green-bg); color: var(--status-green-ink); }
.badge-status--red     { background: var(--status-red-bg);   color: var(--status-red-ink); }
.badge-status--amber   { background: var(--status-amber-bg); color: var(--status-amber-ink); }
.badge-status--blue    { background: var(--status-blue-bg);  color: var(--status-blue-ink); }
.badge-status--gray    { background: var(--status-gray-bg);  color: var(--status-gray-ink); }
.badge-status--cyan    { background: var(--status-cyan-bg);  color: var(--status-cyan-ink); }
/* Added for the Timeline tab's "Upcoming" milestone status (see ComputeScheduleActivityStatus in
   DashboardStateService.cs) - every other status word already had a home in an existing badge
   color; this is the first one to need the purple token as a badge rather than just a status-pill. */
.badge-status--purple   { background: var(--status-purple-bg); color: var(--status-purple-ink); }

/* Audit Log row's right-hand cluster - action badge + resource + details, all inline and wrapping
   on narrow viewports. Reuses .user-row's own layout (the log list is one .user-row per entry). */
.audit-entry-action { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-left: auto; }
/* Column-header row above the audit log's own .user-row entries - same div-row shape (this app has
   no real <table> here), just re-styled to read as labels rather than a record: uppercase, quiet,
   no hover state, and immune to .user-row's own :last-child border removal since it's never actually
   last. See the equivalent .user-list-header just below for the Users list's version. */
.audit-log-header, .user-list-header {
    font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--text-secondary); border-bottom: 1px solid var(--border-strong);
}
.audit-log-header:hover, .user-list-header:hover { background: none; }
.audit-log-header .u-name, .user-list-header .u-name { font-weight: 700; }

.scroll-box {
    background: var(--card-bg-inner);
    border-radius: var(--radius-sm);
    padding: var(--space-2);
    max-height: 220px;
    overflow-y: auto;
}

/* ---------- Toast notifications (see showToast in app.js) ---------- */
#toast-container {
    position: fixed;
    top: var(--space-4);
    right: var(--space-4);
    z-index: 200; /* above everything, including the login/lightbox overlays' own stacking */
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    width: 340px;
    max-width: calc(100vw - var(--space-4) * 2);
    pointer-events: none; /* the empty container shouldn't block clicks; each .toast opts back in */
}
.toast {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    background: var(--card-bg);
    border: 1px solid var(--border-strong);
    border-left: 3px solid var(--status-blue-ink);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    padding: var(--space-3);
    pointer-events: auto;
    opacity: 0;
    transform: translateX(24px);
    transition: opacity 0.18s ease, transform 0.18s ease;
}
.toast.is-visible { opacity: 1; transform: translateX(0); }
.toast.is-leaving { opacity: 0; transform: translateX(24px); }
@media (prefers-reduced-motion: reduce) {
    .toast { transition: opacity 0.12s linear; transform: none; }
}
.toast-icon { flex-shrink: 0; margin-top: 1px; color: var(--status-blue-ink); }
.toast-message { flex: 1; font-size: var(--text-xs); font-weight: 600; color: var(--text-primary); line-height: 1.5; }
.toast-close {
    flex-shrink: 0; background: none; border: none; color: var(--text-secondary); cursor: pointer;
    font-size: 16px; line-height: 1; padding: 0 0 0 4px;
}
.toast-close:hover { color: var(--text-primary); }
.toast--success { border-left-color: var(--status-green-ink); }
.toast--success .toast-icon { color: var(--status-green-ink); }
.toast--error { border-left-color: var(--status-red-ink); }
.toast--error .toast-icon { color: var(--status-red-ink); }
.toast--warning { border-left-color: var(--status-amber-ink); }
.toast--warning .toast-icon { color: var(--status-amber-ink); }
.toast--info { border-left-color: var(--status-blue-ink); }
.toast--info .toast-icon { color: var(--status-blue-ink); }

/* ---------- Loading skeletons (see skeletonRowsHtml in app.js) ----------
   Used for the handful of data-dense, "this is the main thing on the page" lists/tables (Users,
   Audit Log, Trade Performance, Groups) where a beat of literal "Loading..." text read as the page
   doing nothing - a shimmering placeholder shaped like the real content shows the app is actually
   working AND roughly what's about to appear, without a spinner (there isn't one anywhere else in
   this app, and adding a second loading idiom on top of "Loading..." text would be one too many). */
.skeleton-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-1); }
.skeleton-block {
    height: 13px;
    border-radius: 4px;
    background: linear-gradient(90deg, var(--card-bg-inner) 25%, var(--track-bg) 37%, var(--card-bg-inner) 63%);
    background-size: 400% 100%;
    animation: skeleton-shimmer 1.4s ease infinite;
}
@keyframes skeleton-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) {
    .skeleton-block { animation: none; background: var(--card-bg-inner); }
}

/* ---------- Empty states ---------- */
.empty-state {
    text-align: center;
    padding: var(--space-8) var(--space-4);
    color: var(--text-secondary);
}
.empty-state .empty-title {
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 4px;
}
.empty-state .empty-body { font-size: var(--text-sm); max-width: 420px; margin: 0 auto; line-height: 1.6; }

/* ================================================================================================
   DASHBOARD
   ================================================================================================ */

/* ---------- Project Information (#project-info-card - see renderProjectInfoPanel in app.js) ----------
   Identity/business-metadata header for whichever project is current. Sits above the stat grid
   below since "what project is this" is the first thing this page should answer. */
.project-info-card { display: flex; flex-direction: column; gap: var(--space-4); }
/* align-items:center (not flex-start) - the name/address row is shorter than the icon box, and
   centering it vertically against the icon's height reads as "the name sits centered in the
   header band above the divider" rather than pinned to the top with dead space below it before
   the line, per direct user feedback on a live render. */
.project-info-header { display: flex; align-items: center; gap: var(--space-3); }

/* Square (not circular) version of the same "icon placeholder that becomes an image" idiom
   .header-avatar/.account-avatar already use elsewhere in this app - sized up since this is the
   page's own primary identity mark, not a small header accessory. Sits on the right of the header
   (see .project-info-heading's flex:1, which fills the rest of the row and pushes this to the
   edge) so the name/address - the important part - reads first. One of a small set of glyphs keyed
   off the project's own ProjectType, chosen client-side - see projectTypeIconSvg in app.js. */
.project-info-icon {
    width: 44px; height: 44px; border-radius: var(--radius-md); overflow: hidden; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    background: var(--card-bg-inner); border: 1px solid var(--border); color: var(--text-secondary);
}
.project-info-icon img { width: 100%; height: 100%; object-fit: cover; }
.project-info-icon svg { width: 20px; height: 20px; }

/* Thick, rounded decorative rule under the header row - see index.html's own comment on
   #project-info-divider for why. Sized/colored to actually register as a deliberate section
   break rather than a stray line - a plain 1px --border hairline disappeared next to the bold
   name above it. */
.project-info-divider { height: 2px; border-radius: var(--radius-pill); background: var(--border-strong); }

.project-info-heading { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* Name + address pin on one row, baseline-aligned so the smaller address text sits naturally
   against the larger heading instead of stacked in its own row below it - per direct user
   feedback on the live render. Wraps (rare - only a genuinely long name + long address together)
   rather than overflowing or forcing a horizontal scroll. */
.project-info-name-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-3); }
/* Strongest visual element on the panel by design (see redesign spec) - larger than .page-title
   itself, since this names the specific project, not just the page. */
#project-info-name {
    margin: 0; font-size: 26px; font-weight: 700; color: var(--text-heading); line-height: 1.2;
    text-wrap: balance;
}
/* Secondary to the name - smaller, muted, and only visually "clickable" via the brand-red hover,
   not competing with the name for attention. */
.project-info-address {
    display: inline-flex; align-items: center; gap: 5px; width: fit-content;
    font-size: var(--text-sm); color: var(--text-secondary); text-decoration: none;
}
/* Found during a live-render check: this class sets display:inline-flex unconditionally, which -
   same bug shape as .my-clashes-card elsewhere in this file - beats the native [hidden] UA rule
   (equal specificity, author style wins), so a project with no address on file (Location empty)
   still showed the bare pin icon floating under the name with nothing next to it, instead of not
   rendering at all like renderProjectInfoPanel's own hidden=true intended. */
.project-info-address[hidden] { display: none; }
.project-info-address:hover { color: var(--brand-red-bright); text-decoration: underline; }
.project-info-address svg { flex-shrink: 0; color: var(--brand-red-bright); }

/* Project.Description - the project's own free-text scope blurb. Only surfaces here on the
   Dashboard now that "Your Projects" (which used to show it per-card) is hidden - see that
   section's own comment in index.html. Quiet/secondary, same register as .health-why, not
   competing with the name above it. */
.project-info-description { font-size: var(--text-sm); color: var(--text-secondary); margin: 0; max-width: 80ch; line-height: 1.5; }

.project-info-body { display: flex; flex-direction: column; gap: var(--space-4); }
.section-eyebrow {
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-secondary); margin-bottom: var(--space-2);
}

/* Fixed column counts (5 / 3 / 1, stepped at the breakpoints below) rather than auto-fill - on
   purpose. Auto-fill let the column count itself change with viewport width (7 columns at full
   screen vs. 5 at half), which is exactly why the fields ran together at wide widths (more,
   narrower columns) per direct user feedback on a live render. A fixed count also lets the
   dividers below use plain nth-child math - auto-fill's column count isn't knowable in CSS, so
   a divider rule would either miss real column boundaries or draw a stray one at every wrapped
   row's leading edge. */
.project-data-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: var(--space-4) 0;
}
.project-data-field {
    padding: 0 0 0 var(--space-4);
    border-left: 1px solid var(--border-strong);
}
/* First column of each row sits flush against the card edge - no divider with nothing to its
   left. Class-based (set by insertProjectDataRowDividers in app.js), not :nth-child - the row
   divider below is a real sibling element inserted INTO this same grid, which shifts every
   field's child index by one for each divider before it. :nth-child math looked right until a
   divider actually existed, then the "first in row" field it targeted quietly drifted by one
   position (found on a live render - the border showed up on the wrong field). JS already has to
   compute the real row boundaries to place the dividers, so it sets this class at the same time
   instead of CSS re-deriving (and mis-deriving) the same thing. */
.project-data-field.project-data-row-start { border-left: none; padding-left: 0; }
/* Row divider - one real element spanning every active column (grid-column:1/-1 always means
   "full width", whatever the column count is at the current breakpoint), inserted between rows
   by insertProjectDataRowDividers in app.js rather than styled per-field. A per-field
   border-top (an earlier approach) needed 5 separate 1px segments to meet edge-to-edge with
   zero gap - with fractional 1fr column widths that didn't always land on a whole pixel, which
   is exactly the visible gap reported on a live render. One spanning element can't have that
   problem at any column count. */
.project-data-row-divider { grid-column: 1 / -1; height: 1px; background: var(--border-strong); }
@media (max-width: 860px) {
    .project-data-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 560px) {
    .project-data-grid { grid-template-columns: 1fr; }
}
.project-data-label { font-size: var(--text-2xs); color: var(--text-secondary); margin-bottom: 2px; }
.project-data-value { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); }
/* "Not specified" - a subtle, deliberately unstyled-as-data placeholder (see redesign spec's own
   example) rather than hiding the field entirely, so the grid's shape stays predictable as fields
   get filled in over a project's life instead of visibly reflowing each time. */
.project-data-value.is-unset { font-weight: 400; color: var(--text-secondary); font-style: italic; }

/* ---------- Coordination Health hero (Phase 3) ----------
   .panel (see the Phase 1 design-system tokens) - deliberately NOT a card. This is the first real
   content on the Dashboard and the single most important read on the page, so it gets its own
   quiet, wide, un-boxed treatment instead of competing with a dozen identically-bordered cards
   below it (see the Phase 0 UX audit's "12 white cards" finding) - open layout, a subtle tinted
   surface, not another white rectangle. */
.health-hero { padding: var(--space-5); }
.health-hero-top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: 4px; }
.health-why { font-size: var(--text-sm); color: var(--text-secondary); margin: 0 0 var(--space-4); max-width: 68ch; }

/* Five metrics as one connected strip - "an information system, not five disconnected cards" (the
   redesign brief's own words). Dividers between them (not individual boxes) are what makes this
   read as one object instead of a row of mini stat-cards. */
.health-metric-row { display: flex; align-items: stretch; gap: 0; flex-wrap: wrap; }
.health-metric {
    flex: 1 1 140px; display: flex; flex-direction: column; gap: 2px;
    background: none; border: none; border-left: 1px solid var(--border);
    padding: 2px var(--space-4); text-align: left; cursor: pointer; font: inherit;
    border-radius: 0;
}
.health-metric:first-child { border-left: none; padding-left: 0; }
.health-metric:hover, .health-metric:focus-visible { background: var(--card-bg); border-radius: var(--radius-sm); }
.health-metric-static { cursor: default; }
.health-metric-static:hover { background: none; }
.health-metric-n { font-size: 26px; line-height: 1.1; }
.health-metric-n.accent { color: var(--brand-red-bright); }
.health-metric-l { font-size: var(--text-xs); color: var(--text-secondary); }

/* Needs Attention reuses the exact same metric-row/metric component as Coordination Health above -
   one interaction pattern for "a clickable count that drills into Clash Coordination," not two to
   learn. Folded into .health-hero as a second section (see .health-subsection-divider below)
   rather than its own bordered .panel - direct user feedback on the live Phase 3 render was that
   Coordination Health / Needs Attention / Coordination Progress as three separate stacked boxes
   read as disconnected, even though they're one story. */
#needs-attention-card .section-eyebrow { margin-bottom: var(--space-3); }

/* Hairline that separates the hero's sections (Coordination Health / Needs Attention /
   Coordination Progress) from each other WITHOUT breaking them back into separate boxes - a
   border-top on the next section-eyebrow would work too, but a dedicated element keeps the
   spacing symmetric above and below the line regardless of which sections are present (Needs
   Attention is hidden most of the time - see updateNeedsAttention in app.js). */
.health-subsection-divider { border-top: 1px solid var(--border); margin: var(--space-5) 0; }

/* ---------- Your Projects (project cards) ---------- */
.project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--space-3);
}
/* The whole card is the click target now (see switchToProject in app.js) - role="button" +
   tabindex="0" in the markup, cursor+hover+focus states here so it reads as clickable rather than
   just decorative. */
/* Flat in light mode, same reasoning as .stat-card above - hover/current state still shows via
   border-color/shadow, just no gradient tint. */
.project-card {
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    cursor: pointer;
    transition: box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
/* Hovering (or keyboard-focusing) previews the same red glass wash the currently-open card
   already wears permanently below in dark mode - "this is the one you'd switch to" reads at a
   glance there. Light mode's version (below) is flat, so this just shows via border/shadow. */
.project-card:hover, .project-card:focus-visible {
    box-shadow: var(--shadow-md);
    border-color: var(--brand-red-bright);
    background: var(--card-bg);
}
.project-card:focus-visible { outline: 2px solid var(--brand-red-bright); outline-offset: 2px; }
/* The currently-open project gets a red border to say "this one" - light mode's flat version
   stops there; dark mode (below) also washes it in the same red glass as the stat cards. */
.project-card.is-current { border-color: var(--brand-red-bright); background: var(--card-bg); }
.project-card .project-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2); }
.project-card .project-card-name { font-size: var(--text-lg); font-weight: 700; color: var(--text-heading); }
.project-card .project-card-stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-2);
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    padding: var(--space-2) 0;
}
.project-card .project-card-stat { text-align: left; }
.project-card .project-card-stat .stat-num { font-size: var(--text-lg); font-weight: 700; color: var(--text-primary); }
.project-card .project-card-stat .stat-lbl { font-size: var(--text-2xs); color: var(--text-secondary); }
.project-card .project-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }

/* ---------- DARK MODE CARD GRADIENTS ----------
   .project-card is flat above (light mode) - dark mode gets its gradient back exactly as
   originally built (45%/55% stops), since that version wasn't the complaint. Component-level
   override rather than just swapping a token, same reasoning as --heatmap-bg/.trade-view-report-
   frame elsewhere: light and dark genuinely want a different TREATMENT here (flat vs. glass), not
   just different colors of the same treatment. High specificity (:root[...] + a class) beats the
   plain .project-card rule above regardless of source order, in both the media-query (OS default)
   and [data-theme="dark"] (explicit toggle) cases - see the DARK MODE token section near the top
   of this file for the same two-block pattern. (.stat-card's own dark-mode gradient was removed
   here in Phase 3 along with its base rule - see the Coordination Health hero above, which
   replaced it.) */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .project-card { background: linear-gradient(160deg, var(--card-bg-inner) 0%, var(--card-bg) 45%); }
    :root:not([data-theme="light"]) .project-card:hover,
    :root:not([data-theme="light"]) .project-card:focus-visible { background: linear-gradient(160deg, var(--card-glass-tint) 0%, var(--card-bg) 45%); }
    :root:not([data-theme="light"]) .project-card.is-current { background: linear-gradient(160deg, var(--card-glass-tint) 0%, var(--card-bg) 55%); }
    /* Phase 12 (Dark Mode Refinement) - .btn-danger:hover's light-mode background (#FCA5A5) is
       the EXACT same hex --status-red-ink resolves to in dark mode (see the dark token block near
       the top of this file) - hovering a danger button in dark mode made its own text vanish into
       an identically-colored background. --brand-red-secondary is a fixed-across-themes dark red
       (see its own comment) - bright pink ink on it stays clearly legible instead. */
    :root:not([data-theme="light"]) .btn-danger:hover { background: var(--brand-red-secondary); }
    /* .banner's border-bottom (#FCD34D) is a fixed light-mode amber that never tracked
       --status-amber-ink's own dark-mode value - left alone in light mode (untouched, still
       #FCD34D) since that combination was already fine; only dark mode gets its own value here. */
    :root:not([data-theme="light"]) .banner { border-bottom-color: var(--status-amber-ink); }
}
:root[data-theme="dark"] .project-card { background: linear-gradient(160deg, var(--card-bg-inner) 0%, var(--card-bg) 45%); }
:root[data-theme="dark"] .project-card:hover,
:root[data-theme="dark"] .project-card:focus-visible { background: linear-gradient(160deg, var(--card-glass-tint) 0%, var(--card-bg) 45%); }
:root[data-theme="dark"] .project-card.is-current { background: linear-gradient(160deg, var(--card-glass-tint) 0%, var(--card-bg) 55%); }
:root[data-theme="dark"] .btn-danger:hover { background: var(--brand-red-secondary); }
:root[data-theme="dark"] .banner { border-bottom-color: var(--status-amber-ink); }

/* ---------- Heatmap ---------- */
.heatmap-panel {
    position: relative;
    background: var(--heatmap-bg);
    border-radius: var(--radius-sm);
    height: 220px;
    overflow: hidden;
}
.heat-point { position: absolute; border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; }
.heat-point.Low { background: radial-gradient(circle, #4B162066 0%, #4B162000 100%); }
.heat-point.Medium { background: radial-gradient(circle, #9A2530AA 0%, #9A253000 100%); }
.heat-point.High { background: radial-gradient(circle, #E2364ADD 0%, #E2364A00 100%); }
.heat-point.Critical { background: radial-gradient(circle, #FF8A80FF 0%, #FF6B6B00 100%); }

/* ---------- Open Clashes by Trade (bar chart) ---------- */
.bar-chart { display: flex; align-items: flex-end; gap: 6px; height: 140px; overflow-x: auto; padding-top: 4px; }
/* .bar-col is a real <button> now (see renderTradeBreakdown, app.js - each bar drills into Clash
   Coordination filtered to that trade), so this resets the native button chrome (border/background/
   font/padding) the layout below doesn't want, and adds a hover/focus cue so it reads as clickable. */
.bar-col {
    display: flex; flex-direction: column; align-items: center; width: 40px; flex-shrink: 0;
    height: 100%; justify-content: flex-end;
    background: none; border: none; padding: 0; font: inherit; cursor: pointer; border-radius: var(--radius-sm);
}
.bar-col:hover, .bar-col:focus-visible { background: var(--card-bg-inner); }
/* tabular-nums so counts across the row of bars line up digit-for-digit rather than drifting
   side to side as a wider/narrower value changes on refresh - same reasoning as .text-kpi (see
   its own comment), applied directly here since this label is smaller/lighter-weight than a true
   KPI tile's number. Phase 9 of the UI/UX modernization program. */
.bar-count { font-size: var(--text-2xs); font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.bar-fill { width: 100%; background: var(--brand-red-bright); border-radius: var(--radius-xs) var(--radius-xs) 0 0; margin-top: 2px; }
.bar-label { font-size: 8px; color: var(--text-secondary); margin-top: 2px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; }

/* ---------- Coordination Progress (Phase 3) - Open -> Assigned -> In Progress -> Resolved ->
   Verified as one connected pipeline (see renderStatusProgress, app.js), not five independent
   donut rings with no visual relationship to each other. ---------- */
.progress-pipeline { padding-top: var(--space-2); }
.pipeline-bar {
    display: flex; width: 100%; height: 28px; border-radius: var(--radius-sm); overflow: hidden;
    background: var(--track-bg); gap: 2px; /* 2px surface gap between segments, not a hard border */
}
.pipeline-seg { border: none; padding: 0; cursor: pointer; min-width: 4px; transition: filter 0.12s ease; }
.pipeline-seg:hover, .pipeline-seg:focus-visible { filter: brightness(1.12); }
.pipeline-legend { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-3); }
.pipeline-legend-item {
    display: flex; align-items: center; gap: 6px; background: none; border: none; padding: 4px 6px;
    margin: -4px -6px; border-radius: var(--radius-sm); cursor: pointer; font: inherit;
}
.pipeline-legend-item:hover, .pipeline-legend-item:focus-visible { background: var(--card-bg-inner); }
.pipeline-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.pipeline-legend-label { font-size: var(--text-xs); color: var(--text-secondary); }
.pipeline-legend-n { font-size: var(--text-sm); color: var(--text-primary); }
.pipeline-side-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--border); }
.pipeline-side-chip {
    display: inline-flex; align-items: center; gap: 5px; background: var(--card-bg-inner);
    border: none; border-radius: var(--radius-pill); padding: 4px 10px 4px 8px;
    font-size: var(--text-xs); color: var(--text-secondary); cursor: pointer; font: inherit;
}
.pipeline-side-chip:hover, .pipeline-side-chip:focus-visible { background: var(--border); color: var(--text-primary); }

/* ---------- Recent Activity (also used by Live Tracking) ---------- */
.alert-item, .feed-item {
    background: var(--card-bg-inner);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-1);
}
/* Phase 3: the narrated sentence (.alert-details/.feed-details, e.g. "Jane Doe moved MEP-042 to
   Resolved.") is the row's real content and comes first in the markup now - .event-type used to be
   a raw C# enum value (ClashReviewStatusChanged) styled as this row's boldest, most prominent text,
   sitting ABOVE the sentence that already said the same thing in plain English (see the Phase 0 UX
   audit's own finding, and humanEventType in app.js, which now also gives it a real label). It's a
   small secondary tag below the sentence now, not competing with it. */
.alert-details, .feed-details { font-size: var(--text-xs); color: var(--text-primary); }
.alert-meta, .feed-meta { display: flex; align-items: baseline; gap: var(--space-2); color: var(--text-secondary); margin-top: 3px; }
.alert-meta .event-type, .feed-meta .event-type { font-size: var(--text-2xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.03em; }
.alert-meta .event-when, .feed-meta .event-when { font-size: var(--text-2xs); }
.event-actor-name { color: var(--text-primary); }
/* An actual vertical rule, not just a character - a middle-dot read as too subtle against the
   bold name/pill next to it. Empty element sized purely by CSS (see the empty span in
   renderAlerts, app.js). */
.event-actor-sep {
    display: inline-block;
    width: 1px;
    height: 13px;
    background: var(--border-strong);
    vertical-align: -2px;
    margin: 0 7px;
}
/* Brand-red tint pill, same colored-chip idea as .status-chip.review/.test-status-chip - a plain
   grey pill here read as too quiet next to the bold name. --brand-red-tint is already theme-aware
   (a pale pink fill in light mode, a deep muted red in dark - see its :root/dark-mode definitions
   above) so this doesn't need its own light/dark overrides. See renderAlerts in app.js for when
   this actually renders (only events with a real "who did this", like a file upload or report
   import). */
/* Shape only - color comes from one of the .company-pill-N classes below (see companyColorClass
   in app.js), so the same company always renders the same color everywhere this appears. */
.event-actor-company {
    display: inline-block;
    border-radius: var(--radius-pill);
    padding: 1px 9px;
    font-size: var(--text-2xs);
    font-weight: 700;
    vertical-align: middle;
}
.company-pill-0 { background: var(--company-pill-0-bg); color: var(--company-pill-0-ink); }
.company-pill-1 { background: var(--company-pill-1-bg); color: var(--company-pill-1-ink); }
.company-pill-2 { background: var(--company-pill-2-bg); color: var(--company-pill-2-ink); }
.company-pill-3 { background: var(--company-pill-3-bg); color: var(--company-pill-3-ink); }
.company-pill-4 { background: var(--company-pill-4-bg); color: var(--company-pill-4-ink); }
.company-pill-5 { background: var(--company-pill-5-bg); color: var(--company-pill-5-ink); }
.company-pill-6 { background: var(--company-pill-6-bg); color: var(--company-pill-6-ink); }
.company-pill-7 { background: var(--company-pill-7-bg); color: var(--company-pill-7-ink); }
/* StudioJAED always gets the actual brand color, not a hashed one - see companyColorClass in
   app.js. Reuses the same brand tokens as everywhere else (already theme-aware). */
.company-pill-brand { background: var(--brand-red-tint); color: var(--brand-red-bright); }
/* Phase 3: a real row with real badges and a real click target now (was plain bulleted text - see
   the Phase 0 UX audit's own "unstyled plain text in an otherwise badge-driven UI" finding) -
   clicking jumps straight into that test in Clash Coordination (see renderClashTests, app.js). */
.test-item {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    width: 100%; background: none; border: none; border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-2); margin: 0; font: inherit; text-align: left; cursor: pointer;
}
.test-item:hover, .test-item:focus-visible { background: var(--card-bg-inner); }
.test-item-name { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.test-item-badges { display: flex; gap: 6px; flex-shrink: 0; }

/* ================================================================================================
   FORMS / BUTTONS
   ================================================================================================ */
.explainer {
    background: var(--card-bg-inner);
    border-left: 3px solid var(--brand-red-bright);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    padding: var(--space-3) var(--space-4);
    font-size: var(--text-xs);
    color: var(--text-primary);
    margin-bottom: var(--space-4);
    line-height: 1.6;
}

select, input[type="text"], input[type="number"], input[type="email"], input[type="password"], input[type="file"], input[type="search"], textarea {
    font-size: var(--text-sm);
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--card-bg);
    color: var(--text-primary);
    font-family: inherit;
}
/* box-shadow, not outline - a plain CSS outline on <select> is unreliable across browsers
   (Chromium/Edge on Windows can paint their own native/OS-themed blue focus ring for <select>'s
   part-native chrome regardless of an author outline, the same class of thing that made the
   Navisworks plugin's ComboBox resist plain property styling). box-shadow is a paint effect the
   browser always honors, so it reliably replaces the ring instead of layering under/over it. */
select:focus, input:focus, textarea:focus {
    outline: none;
    box-shadow: 0 0 0 2px var(--brand-red-tint);
    border-color: var(--brand-red-bright);
}
select { width: 100%; }

/* Replaces the browser's own default gray "Choose File" button (which ignores the theme entirely,
   light or dark) with one that matches the rest of the app - ::file-selector-button is the
   standard way to restyle just that inner button while the native "no file chosen"/filename text
   next to it keeps working exactly as before (no JS needed to keep it in sync). */
input[type="file"] { padding: 6px 10px; cursor: pointer; }
input[type="file"]::file-selector-button {
    border: none;
    border-radius: var(--radius-sm);
    padding: 8px 16px;
    margin-right: var(--space-3);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    font-family: inherit;
    cursor: pointer;
    background: var(--card-bg-inner);
    color: var(--text-primary);
    transition: background-color 0.12s ease;
}
input[type="file"]::file-selector-button:hover { background: var(--track-bg); }

.radius-row { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 4px; }
.radius-row input { width: 80px; }

.btn-primary, .btn-secondary, .btn-ghost, .btn-danger {
    border: none;
    border-radius: var(--radius-sm);
    padding: 10px 18px;
    font-weight: 600;
    font-size: var(--text-xs);
    letter-spacing: 0.03em;
    cursor: pointer;
    margin-top: var(--space-3);
    transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.btn-primary { background: var(--brand-red-action); color: white; }
.btn-primary:hover { background: var(--brand-red-deep); }
/* Phase 12 (Dark Mode Refinement) - was a fixed light grey (#C1C4CB) with no color override, so
   this inherited .btn-primary's white text: white-on-light-grey is already barely legible in
   light mode, and in dark mode that same fixed grey sits jarringly bright against the dark
   surface around it, never having been designed for it. Token-based instead of a second
   dark-mode override block - --border-strong/--text-secondary already carry appropriate light/
   dark values, so this is correct in both themes for free. */
.btn-primary:disabled { background: var(--border-strong); color: var(--text-secondary); cursor: not-allowed; }

.btn-secondary { background: var(--card-bg-inner); color: var(--text-primary); border: 1px solid var(--border-strong); }
.btn-secondary:hover { background: var(--track-bg); }
/* "Select" mode toggle on the clash grid (see toggleGallerySelectMode in app.js) - reuses the
   .filter-pill.active look so it reads as "currently on", not just a hover state. */
#gallery-select-mode-btn.active { background: var(--brand-red-action); border-color: var(--brand-red-action); color: #FFFFFF; }

.btn-ghost { background: transparent; color: var(--brand-red-bright); padding: 6px 8px; }
.btn-ghost:hover { text-decoration: underline; }

.btn-danger { background: var(--status-red-bg); color: var(--status-red-ink); }
.btn-danger:hover { background: #FCA5A5; }

.log-line { font-size: var(--text-2xs); padding: 2px 0; color: var(--text-secondary); }
.log-line.success { color: var(--status-green-ink); }

.group-item { background: var(--card-bg-inner); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); margin-bottom: var(--space-1); }
.group-name { font-size: var(--text-xs); font-weight: 700; color: var(--brand-red-bright); }
.group-summary { font-size: var(--text-2xs); color: var(--text-secondary); margin-top: 2px; }

/* ---------- Live Tracking ---------- */
.status-strip { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--status-green-ink); }

/* ---------- Assignments (Trade Views - legacy, kept functional) ---------- */
.rule-item { display: flex; align-items: center; gap: var(--space-2); padding: 5px 0; font-size: var(--text-xs); }
.rule-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.rule-keywords { color: var(--text-secondary); font-size: var(--text-2xs); }

.trade-view-form { display: flex; align-items: center; gap: var(--space-2); margin: var(--space-2) 0; }
.trade-view-form select { width: auto; flex: 1 1 auto; }
.trade-view-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-3) 0; }
.trade-view-chip {
    display: flex; align-items: center; gap: 6px;
    background: var(--card-bg-inner); border: 1px solid var(--border); border-radius: var(--radius-pill);
    padding: 6px 12px; font-size: var(--text-xs); font-weight: 600; color: var(--brand-red-bright); cursor: pointer;
}
.trade-view-chip.active { background: var(--brand-red-tint); border-color: var(--brand-red-bright); }
.trade-view-chip .chip-remove { color: var(--text-secondary); font-weight: 400; }
.trade-view-chip .chip-remove:hover { color: var(--status-red-ink); }
/* Fixed white, not themed - this is the desktop plugin's own self-contained HTML report, styled
   for a printed/light page like a document, not this app's UI (same reasoning as the invite email
   preview staying a fixed light "document" regardless of theme). */
.trade-view-report-frame { width: 100%; height: 520px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #FFFFFF; }

/* ================================================================================================
   CLASH COORDINATION (clash tests + galleries)
   ================================================================================================ */
/* Shared by both the report-import form and its Grid-only sibling (#gallery-import-grid-form) -
   two selectors per rule rather than a shared class, matching how this rule was originally scoped
   to just the one form's id. */
#gallery-import-form, #gallery-import-grid-form { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-2); }
#gallery-import-form input[type="file"], #gallery-import-grid-form input[type="file"] { flex: 1 1 260px; font-size: var(--text-xs); }
/* .btn-primary's margin-top:12px (see its shared rule) is meant for a button stacked below a
   vertical form's fields - it just pushes this one down out of line with the file input beside it
   in this horizontal row. */
#gallery-import-form .btn-primary, #gallery-import-grid-form .btn-primary { margin-top: 0; }
#gallery-import-result, #gallery-import-grid-result { margin-top: var(--space-3); font-size: var(--text-xs); }
.import-ok { color: var(--status-green-ink); }
.import-warn { color: var(--status-amber-ink); }
.import-diff { color: var(--text-secondary); font-size: var(--text-xs); margin: 2px 0; }

/* "Viewing: Mechanical clashes only" - see updateTradeRestrictionBanner in app.js. Informational,
   not a warning, so it borrows the blue status pairing rather than amber/red. */
.trade-restriction-banner {
    display: flex; align-items: center; gap: var(--space-2);
    background: var(--status-blue-bg); color: var(--status-blue-ink);
    border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
    font-size: var(--text-xs); font-weight: 600; margin-bottom: var(--space-4);
}
.trade-restriction-banner[hidden] { display: none; }

/* Search + filter bar shared by the Clash Tests list */
.list-toolbar { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-3); }
.list-toolbar input[type="search"] { flex: 1 1 220px; }
.filter-pills { display: flex; gap: 4px; background: var(--card-bg-inner); border-radius: var(--radius-pill); padding: 3px; }
.filter-pill {
    border: none; background: transparent; padding: 6px 14px; border-radius: var(--radius-pill);
    font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); cursor: pointer;
}
.filter-pill.active { background: var(--card-bg); color: var(--brand-red-deep); box-shadow: var(--shadow-sm); }
/* Phase 14 (Micro-UX/Accessibility) finding: this had no explicit focus state at all - a keyboard
   user Tabbing through All/Open/Resolved/Assigned to Me got only the browser's own bare default
   outline (inconsistent with literally every other interactive element in this app, which all
   define their own branded focus-visible treatment) rather than nothing being visible - so this
   was a consistency gap, not a broken/invisible one, but worth closing now that it's found. */
.filter-pill:focus-visible { box-shadow: 0 0 0 2px var(--brand-red-tint); outline: 2px solid var(--brand-red-bright); outline-offset: 1px; }

/* Clash test cards - richer than a chip: name, total/open/resolved, explicit "Open Test" action.
   Phase 5 of the UI/UX modernization program: this had been hand-copying .card--interactive's own
   hover recipe (box-shadow:var(--shadow-md); border-color:var(--border-strong);) verbatim since
   before that canonical component existed (see Phase 1's own "opportunistically consolidate once
   a page is already being touched for its own reasons" note) - now uses it directly (see the
   .card--interactive class added alongside .gallery-test-item in renderGalleryTestList) instead of
   redefining the same hover/focus recipe a second time. Purely a consolidation - the resulting
   CSS, and every pixel of it, is unchanged. */
#gallery-test-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-3); }
.gallery-test-item {
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: var(--space-4);
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.gallery-test-item.active { border-color: var(--brand-red-bright); }
.gallery-test-item .test-card-name { font-size: var(--text-lg); font-weight: 700; color: var(--text-heading); }
/* Small red pill next to a test's name for 48 hours after it's imported (or re-imported) - see
   the isNew calc in renderGalleryTestList. Deliberately not an animated/pulsing badge - it needs
   to read as "recently added", not demand attention every time the list re-renders. */
.new-badge {
    display: inline-block; vertical-align: middle; margin-left: 6px;
    background: var(--brand-red-action); color: #FFFFFF;
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
    padding: 2px 8px; border-radius: var(--radius-pill); line-height: 1.4;
}
.gallery-test-item .test-card-clashes { font-size: var(--text-md); font-weight: 700; color: var(--text-primary); }
/* Breakdown of a test's clashes by review stage - same four colors as the grid card's pill/border
   and the lightbox's status boxes (see reviewStatusClass in app.js). Bare class, not scoped to
   .gallery-test-item - shared by the clash grid's banner above the grid itself (see
   .clash-grid-banner / #gallery-clash-grid-statuses), which is where it actually lives now. */
.test-card-statuses { display: flex; gap: 6px; flex-wrap: wrap; }
.test-status-chip {
    display: inline-flex; align-items: center; gap: 5px;
    background: var(--card-bg-inner); border-radius: var(--radius-pill); padding: 3px 9px;
    font-size: var(--text-2xs); font-weight: 600; color: var(--text-secondary);
}
.test-status-chip .chip-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.test-status-chip.status-open .chip-dot { background: var(--status-gray-ink); }
.test-status-chip.status-assigned .chip-dot { background: var(--status-blue-ink); }
.test-status-chip.status-in-progress .chip-dot { background: var(--status-orange-ink); }
.test-status-chip.status-resolved .chip-dot { background: var(--status-green-ink); }
.test-status-chip.status-verified .chip-dot { background: var(--status-cyan-ink); }
.test-status-chip.status-reopened .chip-dot { background: var(--status-red-ink); }
.test-status-chip.status-deferred .chip-dot { background: var(--status-purple-ink); }
.test-status-chip.status-closed .chip-dot { background: var(--status-slate-ink); }
.test-status-chip.status-overdue { background: var(--status-red-bg); color: var(--status-red-ink); }
.test-status-chip.status-overdue .chip-dot { background: var(--status-red-ink); }
/* Aging - amber, deliberately distinct from Overdue's red so the two read as different signals
   (Overdue = missed a date someone set; Aging = nobody's touched this in a while, due date or
   not) - see DashboardStateService.BuildAgingInfo. */
.test-status-chip.status-aging { background: var(--status-amber-bg); color: var(--status-amber-ink); }
.test-status-chip.status-aging .chip-dot { background: var(--status-amber-ink); }
.gallery-test-item .test-card-uploaded { font-size: var(--text-2xs); color: var(--text-secondary); margin-top: -4px; }
.gallery-test-item .test-card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 2px; }
.gallery-test-item .open-test-link { color: var(--brand-red-bright); font-size: var(--text-xs); font-weight: 700; }
/* Bottom-right of the card, via the footer's own space-between rather than absolute positioning -
   only rendered at all when this test has a downloadable source report (see t.hasSourceReport /
   ClashTestGallery.SourceReportStoredFileName), so a test imported before this feature existed
   just doesn't show one instead of linking to a 404. */
.test-card-download-btn {
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border-strong);
    color: var(--text-secondary); background: var(--card-bg-inner);
    transition: background 0.15s ease, color 0.15s ease;
}
.test-card-download-btn:hover { background: var(--brand-red-tint); color: var(--brand-red-deep); }
.gallery-test-item .chip-remove {
    color: var(--text-secondary); font-weight: 400; border: none; background: none; cursor: pointer; font-size: var(--text-sm); padding: 2px 4px;
}
.gallery-test-item .chip-remove:hover { color: var(--status-red-ink); }

/* Banner above the grid - the open test's name + its status breakdown (reuses the exact same
   .test-status-chip look the test cards use above, just here instead of duplicated on both). */
.clash-grid-banner {
    display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
    gap: var(--space-3); padding-bottom: var(--space-3); margin-bottom: var(--space-3);
    border-bottom: 1px solid var(--border);
}
.clash-grid-heading { display: flex; flex-direction: column; gap: 1px; }
/* Project > Test - see renderGalleryClashGrid in app.js. Quiet on purpose (same weight as a
   caption, not competing with the test name's own h3 styling right below it). */
.clash-grid-breadcrumb { font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); }
.clash-grid-heading h3 { margin: 0; }
/* .btn-secondary's own rules never set `display` (buttons don't need it) - an <a> defaults to
   inline, which drops the padding/sizing a real button gets. Same fix already needed for the My
   Account modal's "Change Photo" label. margin-top:0 overrides the shared .btn-primary/.btn-
   secondary/etc. rule's `margin-top: var(--space-3)` (meant for a button sitting below a form/
   paragraph) - here it's a flex item in a horizontal row, so that top margin just pushed it below
   its siblings instead of centering with them. */
/* Groups Select / History / Generate Report as one cluster - see the comment in index.html on
   .clash-grid-actions for why this wrapper exists (keeps the buttons tight against each other
   regardless of how .clash-grid-banner's own space-between spreads its top-level children).
   Every button in the row gets the same override, not just the <a> ones - the shared .btn-primary/
   .btn-secondary rule's margin-top:var(--space-3) is meant for a button sitting below a form/
   paragraph; left in place here it pushes whichever button has it down out of vertical center with
   its siblings even though .clash-grid-actions already centers align-items. */
.clash-grid-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
#gallery-select-mode-btn, #gallery-history-btn, #gallery-report-btn, #gallery-csv-export-btn {
    display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; text-decoration: none; margin-top: 0;
}
/* ID selector so this cleanly beats the generic small-uppercase-eyebrow .card h3 treatment every
   other card's <h3> uses - this one's meant to read as an actual title, not a section label. */
#gallery-clash-grid-title {
    font-size: var(--text-xl); font-weight: 700; letter-spacing: normal; text-transform: none;
    color: var(--text-heading); margin: 0;
}

/* Clash-level filter bar - status/priority/trade/assignee/due date, all combining with AND (see
   clashFilters/clashMatchesFilters in app.js). Plain native selects (not the app's custom combo
   box) since this is a compact toolbar row, not a form. */
.clash-filter-bar {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
    margin-bottom: var(--space-3);
}
.clash-filter-bar[hidden] { display: none; }
.clash-filter-select {
    width: auto; flex: 0 0 auto;
    background: var(--card-bg-inner); color: var(--text-primary); border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm); padding: 6px 26px 6px 10px; font-size: var(--text-xs); font-family: inherit;
    appearance: none; -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 8px center;
}
.clash-filter-clear {
    border: none; background: none; color: var(--brand-red-bright); font-size: var(--text-xs); font-weight: 700;
    cursor: pointer; padding: 6px 4px;
}
.clash-filter-clear:hover { color: var(--brand-red-deep); }
.clash-filter-count { font-size: var(--text-2xs); color: var(--text-secondary); margin-left: auto; }
/* Same box as .clash-filter-select but for a free-text filter (Trade Performance's Level field) -
   .clash-filter-select's own chevron background-image is select-only and would look like a
   (nonfunctional) dropdown arrow on a text input. */
.clash-filter-input {
    width: 160px; flex: 0 0 auto;
    background: var(--card-bg-inner); color: var(--text-primary); border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm); padding: 6px 10px; font-size: var(--text-xs); font-family: inherit;
}

/* Trade Performance's per-trade rollup table - the app's first real HTML <table> (everything else
   is div-based), so this establishes the pattern rather than reusing one. */
.trade-perf-table { width: 100%; border-collapse: collapse; font-size: var(--text-xs); }
.trade-perf-table th {
    text-align: left; font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--text-secondary); padding: 6px 10px; border-bottom: 1px solid var(--border);
}
.trade-perf-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); color: var(--text-primary); }
.trade-perf-table td.num { font-variant-numeric: tabular-nums; text-align: right; }
.trade-perf-table td.num.is-overdue { color: var(--status-red-ink); font-weight: 700; }
.trade-perf-table tbody tr:last-child td { border-bottom: none; }
/* Groups panel reuses .trade-perf-table's look - .group-row is the only addition, since (unlike a
   trade performance row) clicking one is a real action (filters the grid to that group). */
.group-row { cursor: pointer; }
.group-row:hover, .group-row:focus-visible { background: var(--card-bg-inner); outline: none; }

/* Bulk trade-tagging toolbar - shown only in "Select" mode (see toggleGallerySelectMode in
   app.js). display:flex here needs the explicit [hidden] override, same recurring gotcha as
   .clash-filter-bar/.gallery-history-panel above. */
.bulk-trade-bar {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
    background: var(--brand-red-tint); border: 1px solid var(--brand-red-bright);
    border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); margin-bottom: var(--space-3);
}
.bulk-trade-bar[hidden] { display: none; }
/* "Done" is a plain .link-btn (no border/padding, unlike its bordered Apply/Select All/Clear
   siblings) - align-self makes sure it centers on its own box rather than any baseline drift, and
   margin-left pushes it to sit apart from Clear rather than crowding it under the shared gap. */
#gallery-bulk-done-btn { align-self: center; margin-left: var(--space-2); }
.bulk-trade-count { font-size: var(--text-xs); font-weight: 700; color: var(--brand-red-deep); white-space: nowrap; }
.bulk-trade-bar .lightbox-trade-chips { flex: 1 1 auto; }
/* Reuses .trade-chip's shape but on a light/tinted surface instead of the lightbox's dark one. */
.bulk-trade-bar .trade-chip {
    border-color: var(--border-strong); background: var(--card-bg); color: var(--text-secondary);
}
.bulk-trade-bar .trade-chip:hover:not(:disabled) { border-color: var(--brand-red-bright); color: var(--text-primary); }
.bulk-trade-bar .trade-chip.active { background: var(--brand-red-action); border-color: var(--brand-red-action); color: #FFFFFF; }

/* Selection checkbox overlay on a clash card, top-left of the thumbnail - only rendered at all in
   Select mode (see renderGalleryClashGrid's selectModeOn branch). Sits above the image but below
   the existing hover actions (thumb-actions), same stacking approach as statusPill. */
.clash-select-checkbox {
    position: absolute; top: 8px; left: 8px; z-index: 3;
    width: 22px; height: 22px; border-radius: var(--radius-sm);
    border: 2px solid #FFFFFF; background: rgba(23,24,28,0.55);
    display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.clash-select-checkbox.checked { background: var(--brand-red-action); border-color: var(--brand-red-action); }
.clash-select-checkbox svg { opacity: 0; width: 14px; height: 14px; stroke: #FFFFFF; }
.clash-select-checkbox.checked svg { opacity: 1; }
.gallery-clash-card.select-mode { cursor: pointer; }
.gallery-clash-card.is-selected { outline: 2px solid var(--brand-red-action); outline-offset: -2px; }

/* Coordination trend panel - see renderImportHistory in app.js. display:flex here beats the
   native [hidden] UA rule with no override present, same recurring gotcha as
   .meeting-add-item-row/#create-meeting-form above - the explicit [hidden] override is required,
   not decorative. */
.gallery-history-panel {
    display: flex; flex-direction: column; gap: 6px;
    background: var(--card-bg-inner); border-radius: var(--radius-md); padding: var(--space-3);
    margin-bottom: var(--space-3);
}
.gallery-history-panel[hidden] { display: none; }
.history-header { display: flex; align-items: center; justify-content: space-between; font-size: var(--text-xs); font-weight: 700; color: var(--text-primary); padding-bottom: 4px; border-bottom: 1px solid var(--border); margin-bottom: 2px; }
/* One-line "are we actually getting better" summary above the per-cycle rows - see
   renderImportHistory's oldest-vs-newest tracked-OpenCount comparison. Green when trending down
   (fewer open clashes = progress), red when trending up; no color at all when holding steady. */
.history-trend { font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); padding: 2px 0 6px; }
.history-trend.is-good { color: var(--status-green-ink); }
.history-trend.is-bad { color: var(--status-red-ink); }
.history-row { display: flex; align-items: center; gap: var(--space-3); font-size: var(--text-xs); padding: 4px 0; flex-wrap: wrap; }
.history-cycle { flex: 0 0 56px; font-weight: 700; color: var(--text-primary); }
.history-when { flex: 0 0 100px; color: var(--text-secondary); }
/* Burn-down bar - width relative to the biggest open/total count on record (see
   renderImportHistory's maxValue), so the trend reads at a glance without needing to compare
   numbers row to row. */
.history-bar-track { flex: 1 1 80px; min-width: 60px; max-width: 160px; height: 8px; background: var(--track-bg); border-radius: var(--radius-pill); overflow: hidden; }
.history-bar-fill { height: 100%; background: var(--brand-red-bright); border-radius: var(--radius-pill); }
.history-total { flex: 0 0 110px; font-weight: 700; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.history-tags { display: flex; gap: 6px; flex-wrap: wrap; }
.history-tag { display: inline-block; padding: 1px 8px; border-radius: var(--radius-pill); font-size: var(--text-2xs); font-weight: 700; }
.history-tag.new { background: var(--status-blue-bg); color: var(--status-blue-ink); }
.history-tag.gone { background: var(--status-green-bg); color: var(--status-green-ink); }
.history-tag.reopened { background: var(--status-red-bg); color: var(--status-red-ink); }

.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-4); margin-top: var(--space-2); }
.gallery-clash-card {
    background: var(--card-bg);
    border: 1px solid var(--border);
    /* Status reads as a flag along the top edge, not the whole card outlined in color - a full
       colored border on every card in the grid competed with the photo for attention; one edge is
       still an immediate, scannable signal (a coordinator can read the row of top-bar colors
       without looking at any single pill) without turning the grid into a wall of mixed color.
       Overridden per status below; a card with no class yet (shouldn't normally happen) gets a
       plain neutral top edge, same as the other three sides. */
    border-top: 3px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    cursor: pointer;
    text-align: left;
    transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.gallery-clash-card.status-open { border-top-color: var(--status-gray-ink); }
.gallery-clash-card.status-assigned { border-top-color: var(--status-blue-ink); }
.gallery-clash-card.status-in-progress { border-top-color: var(--status-orange-ink); }
.gallery-clash-card.status-resolved { border-top-color: var(--status-green-ink); }
.gallery-clash-card.status-verified { border-top-color: var(--status-cyan-ink); }
.gallery-clash-card.status-reopened { border-top-color: var(--status-red-ink); }
.gallery-clash-card.status-deferred { border-top-color: var(--status-purple-ink); }
.gallery-clash-card.status-closed { border-top-color: var(--status-slate-ink); }
/* Lifts the whole card off the page on hover (same "this is the one" language as
   .project-card's hover/is-current treatment) - translateY + --shadow-lg reads as the card
   physically rising, not just a border tweak. */
.gallery-clash-card:hover { box-shadow: var(--shadow-lg); transform: translateY(-6px); }
.gallery-clash-card .thumb-wrap {
    position: relative; width: 100%; aspect-ratio: 4 / 3; background: var(--track-bg);
    display: flex; align-items: center; justify-content: center; overflow: hidden;
    /* Ring sits inset so it doesn't get clipped by the card's own overflow:hidden. */
    box-shadow: inset 0 0 0 0 var(--brand-red-bright);
    transition: box-shadow 0.15s ease;
}
.gallery-clash-card:hover .thumb-wrap { box-shadow: inset 0 0 0 3px var(--brand-red-bright); }
/* No more per-priority border stripe (see .gallery-clash-card.status-* above for the status
   color, now a top-edge accent instead of a full outline) - priority stays exactly where it
   already reads clearly, the badge next to the label, rather than adding a second competing
   color signal to the card frame itself. */
.gallery-clash-card img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.2s ease; }
.gallery-clash-card:hover img { transform: scale(1.08); }
@media (prefers-reduced-motion: reduce) {
    .gallery-clash-card, .gallery-clash-card img, .gallery-clash-card .thumb-wrap { transition: none; }
    .gallery-clash-card:hover { transform: none; }
    .gallery-clash-card:hover img { transform: none; }
}
.gallery-clash-card .no-image { font-size: var(--text-2xs); color: var(--text-secondary); }

/* Rounded pill floating over the top-left of the thumbnail (was a full-width banner before - a
   pill reads lighter and leaves more of the image itself visible). */
.clash-status-pill {
    position: absolute; top: 10px; left: 10px; border: none; border-radius: var(--radius-pill);
    padding: 5px 12px; font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.02em;
    white-space: nowrap; cursor: pointer; color: #FFFFFF; background: var(--status-gray-ink);
    box-shadow: 0 2px 6px rgba(16,17,20,0.3);
}
.status-assigned .clash-status-pill { background: var(--status-blue-ink); }
.status-in-progress .clash-status-pill { background: var(--status-orange-ink); }
.status-resolved .clash-status-pill { background: var(--status-green-ink); }
/* No longer needs its own dark-text override - --status-cyan-ink is now a solid, saturated teal
   (see its :root definition above), so white (the .clash-status-pill base's own color) reads fine
   on it, same as every other status here. */
.status-verified .clash-status-pill { background: var(--status-cyan-ink); }
.status-reopened .clash-status-pill { background: var(--status-red-ink); }
.status-deferred .clash-status-pill { background: var(--status-purple-ink); }
.status-closed .clash-status-pill { background: var(--status-slate-ink); }
.clash-status-pill:disabled { cursor: default; opacity: 0.95; }

/* Top-right icon cluster over the thumbnail - Open (always) + Delete (admin only). */
.thumb-actions { position: absolute; top: 10px; right: 10px; display: flex; gap: 6px; }
.thumb-icon-btn {
    width: 28px; height: 28px; border-radius: 50%; border: none;
    background: rgba(16,17,20,0.55); color: #FFFFFF; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 2px 6px rgba(16,17,20,0.3);
}
.thumb-icon-btn:hover { background: rgba(16,17,20,0.8); }
.thumb-icon-btn.delete-icon-btn:hover { background: var(--status-red-ink); }

/* Full-image "click to open" hint - fades in on hover/focus so the thumbnail reads as
   interactive at a glance, not just via cursor:pointer (which isn't visible until you're already
   moving the mouse over it). Sits under .thumb-actions (higher z via source order) so the
   Open/Delete icon buttons stay clickable through it. */
.thumb-hover-hint {
    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 6px;
    background: rgba(16,17,20,0.38); color: #FFFFFF; font-size: var(--text-xs); font-weight: 700;
    opacity: 0; transition: opacity 0.15s ease; pointer-events: none;
}
.gallery-clash-card:hover .thumb-hover-hint, .gallery-clash-card:focus-visible .thumb-hover-hint { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .thumb-hover-hint { transition: none; } }

.clash-card-body { padding: var(--space-3); display: flex; flex-direction: column; gap: 2px; }
.clash-label-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.clash-card-body .clash-label { font-size: var(--text-sm); font-weight: 700; color: var(--text-heading); }
.clash-card-body .clash-sub { font-size: var(--text-2xs); color: var(--text-secondary); }
.clash-level { font-weight: 700; color: var(--text-secondary); }
/* Divider between Level/Grid/Room on the card sub-line (see renderGalleryClashGrid in app.js) -
   previously just a plain space, which read as one run-on value ("Level 4 Grid C.9(2)-3") with no
   visible boundary between them. Quiet/un-bold on purpose, unlike its .clash-level neighbors, so it
   reads as punctuation rather than a fourth value. */
.clash-sub-sep { font-weight: 400; color: var(--border-strong); }
.clash-card-footer {
    display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-start; gap: var(--space-2) 6px;
    margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--border);
}
.clash-meta-item { display: inline-flex; align-items: center; gap: 4px; font-size: var(--text-2xs); color: var(--text-secondary); }
.clash-meta-item svg { flex-shrink: 0; opacity: 0.8; }

/* Priority badge - card + lightbox both use this (see priorityBadgeHtml in app.js). Distinct
   palette from the status language so the two are never visually confused. */
.priority-badge {
    display: inline-block; border-radius: var(--radius-pill); padding: 1px 8px;
    font-size: var(--text-2xs); font-weight: 700; white-space: nowrap; flex-shrink: 0;
}
.priority-badge.priority-low { background: var(--priority-low-bg); color: var(--priority-low-ink); }
.priority-badge.priority-medium { background: var(--priority-medium-bg); color: var(--priority-medium-ink); }
.priority-badge.priority-high { background: var(--priority-high-bg); color: var(--priority-high-ink); }
.priority-badge.priority-critical { background: var(--priority-critical-bg); color: var(--priority-critical-ink); }

.trade-badge {
    display: inline-block; border-radius: var(--radius-pill); padding: 1px 8px;
    font-size: var(--text-2xs); font-weight: 600; background: var(--card-bg-inner); color: var(--text-secondary);
}

/* Due-date chip - plain grey until overdue (see dueDateBadgeHtml in app.js, driven by
   GalleryClashDto.IsOverdue, computed server-side from today's date so it can't drift). */
.due-badge {
    display: inline-block; border-radius: var(--radius-pill); padding: 1px 8px;
    font-size: var(--text-2xs); font-weight: 700; background: var(--card-bg-inner); color: var(--text-secondary);
}
.due-badge.is-overdue { background: var(--status-red-bg); color: var(--status-red-ink); }

/* Aging badge - see agingBadgeHtml in app.js / DashboardStateService.BuildAgingInfo. Amber, same
   "distinct from Overdue's red" reasoning as .test-status-chip.status-aging above. */
.aging-badge {
    display: inline-block; border-radius: var(--radius-pill); padding: 1px 8px;
    font-size: var(--text-2xs); font-weight: 700; background: var(--status-amber-bg); color: var(--status-amber-ink);
}
/* Individual ownership, separate from the trade/company grouping the Assistant tab does - see
   .event-actor-company for the same "small pill" shape used elsewhere for a person/company tag. */
/* Two looks depending on WHO it's assigned to (see renderGalleryClashGrid in app.js, which picks
   the modifier class by comparing assignedToUserId against the signed-in user's own id) - red and
   bold when it's assigned to you (so your own queue jumps out while scanning a grid of clashes),
   muted grey when it's assigned to someone else (still visible, but reads as "not yours, FYI"
   rather than competing with your own red ones for attention). */
.clash-assignee-badge {
    display: inline-block; border-radius: var(--radius-pill); padding: 1px 8px;
    font-size: var(--text-2xs); font-weight: 700;
}
.clash-assignee-badge.is-me { background: var(--brand-red-tint); color: var(--brand-red-bright); }
.clash-assignee-badge.is-other { background: var(--card-bg-inner); color: var(--text-secondary); font-weight: 600; }

/* ================================================================================================
   CLASH LIGHTBOX (image viewer) - a "theater" surface that stays dark regardless of the app's own
   light/dark theme (see the tokens below), so an exported Navisworks screenshot always reads
   against black rather than whatever the surrounding page happens to be set to. Everything in
   here draws from --lb-* rather than the global --card-bg/--text-primary/etc. tokens - do not mix
   the two, or a light-mode page would leave light text unreadable against this dark surface.
   ================================================================================================ */
.lightbox-card {
    --lb-bg: #17181C;
    --lb-surface: #212227;
    --lb-surface-hover: #2C2E36;
    --lb-border: #33353D;
    --lb-text: #F5F6F8;
    --lb-text-secondary: #9BA0AC;

    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--lb-bg);
    color: var(--lb-text);
    border-radius: var(--radius-lg);
    /* Starting size - just a starting point, not a hard cap. resize:both gives a native drag
       handle in the bottom-right corner so the whole viewer can be dragged bigger, up to almost
       the full viewport. overflow:hidden (rather than auto) keeps the header/footer pinned in
       place; only .lightbox-images scrolls internally if a clash has several stacked images. */
    width: min(1000px, 94vw);
    height: min(760px, 90vh);
    min-width: 460px;
    min-height: 380px;
    max-width: 98vw;
    max-height: 98vh;
    resize: both;
    overflow: hidden;
    box-shadow: var(--shadow-lg);
}
.lightbox-card:fullscreen { width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh; border-radius: 0; }

.lightbox-header {
    display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3);
    padding: 18px 18px 14px; border-bottom: 1px solid var(--lb-border); flex-shrink: 0;
}
.lightbox-header-text h2 { margin: 0; font-size: var(--text-lg); font-weight: 700; color: var(--lb-text); }
.lightbox-header-meta { display: flex; align-items: center; gap: var(--space-3); margin-top: 4px; flex-wrap: wrap; }
#clash-lightbox-sub { font-size: var(--text-xs); color: var(--lb-text-secondary); }

/* Read-only - just names the current stage. Same colors as the grid card's pill/border (see
   .gallery-clash-card.status-* above); the controls grid below the image is the actual control. */
.lightbox-status-badge {
    flex-shrink: 0; border-radius: var(--radius-pill);
    padding: 4px 12px; font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.02em;
    color: #FFFFFF; background: var(--status-gray-ink);
}
.lightbox-status-badge.status-assigned { background: var(--status-blue-ink); }
.lightbox-status-badge.status-in-progress { background: var(--status-orange-ink); }
.lightbox-status-badge.status-resolved { background: var(--status-green-ink); }
/* Same removed dark-text override as .status-verified .clash-status-pill above - see its comment. */
.lightbox-status-badge.status-verified { background: var(--status-cyan-ink); }
.lightbox-status-badge.status-reopened { background: var(--status-red-ink); }
.lightbox-status-badge.status-deferred { background: var(--status-purple-ink); }
.lightbox-status-badge.status-closed { background: var(--status-slate-ink); }

/* Priority/trade/level/due-date row under the header - see renderLightboxContent in app.js.
   Reuses .priority-badge/.trade-badge/.due-badge as-is (same look as the grid card), plus a plain
   neutral pill for Level. */
.lightbox-meta-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.lightbox-meta-badges:empty { display: none; }
.lightbox-meta-badge {
    display: inline-block; border-radius: var(--radius-pill); padding: 1px 8px;
    font-size: var(--text-2xs); font-weight: 700; background: var(--lb-surface); color: var(--lb-text-secondary);
    border: 1px solid var(--lb-border);
}

/* Who/what/when/how-urgent, one panel of five fields over the top-right of the image stage.
   Plain <select>/<input>, not the app's own custom-templated combo box - the lightbox is a
   deliberately separate dark "theater" surface (see this section's own header comment), so it
   gets its own minimal control styling instead. */
.lightbox-controls-grid {
    position: absolute; top: 14px; right: 14px; z-index: 2;
    display: flex; flex-direction: column; gap: 8px; width: 196px;
    background: rgba(23,24,28,0.75); backdrop-filter: blur(6px);
    border: 1px solid var(--lb-border); border-radius: var(--radius-md); padding: 10px;
}
.lightbox-control { display: flex; flex-direction: column; gap: 4px; }
.lightbox-control-label { font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--lb-text-secondary); }
.lightbox-control-select {
    width: 100%; background: var(--lb-surface); color: var(--lb-text); border: 1px solid var(--lb-border);
    border-radius: var(--radius-sm); padding: 7px 30px 7px 10px; font-size: var(--text-xs); font-family: inherit;
}
select.lightbox-control-select {
    /* The native dropdown-arrow button is its own little sub-control that ignores the select's own
       background/color entirely, so it stayed a bright OS-default square against this dark surface
       (see screenshot) - appearance:none removes it, and this SVG chevron (stroke="currentColor",
       so it always matches the select's own `color: var(--lb-text)` above) replaces it. */
    appearance: none; -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 9px center; background-size: 14px;
}
input.lightbox-control-select { padding: 6px 10px; color-scheme: dark; }
.lightbox-control-select:disabled { opacity: 0.6; cursor: not-allowed; }
/* The global `select:focus`/`input:focus` glow uses --brand-red-tint, which flips between a pale
   pink (light theme) and a dark maroon (dark theme) - fine for inputs that live on a themed
   surface, but these live on the lightbox's always-dark "theater" surface regardless of site
   theme, so in light mode that pale pink tint rendered as a washed-out halo against the dark
   backdrop. A fixed semi-transparent red reads correctly here no matter the site's theme. */
.lightbox-control-select:focus { box-shadow: 0 0 0 2px rgba(226, 54, 74, 0.45); border-color: var(--brand-red-bright); }
/* The dropdown's own popup list is otherwise unstyled browser-native (white background, black
   text) - it always looked jarring against the dark lightbox behind it. Chromium/Edge honor
   background/color set directly on <option>; the browser still owns the hover/keyboard-selection
   highlight color, but this alone removes the stark white flash. */
.lightbox-control-select option { background: var(--lb-surface); color: var(--lb-text); }
/* The status field reads as a small header within the panel, not just another row - it's the one
   field that determines the whole clash's lifecycle stage. */
.lightbox-control-status { padding-top: 6px; border-top: 1px solid var(--lb-border); }

/* Trade(s) chip group - see renderTradeChips in app.js. A toggle-button row instead of a select,
   since more than one can be active at once (a clash is often between two disciplines). Sized/
   colored to sit on the same dark "theater" surface as .lightbox-control-select above. */
.lightbox-trade-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.trade-chip {
    border: 1px solid var(--lb-border); background: var(--lb-surface); color: var(--lb-text-secondary);
    border-radius: var(--radius-pill); padding: 3px 9px; font-size: var(--text-2xs); font-weight: 600;
    cursor: pointer; transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.trade-chip:hover:not(:disabled) { border-color: var(--brand-red-bright); color: var(--lb-text); }
.trade-chip.active { background: var(--brand-red-action); border-color: var(--brand-red-action); color: #FFFFFF; }
.trade-chip:disabled { opacity: 0.6; cursor: not-allowed; }

/* Shared circular icon button - the header's Close and the footer's Previous/Next all use this. */
.lightbox-icon-btn {
    flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: none;
    background: var(--lb-surface); color: var(--lb-text); cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: background 0.15s ease;
}
.lightbox-icon-btn:hover:not(:disabled) { background: var(--lb-surface-hover); }
.lightbox-icon-btn:disabled { opacity: 0.3; cursor: default; }

/* flex:1 + min-height:0 so this area grows/shrinks to fill whatever room .lightbox-card's resize
   handle leaves it (min-height:0 is the part that's easy to forget - without it a flex child won't
   shrink below its content's natural size, which defeats resizing the card smaller). */
.lightbox-stage-wrap { position: relative; flex: 1; min-height: 0; display: flex; }
.lightbox-images { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: var(--space-3); padding: 16px 18px; overflow-y: auto; }
.lightbox-images .hint { color: var(--lb-text-secondary); }

/* Floating tool panel over the top-left of the stage - Fullscreen, Zoom In/Out, Reset View. These
   are the only real, working controls: Pan is drag-to-move once an image is zoomed in (see
   setupPanZoom), already available directly on the image with no button needed. There's
   deliberately no Orbit or orientation cube here - these are flat screenshots exported from the
   Navisworks report, not a live 3D scene, so there's nothing to actually orbit. */
.lightbox-toolbar {
    position: absolute; top: 14px; left: 14px; z-index: 2;
    display: flex; flex-direction: column; gap: 4px;
    background: rgba(23,24,28,0.75); backdrop-filter: blur(6px);
    border: 1px solid var(--lb-border); border-radius: var(--radius-md); padding: 5px;
}
/* Author-origin display:flex above always beats the browser's built-in [hidden] default regardless
   of specificity - same fix as .lightbox-details below (hidden for a clash with no images to zoom). */
.lightbox-toolbar[hidden] { display: none; }
.lightbox-tool-btn {
    width: 32px; height: 32px; border-radius: var(--radius-sm); border: none;
    background: transparent; color: var(--lb-text); cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: background 0.15s ease;
}
.lightbox-tool-btn:hover { background: var(--lb-surface-hover); }
.lightbox-tool-btn.is-active { background: var(--brand-red-action); color: #FFFFFF; }

/* .lightbox-controls-grid above (who/what/priority/due/status) replaces what used to be a
   separate top-right status-boxes panel - now one panel, see its own comment above. */

/* Each image's own clipped viewport - the img itself is what gets scaled/translated inside it (see
   setupPanZoom in app.js), so zooming in actually has somewhere to pan INTO instead of just
   visually overflowing its own untransformed box. flex:1 (with a min-height floor so several
   stacked images don't get squashed to nothing) means dragging the lightbox bigger makes the image
   itself bigger, not just the whitespace around it. touch-action:none stops the browser's own
   scroll/zoom gestures from fighting with the custom touch pan handling. */
.lightbox-image-stage {
    position: relative;
    flex: 1;
    min-height: 240px;
    background: #0B0C0E;
    border-radius: var(--radius-sm);
    border: 1px solid var(--lb-border);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    touch-action: none;
}
.lightbox-image-stage img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    cursor: zoom-in;
    transform-origin: center center;
    transition: transform 0.1s ease-out;
    user-select: none;
    -webkit-user-drag: none;
}
.lightbox-image-stage.zoomed img { cursor: grab; }
.lightbox-image-stage.zoomed.dragging img { cursor: grabbing; transition: none; }

/* Collapsed by default (see the "View Details" button in the footer) - Status/Distance/Grid
   Location/Item A/Item B/Description, i.e. everything that used to be crammed into one meta line
   under the title. */
.lightbox-details {
    flex-shrink: 0; border-top: 1px solid var(--lb-border);
    padding: 14px 18px; display: flex; flex-direction: column; gap: 8px;
    max-height: 30%; overflow-y: auto;
}
/* Author-origin display:flex above always beats the browser's built-in [hidden] default regardless
   of specificity - this explicit override is required, not decorative. Same pattern as
   .login-overlay/.current-user-badge. Without this, the panel was visible from the start (View
   Details just relabeled the button without actually hiding/showing anything). */
.lightbox-details[hidden] { display: none; }
.lightbox-details .hint { color: var(--lb-text-secondary); margin: 0; }
.lightbox-details-row { display: flex; gap: var(--space-3); font-size: var(--text-xs); }
.lightbox-details-key { flex: 0 0 110px; color: var(--lb-text-secondary); font-weight: 600; }
.lightbox-details-val { color: var(--lb-text); word-break: break-word; }

/* Same toggle-panel shape/pattern as .lightbox-details above (including the [hidden] override -
   see that rule's own comment for why it's required, not decorative, whenever display:flex is
   set here too). A discussion thread, not a fixed key/value list, so its own list/form styling
   rather than reusing .lightbox-details-row. */
.lightbox-comments {
    flex-shrink: 0; border-top: 1px solid var(--lb-border);
    padding: 14px 18px; display: flex; flex-direction: column; gap: 10px;
    max-height: 34%; overflow-y: auto;
}
.lightbox-comments[hidden] { display: none; }
.lightbox-comments-list { display: flex; flex-direction: column; gap: 10px; }
.lightbox-comments .hint { color: var(--lb-text-secondary); margin: 0; }
.clash-comment { background: var(--lb-surface); border-radius: var(--radius-sm); padding: 8px 10px; }
.clash-comment-meta { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.clash-comment-meta .event-actor-name { color: var(--lb-text); font-size: var(--text-xs); }
.clash-comment-when { font-size: var(--text-2xs); color: var(--lb-text-secondary); margin-left: auto; }
.clash-comment-delete {
    background: none; border: none; color: var(--lb-text-secondary); cursor: pointer;
    font-size: var(--text-2xs); padding: 0 0 0 4px; line-height: 1;
}
.clash-comment-delete:hover { color: var(--status-red-ink); }
.clash-comment-text { font-size: var(--text-xs); color: var(--lb-text); white-space: pre-wrap; word-break: break-word; }
.lightbox-comment-form { display: flex; gap: 8px; align-items: flex-end; }
.lightbox-comment-form textarea {
    flex: 1; resize: vertical; min-height: 38px; max-height: 100px;
    background: var(--lb-surface); color: var(--lb-text); border: 1px solid var(--lb-border);
    border-radius: var(--radius-sm); padding: 8px 10px; font-size: var(--text-xs); font-family: inherit;
}
.lightbox-comment-form textarea::placeholder { color: var(--lb-text-secondary); }

/* Same toggle-panel shape as .lightbox-comments above - a read-only coordination trail (see
   ClashActivityEvent server-side), so no form at the bottom, just the list. */
.lightbox-history {
    flex-shrink: 0; border-top: 1px solid var(--lb-border);
    padding: 14px 18px; display: flex; flex-direction: column; gap: 10px;
    max-height: 34%; overflow-y: auto;
}
.lightbox-history[hidden] { display: none; }
.lightbox-history-list { display: flex; flex-direction: column; gap: 8px; }
.lightbox-history .hint { color: var(--lb-text-secondary); margin: 0; }
.clash-history-item {
    display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3);
    font-size: var(--text-xs); color: var(--lb-text); padding-bottom: 8px; border-bottom: 1px solid var(--lb-border);
}
.clash-history-item:last-child { border-bottom: none; padding-bottom: 0; }
.clash-history-text { flex: 1; min-width: 0; }
/* Same fix as .clash-comment-meta .event-actor-name - the global .event-actor-name rule uses
   --text-primary (the main site's theme-dependent text color), which is near-black in light mode
   and unreadable against the lightbox's always-dark surface. */
.clash-history-text .event-actor-name { color: var(--lb-text); }
.clash-history-when { flex-shrink: 0; font-size: var(--text-2xs); color: var(--lb-text-secondary); white-space: nowrap; }

.lightbox-footer {
    flex-shrink: 0; display: flex; align-items: center; gap: var(--space-3);
    padding: 12px 18px; border-top: 1px solid var(--lb-border); flex-wrap: wrap;
}
/* flex:1 makes this the one element that stretches to soak up leftover width - the details button
   and the nav group (both flex-shrink:0, never grow) end up at a stable distance from the right
   edge no matter how long the id/date text gets, instead of everything redistributing around a
   `justify-content: space-between` whenever any sibling's width changes. */
.lightbox-footer-meta { flex: 1 1 auto; min-width: 0; display: flex; gap: var(--space-4); flex-wrap: wrap; }
.lightbox-meta-item { display: inline-flex; align-items: center; gap: 5px; font-size: var(--text-2xs); color: var(--lb-text-secondary); }
.lightbox-meta-item svg { opacity: 0.85; }
/* Pinned to the bottom-right corner - last in source order (see index.html) with flex-shrink:0 and
   nothing after it, so it never wobbles left/right as the clash you're viewing changes. */
.lightbox-footer-nav { flex-shrink: 0; display: flex; align-items: center; gap: var(--space-3); }
.lightbox-counter { font-size: var(--text-xs); font-weight: 600; color: var(--lb-text-secondary); min-width: 56px; text-align: center; font-variant-numeric: tabular-nums; }
.lightbox-details-btn {
    flex-shrink: 0; border: none; border-radius: var(--radius-md); padding: 8px 16px;
    font-size: var(--text-xs); font-weight: 700; cursor: pointer;
    background: var(--lb-surface); color: var(--lb-text);
}
.lightbox-details-btn:hover { background: var(--lb-surface-hover); }

/* ================================================================================================
   FILES TAB
   ================================================================================================ */
/* Real upload progress (see the xhr.upload 'progress' handler in app.js), not a fake animation -
   the fill's width is set directly from bytes-loaded/bytes-total on every progress event. */
.upload-progress { margin-top: var(--space-2); }
.upload-progress-label { font-size: var(--text-xs); color: var(--text-secondary); margin-bottom: 4px; }
.upload-progress-label strong { color: var(--text-primary); font-weight: 700; }
.upload-progress-track { height: 8px; border-radius: var(--radius-pill); background: var(--track-bg); overflow: hidden; }
.upload-progress-fill { height: 100%; background: var(--brand-red-action); border-radius: var(--radius-pill); transition: width 0.15s ease; }
.upload-progress-meta { font-size: var(--text-2xs); color: var(--text-secondary); margin-top: 4px; font-variant-numeric: tabular-nums; }

.file-week-heading {
    font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--text-secondary); padding: var(--space-2) var(--space-1) 4px var(--space-1);
}
.file-week-heading:not(:first-child) { margin-top: var(--space-2); border-top: 1px solid var(--border); padding-top: var(--space-3); }

.file-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-2) var(--space-1); border-bottom: 1px solid var(--border);
}
.file-row:last-child { border-bottom: none; }
.file-row:hover { background: var(--card-bg-inner); }
.file-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; text-decoration: none; }
.file-name { font-size: var(--text-xs); font-weight: 600; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-info:hover .file-name { text-decoration: underline; color: var(--brand-red-action); }
.file-meta { font-size: var(--text-2xs); color: var(--text-secondary); }
.file-row .delete-btn { flex-shrink: 0; }

/* ================================================================================================
   COORDINATION MEETINGS
   ================================================================================================ */
#create-meeting-form { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-3); }
/* Same [hidden]-beaten-by-display:flex gotcha as .meeting-add-item-row below - without this,
   toggling the form closed (the "+ New Meeting" button, and the auto-close after a successful
   create) silently does nothing. */
#create-meeting-form[hidden] { display: none; }
#create-meeting-form input[type="text"] { flex: 1 1 320px; }
#create-meeting-form input[type="date"] { flex: 0 0 auto; }

#meetings-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--space-3); }
.meeting-card {
    text-align: left; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-md);
    padding: var(--space-3); cursor: pointer; box-shadow: var(--shadow-sm); display: flex; flex-direction: column; gap: 4px;
    font-family: inherit; transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.meeting-card:hover { box-shadow: var(--shadow-md); border-color: var(--border-strong); }
.meeting-card-title { font-size: var(--text-lg); font-weight: 700; color: var(--text-heading); }
.meeting-card-stats { display: flex; gap: var(--space-3); margin-top: 4px; }

#meeting-detail-card h2 { margin: var(--space-3) 0 2px; font-size: var(--text-xl); }

/* Attendee picker - see renderMeetingAttendees in app.js. Deliberately NOT a reskinned
   .trade-chip: that class (see the lightbox trade chips below) is built on the lightbox's dark
   --lb-* theater tokens, and every use of it on a light surface (.bulk-trade-bar .trade-chip)
   has to override several of them one by one. This block sits on a normal card, so it's built
   directly on the normal light/dark-aware surface tokens with nothing to override. */
.meeting-attendees-block { display: flex; flex-direction: column; gap: 8px; margin: var(--space-4) 0; }
/* display:flex above beats the native [hidden] UA rule with no override present - same recurring
   gotcha as .meeting-add-item-row/#create-meeting-form elsewhere in this file. */
.meeting-attendees-block[hidden] { display: none; }
.meeting-attendees-head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
#meeting-attendee-search { flex: 0 1 240px; margin-left: auto; }

.meeting-attendee-group { margin-bottom: var(--space-3); }
.meeting-attendee-group:last-child { margin-bottom: 0; }
.meeting-attendee-group-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 6px; flex-wrap: wrap; }
.meeting-attendee-group-actions { display: flex; gap: var(--space-2); margin-left: auto; }
.meeting-attendee-group-other {
    font-size: var(--text-2xs); font-weight: 700; color: var(--text-secondary);
    text-transform: uppercase; letter-spacing: 0.04em;
}
.meeting-attendee-chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* Unselected: visible but subdued (opacity, not hidden) - the spec's own "unselected users
   should remain visible but visually subdued" requirement. Selected: full opacity + a real
   border, not a solid fill - a solid fill would fight the company-colored initials circle
   sitting inside the chip. */
.meeting-attendee-chip {
    display: inline-flex; align-items: center; gap: 7px;
    border: 1px solid var(--border); background: var(--card-bg-inner); color: var(--text-secondary);
    border-radius: var(--radius-pill); padding: 4px 12px 4px 4px;
    font-family: inherit; font-size: var(--text-xs); font-weight: 600; cursor: pointer;
    opacity: 0.55; transition: opacity 0.12s ease, background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.meeting-attendee-chip:hover:not(:disabled) { opacity: 0.85; border-color: var(--border-strong); }
.meeting-attendee-chip.selected { opacity: 1; background: var(--card-bg); border-color: var(--brand-red-action); color: var(--text-primary); }
.meeting-attendee-chip.is-locked { cursor: not-allowed; }
.meeting-attendee-off-project { font-size: var(--text-2xs); font-style: italic; color: var(--text-secondary); }

/* Initials circle - color comes from one of the .company-pill-N classes (see companyColorClass/
   attendeeCompanyClass in app.js), so a person's circle matches their company pill everywhere
   else this app already shows one (comments, activity feed, history). No photo/avatar idiom -
   the only real avatar anywhere in this app is the signed-in user's own header/My Account one. */
.meeting-attendee-initials {
    width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.02em;
}
.meeting-attendee-initials--none { background: var(--border-strong); color: var(--text-secondary); }
@media (prefers-reduced-motion: reduce) {
    .meeting-attendee-chip { transition: none; }
}

.meeting-notes-block { display: flex; flex-direction: column; gap: 6px; margin: var(--space-4) 0; }
.meeting-notes-block textarea {
    width: 100%; min-height: 70px; resize: vertical; font-family: inherit;
}
.meeting-notes-block button { align-self: flex-end; margin-top: 0; }

.meeting-add-item-row { display: flex; gap: var(--space-2); flex-wrap: wrap; margin: var(--space-3) 0; }
/* display:flex above beats the native [hidden] UA rule with no override present - same recurring
   gotcha as #nav-rail .nav-item[hidden]/.project-edit-panel[hidden] elsewhere in this file. Without
   this, hiding the add-item row for a non-Coordinator role (see renderMeetingDetail in app.js) is
   a silent no-op. */
.meeting-add-item-row[hidden] { display: none; }
.meeting-add-item-row select { flex: 1 1 220px; }
.meeting-add-item-row button { flex: 0 0 auto; margin-top: 0; }

.meeting-agenda-item {
    border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-3);
    margin-bottom: var(--space-3); background: var(--card-bg);
}
.meeting-item-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.meeting-item-title-wrap { display: flex; flex-direction: column; gap: 2px; }
.meeting-item-label { font-size: var(--text-md); font-weight: 700; color: var(--text-heading); }
.meeting-item-badges { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.reason-badge {
    display: inline-block; border-radius: var(--radius-pill); padding: 2px 9px;
    font-size: var(--text-2xs); font-weight: 700; background: var(--card-bg-inner); color: var(--text-secondary);
}
.meeting-discussed-label {
    display: flex; align-items: center; gap: 6px; font-size: var(--text-xs); font-weight: 600;
    color: var(--text-secondary); margin: var(--space-2) 0; cursor: pointer;
}
.meeting-discussed-label input { width: 15px; height: 15px; accent-color: var(--brand-red-action); cursor: pointer; }
.meeting-item-fields { display: flex; flex-direction: column; gap: 4px; }
.meeting-item-fields label { font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--text-secondary); margin-top: 4px; }
.meeting-item-fields textarea { width: 100%; min-height: 48px; resize: vertical; font-family: inherit; }
.meeting-action-row { display: flex; gap: var(--space-2); }
.meeting-action-row input { flex: 1 1 auto; }
.meeting-action-row input.meeting-owner-input { flex: 0 0 160px; }
.meeting-item-remove-btn {
    border: none; background: none; color: var(--text-secondary); font-size: var(--text-2xs); font-weight: 600;
    cursor: pointer; padding: var(--space-2) 0 0; margin-top: var(--space-2);
}
.meeting-item-remove-btn:hover { color: var(--status-red-ink); }

/* ================================================================================================
   ADMIN CONSOLE
   ================================================================================================ */
.admin-subnav { display: flex; gap: 4px; background: var(--card-bg-inner); border-radius: var(--radius-sm); padding: 3px; margin-bottom: var(--space-4); width: fit-content; }
.admin-subnav-item {
    border: none; background: transparent; padding: 8px 16px; border-radius: var(--radius-sm);
    font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); cursor: pointer;
}
.admin-subnav-item.active { background: var(--card-bg); color: var(--brand-red-deep); box-shadow: var(--shadow-sm); }
.admin-subpanel { display: none; }
.admin-subpanel.active { display: block; }

#create-user-form select { width: 100%; margin-bottom: 4px; }

.user-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-1);
    border-bottom: 1px solid var(--border);
    font-size: var(--text-xs);
}
.user-row:last-child { border-bottom: none; }
.user-row:hover { background: var(--card-bg-inner); }
.user-row .u-name { font-weight: 600; flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.user-row .u-name .u-username { display: block; font-weight: 400; color: var(--text-secondary); font-size: var(--text-2xs); overflow: hidden; text-overflow: ellipsis; }
.user-row select { width: 150px; flex: 0 0 auto; padding: 4px 6px; font-size: var(--text-2xs); }
.user-row .u-company-input { width: 150px; flex: 0 0 auto; padding: 4px 6px; font-size: var(--text-2xs); }
.user-row .badge-toggle, .user-row .link-btn { flex-shrink: 0; }
.user-row .u-inactive { opacity: 0.5; }

/* Dashed border (vs. .badge-status's solid, undecorated pill) is the deliberate "this one's a
   control, not a label" cue - both are colored rounded chips at a glance, but this is a real
   <button> that flips a user's Active/Project status on click (see refreshUserList/refreshProjectsTab
   in app.js), and previously had zero hover/focus feedback despite the pointer cursor. Dashed border
   is a common, low-noise convention for "click to change this" that doesn't need an icon or extra
   color to read correctly next to a purely decorative .badge-status pill. */
.badge-toggle {
    border: 1px dashed var(--border-strong); border-radius: var(--radius-pill); background: var(--card-bg-inner);
    font-size: var(--text-2xs); padding: 4px 10px; cursor: pointer; font-weight: 600;
    transition: box-shadow 0.12s ease, transform 0.12s ease;
}
.badge-toggle:hover { box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.badge-toggle:focus-visible { outline: 2px solid var(--brand-red-bright); outline-offset: 2px; }
.badge-toggle.active { color: var(--status-green-ink); border-color: var(--status-green-ink); background: var(--status-green-bg); }
.badge-toggle.inactive { color: var(--status-red-ink); border-color: var(--status-red-ink); }
/* Neutral grey variant - used for a Project's Active/Inactive toggle (dormant, not "blocked" the
   way a deactivated user account is, so red would be the wrong tone here). */
.badge-toggle.gray { color: var(--status-gray-ink); border-color: var(--border-strong); background: var(--status-gray-bg); }

/* Status + Edit toggle row for a Project (Admin > Projects) - Delete now lives inside the edit
   panel itself (see .project-danger-zone below), not out here where it used to always be visible. */
.project-actions-row { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-2); }

/* Inline "Edit" panel for a Project's name/description (Admin > Projects) - a flex column with a
   real gap between fields (previously plain block flow, so Name/Description/Save all sat jammed
   directly against each other) and full-width inputs (previously the browser's own tiny default
   size for both the name field and the description textarea - the textarea in particular was too
   short AND too narrow to read more than a line or two of its own content without scrolling). */
.project-edit-panel {
    display: flex; flex-direction: column; gap: 4px;
    background: var(--card-bg-inner); border-radius: var(--radius-sm);
    padding: var(--space-3); margin-top: var(--space-2);
}
/* Author-origin display:flex above always beats the browser's built-in [hidden] default regardless
   of specificity - without this, every project's edit panel rendered permanently open (the "Edit"
   button's own toggle became a no-op), the exact regression this same gotcha has caused elsewhere
   in this app whenever a togglable panel gets a `display` property without this override too. */
.project-edit-panel[hidden] { display: none; }
.project-edit-panel input, .project-edit-panel textarea { width: 100%; }
.project-edit-panel textarea { resize: vertical; min-height: 80px; line-height: 1.5; }
.project-edit-panel .field-label:not(:first-child) { margin-top: var(--space-2); }
/* Save pushed to the far right, on its own row below the fields, rather than sitting directly
   against the (now much taller) description textarea. */
.project-edit-actions { display: flex; justify-content: flex-end; margin-top: var(--space-2); }
.project-edit-actions .btn-primary { margin-top: 0; }
/* Delete Project - bottom-right corner of the (collapsible) edit panel, same place Save sits, but
   a border-top plus its own extra spacing keeps it from reading as part of that primary-action
   cluster - a destructive action should never be one careless click away from Save. */
.project-danger-zone { display: flex; justify-content: flex-end; margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--border); }

/* Checkbox-style assignment row (Manage Project -> Assigned Users) */
.assign-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-1); border-bottom: 1px solid var(--border); }
.assign-row:last-child { border-bottom: none; }
.assign-row:hover { background: var(--card-bg-inner); }
.assign-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--brand-red-action); cursor: pointer; }
.assign-row label { flex: 1 1 auto; font-size: var(--text-xs); font-weight: 600; cursor: pointer; }
.assign-row .u-username { font-weight: 400; color: var(--text-secondary); margin-left: 4px; }
.assign-row .assign-saved { font-size: var(--text-2xs); color: var(--status-green-ink); font-weight: 600; opacity: 0; transition: opacity 0.2s ease; }
.assign-row .assign-saved.show { opacity: 1; }

.project-users-panel { background: var(--card-bg-inner); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); margin-top: var(--space-2); }
.assigned-users-heading { font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); padding: 2px 4px 6px; }

.guest-hint { font-size: var(--text-2xs); color: var(--brand-red-secondary); margin-top: 4px; }

.banner { background: var(--status-amber-bg); color: var(--status-amber-ink); border-bottom: 1px solid #FCD34D; padding: var(--space-2) var(--space-5); font-size: var(--text-sm); }
.banner code { background: rgba(0, 0, 0, 0.08); padding: 1px 4px; border-radius: var(--radius-xs); }

/* ================================================================================================
   BIM/VDC COORDINATION TIMELINE (Timeline tab), Increment 1 - Foundation
   Project -> Schedule Activity - see Controllers/TimelineController.cs, renderGanttTimeline/
   openTimelineImportModal in app.js. The Gantt view is hand-rolled (no charting library is loaded
   anywhere in this app - see the plan's own architecture audit), built on the same design tokens
   as everything else rather than a new palette. Every color reused below (status/priority/company
   tokens) already exists; nothing new was introduced except .badge-status--purple above.
   ================================================================================================ */
.timeline-zoom-toggle { display: flex; background: var(--card-bg-inner); border-radius: var(--radius-pill); padding: 3px; gap: 2px; }
.timeline-zoom-toggle button {
    border: none; background: transparent; padding: 6px 14px; border-radius: var(--radius-pill);
    font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); cursor: pointer; font-family: inherit;
}
.timeline-zoom-toggle button.active { background: var(--card-bg); color: var(--text-primary); box-shadow: var(--shadow-sm); }
/* Same fix .clash-grid-actions's own button ids already need (see that rule's comment) - the
   shared .btn-primary/.btn-secondary rule's margin-top:var(--space-3) is meant for a button
   sitting below a form/paragraph; left in place here it pushed these two down out of vertical
   center with .timeline-zoom-toggle, the one sibling in this row that ISN'T a .btn-primary/
   .btn-secondary and so never had that margin to begin with. */
#timeline-add-activity-toggle-btn, #timeline-import-btn, #timeline-export-btn { margin-top: 0; }
/* #timeline-export-btn is an <a>, not a <button> - .btn-secondary's own rules never set `display`
   (a real button doesn't need it), and an <a> defaults to inline, which drops the padding/sizing a
   button gets. Same fix already needed for #gallery-csv-export-btn/#gallery-report-btn. */
#timeline-export-btn { display: inline-flex; align-items: center; text-decoration: none; }

.timeline-form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-2) var(--space-3); margin-top: var(--space-3); }
/* display:flex above (via #timeline-activity-form, not this inner grid) beats the native [hidden]
   UA rule with no override present - same recurring gotcha as #create-meeting-form/
   .meeting-add-item-row elsewhere in this file. */
#timeline-activity-form { display: flex; flex-direction: column; margin-top: var(--space-2); }
#timeline-activity-form[hidden] { display: none; }
.timeline-milestone-check { display: flex; align-items: center; gap: 6px; font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); cursor: pointer; }
.timeline-milestone-check input { width: 16px; height: 16px; accent-color: var(--brand-red-action); cursor: pointer; }

#timeline-gantt { margin-top: var(--space-4); }

/* Two side-by-side scroll panels, kept in sync vertically by app.js - the list's own header and
   the ruler below both use position:sticky so they stay pinned to the top of their shared
   vertical scroll while the timeline panel's horizontal scroll (Week/Month zoom can make the
   track much wider than the card) stays completely independent of the list. */
.gantt-shell { display: flex; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
/* Widened from an earlier 320px (found from a live render: at 320px the Details column had barely
   ~65px to work with, forcing "Behan Bros., Inc."/"ABC Mechanical" to wrap across 2-3 cramped
   lines instead of reading as one line) then 400px (found from a second live render: an 84px
   Status column was still too narrow for the status <select>'s own text - "Auto (Behind
   Schedule)"-style labels rendered raggedly truncated with no ellipsis at all). 420px total, with
   Status now a fixed 108px (see .timeline-status-select below, plus the shortened option labels
   in app.js), is what actually fits every status word at this font size without truncating. */
.gantt-activity-list { flex: 0 0 420px; border-right: 1px solid var(--border); overflow-y: auto; max-height: 560px; background: var(--card-bg); }
.timeline-list-header {
    position: sticky; top: 0; z-index: 2; display: grid; grid-template-columns: 1.3fr 1.3fr 108px; gap: var(--space-2);
    padding: var(--space-2) var(--space-3); background: var(--card-bg-inner); border-bottom: 1px solid var(--border);
    font-size: var(--text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary);
}
/* Height bumped from an earlier 36px to 52px, and MUST stay equal to .timeline-row's own height
   below - the two are two independently-scrolled panels rendered in the same sorted order (see
   renderGanttTimeline in app.js), so a mismatch here would silently break the row-by-row alignment
   between a list row and its own Gantt bar/milestone, not just look wrong. The extra height is what
   makes room for the Activity/Details columns to each stack a second line (see
   .timeline-list-name-col and .timeline-list-meta below) instead of forcing everything onto one
   cramped line. */
/* A plain <div>, not a <button> - a coordinator's row holds a real <select> (the status picker
   below), and interactive content isn't legal inside a <button>. role="button"/tabindex="0" (added
   in app.js only for a coordinator, who's the only one with anything to click) restore the same
   click/Enter/Space/focus-outline behavior a real button would give for free. */
.timeline-list-row {
    display: grid; grid-template-columns: 1.3fr 1.3fr 108px; gap: var(--space-2); align-items: center; width: 100%;
    height: 52px; padding: 0 var(--space-3); border: none; border-bottom: 1px solid var(--border); background: var(--card-bg);
    font-family: inherit; text-align: left;
}
.timeline-list-row:last-child { border-bottom: none; }
.timeline-list-row[role="button"] { cursor: pointer; }
.timeline-list-row[role="button"]:hover { background: var(--card-bg-inner); }
.timeline-list-row[role="button"]:focus-visible { outline: 2px solid var(--brand-red-bright); outline-offset: -2px; }
/* Activity ID + Name stack above the MILESTONE tag (its own line) rather than sharing one row with
   it - crowding all three onto one line left the name almost no room to truncate gracefully (a
   real name could render as little as one or two letters before "..."). */
.timeline-list-name-col { display: flex; flex-direction: column; justify-content: center; gap: 3px; min-width: 0; height: 100%; }
.timeline-list-name-row { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.timeline-list-code { font-size: var(--text-2xs); color: var(--text-secondary); font-weight: 700; flex-shrink: 0; }
/* flex:1 1 auto + min-width:0 together are what actually let text-overflow:ellipsis engage inside
   a flex row - a flex item's default min-width is auto (effectively "never shrink below my own
   content width"), which silently defeats ellipsis truncation entirely without this. */
.timeline-list-name { flex: 1 1 auto; min-width: 0; font-size: var(--text-xs); font-weight: 600; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.timeline-list-milestone-tag {
    align-self: flex-start; flex-shrink: 0; font-size: 9px; font-weight: 800; letter-spacing: 0.05em; color: var(--status-purple-ink);
    background: var(--status-purple-bg); border-radius: var(--radius-xs); padding: 1px 5px;
}
/* Organization pill above Location, each its own line and each truncated with an ellipsis rather
   than left to wrap inside the pill - see the two rules right below for the actual truncation
   (the earlier version only ever applied nowrap/ellipsis to .text-meta, never to the org pill
   itself, which is exactly what let a multi-word company name wrap across 2-3 lines). */
.timeline-list-meta { display: flex; flex-direction: column; justify-content: center; gap: 3px; min-width: 0; height: 100%; overflow: hidden; }
/* Plain text, not a colored company pill (found from direct feedback on a live render: the hash-
   based company-pill color used elsewhere in this app - comments, activity feed, meeting
   attendees - has no meaning of its own; it's just a consistent per-name identifier there. Sitting
   directly next to the Gantt's own deliberate, semantic status colors (gray/blue/green/amber/red),
   an unrelated, meaningless color on the SAME row read as arbitrary rather than as useful
   signal. Status is the one color system this view actually needs - organization stays plain,
   slightly bolder text instead, still visually distinct from Location right below it.) */
.timeline-list-org { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); font-weight: 600; color: var(--text-primary); }
.timeline-list-meta .text-meta { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The status cell for a coordinator - a real <select>, styled to still read as a colored status
   pill (same .badge-status--* background/ink pairing the read-only badge uses) rather than a
   generic form control, so "this is the same kind of thing" is visually obvious. Its own color
   always reflects the EFFECTIVE status (see the "Auto (<computed>)" first option) regardless of
   whether that's the automatic guess or a manual override - see setTimelineActivityStatus in
   app.js, which re-renders from the server's own response after every change. */
/* Kept at native appearance (no custom dropdown-arrow background-image) on purpose - a colored
   arrow drawn as a fixed hex value would be illegible against whichever status's background
   happens to be dark in dark mode (badge-status--* swaps to dark backgrounds with light ink under
   dark mode - see main.css's own dark-mode token block), and an inline SVG data-URI's stroke
   can't reliably pick up currentColor the way a real DOM element can. The browser's own native
   arrow always has correct contrast for its own OS theme instead. */
.timeline-status-select {
    justify-self: start; max-width: 100%; border: none; border-radius: var(--radius-pill); padding: 3px 8px;
    font-size: var(--text-2xs); font-weight: 700; cursor: pointer; font-family: inherit;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.timeline-status-select:focus-visible { outline: 2px solid var(--brand-red-bright); outline-offset: 1px; }
/* Belt-and-suspenders for the read-only badge (a non-coordinator's view) - .badge-status already
   sets white-space:nowrap, but on its own that just overflows the fixed-width Status column
   instead of wrapping; this makes an unusually long value ("Behind Schedule") truncate the same
   graceful way the select above does, in the one place (a real construction schedule) this could
   plausibly be tight. */
.timeline-list-row .badge-status { max-width: 100%; overflow: hidden; text-overflow: ellipsis; display: inline-block; }

.gantt-timeline { flex: 1 1 auto; overflow: auto; max-height: 560px; position: relative; }
.gantt-ruler { position: sticky; top: 0; z-index: 2; height: 32px; background: var(--card-bg-inner); border-bottom: 1px solid var(--border); }
.gantt-ruler-col { position: absolute; top: 0; bottom: 0; border-left: 1px solid var(--border); padding-left: 6px; display: flex; align-items: center; }
.gantt-ruler-line { display: none; }
.gantt-ruler-label { font-size: var(--text-2xs); font-weight: 600; color: var(--text-secondary); white-space: nowrap; }
.gantt-body { position: relative; }
/* Must stay equal to .timeline-list-row's own height above - see that rule's own comment. */
.timeline-row { position: relative; height: 52px; border-bottom: 1px solid var(--border); }
.timeline-row:last-child { border-bottom: none; }
.gantt-today-line { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--brand-red-bright); z-index: 1; }

.timeline-bar {
    position: absolute; top: 50%; transform: translateY(-50%); height: 16px; border-radius: var(--radius-xs);
    min-width: 8px;
}
.timeline-bar--not-started { background: var(--status-gray-ink); opacity: 0.55; }
.timeline-bar--in-progress { background: var(--status-blue-ink); }
.timeline-bar--complete { background: var(--status-green-ink); }
/* A coordinator's manual correction (Behind Schedule/At Risk -> amber, Overdue/Blocked -> red) -
   these two never come from the automatic date-only guess (see ComputeAutoStatus in
   DashboardStateService.cs), only from an explicit override, so they're deliberately the two most
   attention-grabbing colors already in the status palette. */
.timeline-bar--behind { background: var(--status-amber-ink); }
.timeline-bar--overdue { background: var(--status-red-ink); }

/* Milestone - a rotated square (diamond), not a bar, so it's unmistakably a point in time rather
   than a duration, per the spec's own "milestones should be visually distinct" requirement.
   Hollow until Reached (today's date has passed the milestone), then fills solid. */
.timeline-milestone-marker {
    position: absolute; top: 50%; left: 0; width: 14px; height: 14px; margin-left: -7px;
    transform: translateY(-50%) rotate(45deg); border: 2px solid var(--status-purple-ink); background: var(--card-bg);
    border-radius: 2px;
}
.timeline-milestone-marker.is-reached { background: var(--status-purple-ink); }
/* A coordinator's manual correction on a milestone (see timelineMilestoneStatusClass in app.js) -
   same amber/red split as the bar modifiers above, just applied to the diamond's border+fill
   instead of a solid rectangle. Takes precedence visually over .is-reached by simply being a
   different, more specific color - a milestone that's both "reached" and separately marked
   Blocked is the rarer, more informative case (something was hit on time but the sign-off itself
   stalled), so its border color is what should stand out. */
.timeline-milestone-marker.is-caution { border-color: var(--status-amber-ink); }
.timeline-milestone-marker.is-caution.is-reached { background: var(--status-amber-ink); }
.timeline-milestone-marker.is-serious { border-color: var(--status-red-ink); }
.timeline-milestone-marker.is-serious.is-reached { background: var(--status-red-ink); }

/* ---- Import Schedule wizard ---- */
.timeline-wizard-card { width: 640px; padding: 28px; }
.timeline-wizard-step .hint { margin-bottom: var(--space-3); }
#timeline-upload-form { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
#timeline-upload-form input[type="file"] { flex: 1 1 240px; }

.timeline-map-rows { display: flex; flex-direction: column; gap: var(--space-2); max-height: 360px; overflow-y: auto; margin: var(--space-2) 0; }
.timeline-map-row {
    display: grid; grid-template-columns: 1fr 24px 1fr; align-items: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-3); background: var(--card-bg-inner); border-radius: var(--radius-sm);
}
.timeline-map-source { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.timeline-map-source-name { font-size: var(--text-xs); font-weight: 700; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.timeline-map-arrow { text-align: center; color: var(--text-secondary); }

.timeline-preview-summary { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3); }
.timeline-preview-rows { display: flex; flex-direction: column; gap: var(--space-2); max-height: 360px; overflow-y: auto; }
.timeline-preview-row { padding: var(--space-2) var(--space-3); background: var(--card-bg-inner); border-radius: var(--radius-sm); border-left: 3px solid var(--status-green-ink); }
.timeline-preview-row.has-error { border-left-color: var(--status-red-ink); }
.timeline-preview-row-head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; font-size: var(--text-xs); }
.timeline-preview-row-issues { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.timeline-row-issue { font-size: var(--text-2xs); }
.timeline-row-issue--error { color: var(--status-red-ink); }
.timeline-row-issue--warning { color: var(--status-amber-ink); }

@media (max-width: 720px) {
    .gantt-shell { flex-direction: column; }
    .gantt-activity-list { flex: none; max-height: 260px; border-right: none; border-bottom: 1px solid var(--border); }
}

/* ================================================================================================
   LOGIN / MODALS
   ================================================================================================ */
.login-overlay { position: fixed; inset: 0; background: rgba(16, 17, 20, 0.55); display: flex; align-items: center; justify-content: center; z-index: 100; padding: var(--space-4); }
.login-overlay[hidden] { display: none; }

.login-card { position: relative; background: var(--card-bg); border-radius: var(--radius-lg); padding: 32px 28px 26px; width: 320px; max-width: 90vw; box-shadow: var(--shadow-lg); }
.modal-close { position: absolute; top: 10px; right: 12px; background: none; border: none; font-size: 20px; line-height: 1; color: var(--text-secondary); cursor: pointer; padding: 4px; }
.modal-close:hover { color: var(--text-primary); }
/* Width-based (not height:32px) - same reasoning as .rail-logo above: the Collision Suite
   wordmark is ~11:1, and height-based sizing would push well past .login-card's own content
   width (320px card - 2*28px padding). 200px comfortably fits with margin either side. */
.login-logo { width: 200px; height: auto; margin-bottom: var(--space-2); }
.login-card h2 { margin: 4px 0 var(--space-2) 0; font-size: var(--text-md); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-heading); }
.login-card input { width: 100%; margin-bottom: var(--space-2); }
.login-error { background: var(--status-red-bg); color: var(--status-red-ink); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); font-size: var(--text-xs); margin: 4px 0 var(--space-2) 0; }

/* Snapshots tab's full-size viewer (see openSnapshotLightbox in app.js) - built on the same
   .login-overlay backdrop/.modal-close every other modal in this app uses; only this content box
   is bespoke, sized for a single image rather than a form. */
.snapshot-lightbox-content {
    position: relative; background: var(--card-bg); border-radius: var(--radius-lg);
    padding: var(--space-4); width: min(92vw, 900px); max-height: 90vh; overflow-y: auto;
    display: flex; flex-direction: column; align-items: stretch; box-shadow: var(--shadow-lg);
}
.snapshot-lightbox-content img { max-width: 100%; max-height: 50vh; border-radius: var(--radius-md); object-fit: contain; align-self: center; }
.snapshot-lightbox-caption { margin-top: var(--space-3); text-align: center; color: var(--text-primary); }
.snapshot-lightbox-caption span { display: block; margin-top: 2px; font-size: var(--text-xs); color: var(--text-secondary); }

/* The editable fields panel below the image (Caption/Location/Notes/Status/Priority/Assignee/Due
   Date/Group) - see openSnapshotLightbox in app.js. Same label-wraps-input idiom used throughout
   this app's other forms, just laid out in a simple stack/two-column grid rather than a full form
   element (each field saves independently on change, there's no single Submit). */
.snapshot-lightbox-fields { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-4); }
.snapshot-lightbox-fields label { display: flex; flex-direction: column; gap: 4px; font-size: var(--text-xs); font-weight: 600; color: var(--text-secondary); }
.snapshot-lightbox-fields input, .snapshot-lightbox-fields select, .snapshot-lightbox-fields textarea { font-weight: 400; color: var(--text-primary); }
.snapshot-lightbox-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }

.snapshot-lightbox-comments { margin-top: var(--space-4); border-top: 1px solid var(--border); padding-top: var(--space-3); }
.snapshot-lightbox-comments h4 { margin: 0 0 var(--space-2) 0; font-size: var(--text-sm); color: var(--text-heading); }
.snapshot-comment { padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
.snapshot-comment:last-of-type { border-bottom: none; }
.snapshot-comment-meta { font-size: var(--text-2xs); color: var(--text-secondary); display: flex; align-items: center; gap: 6px; }
.snapshot-comment-text { margin-top: 2px; font-size: var(--text-sm); color: var(--text-primary); white-space: pre-wrap; }
.snapshot-lightbox-comments textarea { width: 100%; margin-top: var(--space-2); }
.snapshot-lightbox-comments .btn-secondary { margin-top: var(--space-2); }

/* Replaces the browser's native confirm() (see #confirm-modal in index.html / confirmDialog in
   app.js) - narrower than .login-card (no logo/form to fit). #confirm-modal itself (the overlay,
   not just this inner card) needs its OWN higher z-index, not the card's - it can be triggered
   from inside another already-open overlay (e.g. deleting a comment from the clash lightbox, which
   is also a .login-overlay at the same z-index:100), and a child's z-index only ranks it among its
   own siblings, never above a sibling overlay entirely outside its stacking context. */
#confirm-modal { z-index: 200; }
.confirm-card { width: 340px; padding: 24px; }
.confirm-message { margin: 0 0 var(--space-4) 0; font-size: var(--text-sm); color: var(--text-primary); line-height: 1.5; }
.confirm-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); }
/* The OK button is .btn-danger for a destructive confirm, .btn-primary for a non-destructive one
   (see confirmDialog's `danger` option in app.js) - both need the same margin-top:0 override as
   Cancel (.btn-secondary), or whichever one's missing keeps the shared .btn-primary/.btn-secondary/
   etc. rule's margin-top:var(--space-3) (meant for a button below a form) and renders visibly
   taller/offset from its sibling in this horizontal row. */
.confirm-actions .btn-secondary, .confirm-actions .btn-danger, .confirm-actions .btn-primary { margin-top: 0; }
/* Explicit matching box for both buttons - .btn-secondary's 1px border (Cancel) vs .btn-primary/
   .btn-danger's none (OK), plus "Cancel" vs a shorter OK label like "Apply", would otherwise still
   render at two slightly different sizes even with the margin-top fix above. */
.confirm-actions button { min-width: 84px; height: 40px; display: inline-flex; align-items: center; justify-content: center; }

/* ================================================================================================
   COMMAND PALETTE (Phase 10)
   Ctrl/Cmd+K from anywhere - see the global keydown listener in app.js. Its own backdrop rule
   rather than reusing .login-overlay: top-anchored (align-items:flex-start + a top padding), not
   centered - the established convention for a quick-jump tool like this (Linear/GitHub/VS Code
   all anchor theirs near the top, not dead center, since it's a fast in-and-out action, not a
   form to focus on for a while).
   ================================================================================================ */
#command-palette {
    position: fixed; inset: 0; background: rgba(16, 17, 20, 0.55);
    display: flex; align-items: flex-start; justify-content: center;
    padding: 8vh var(--space-4) var(--space-4); z-index: 100;
}
#command-palette[hidden] { display: none; }
.command-palette-card {
    background: var(--card-bg); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
    width: 560px; max-width: 100%; max-height: 70vh;
    display: flex; flex-direction: column; overflow: hidden;
}
.command-palette-input-row {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-4); border-bottom: 1px solid var(--border);
    color: var(--text-secondary); flex-shrink: 0;
}
.command-palette-input-row svg { flex-shrink: 0; }
#command-palette-input {
    flex: 1; border: none; background: none; font: inherit; font-size: var(--text-md);
    color: var(--text-primary); padding: 0; margin: 0;
}
#command-palette-input:focus { outline: none; box-shadow: none; }
.command-palette-esc {
    font: inherit; font-size: var(--text-2xs); color: var(--text-secondary); background: var(--card-bg-inner);
    border: 1px solid var(--border); border-radius: var(--radius-xs); padding: 2px 6px; flex-shrink: 0;
}
.command-palette-results { overflow-y: auto; padding: var(--space-2); }
.command-palette-group-label {
    font-size: var(--text-2xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-secondary); padding: var(--space-3) var(--space-3) 4px;
}
/* Reset button chrome (same idiom as .bar-col/.mini-stat-link elsewhere) - a real <button>, not a
   div with a click handler, for free keyboard/screen-reader semantics. */
.command-palette-item {
    display: flex; align-items: center; gap: var(--space-3); width: 100%; text-align: left;
    background: none; border: none; border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3);
    font: inherit; font-size: var(--text-sm); color: var(--text-primary); cursor: pointer;
}
.command-palette-item svg { flex-shrink: 0; color: var(--text-secondary); }
.command-palette-item-sub {
    color: var(--text-secondary); font-size: var(--text-xs); margin-left: auto; flex-shrink: 0;
    padding-left: var(--space-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* .is-active - keyboard-highlighted (Arrow Up/Down), same convention as .fullscreenBtn's own
   is-active toggle elsewhere - :hover gets the identical look so mouse and keyboard selection
   always read the same way. */
.command-palette-item:hover, .command-palette-item.is-active { background: var(--card-bg-inner); }
.command-palette-item.is-active svg { color: var(--brand-red-bright); }
.command-palette-empty { padding: var(--space-5) var(--space-4); text-align: center; color: var(--text-secondary); font-size: var(--text-sm); }

/* ================================================================================================
   3D VIEWER
   ================================================================================================ */
.viewer-layout { display: flex; height: calc(100vh - 165px); min-height: 400px; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; box-shadow: var(--shadow-sm); }
#viewer-sidebar { width: 260px; flex-shrink: 0; background: var(--card-bg); border-right: 1px solid var(--border); padding: var(--space-4); overflow-y: auto; }
#viewer-sidebar section { margin-bottom: var(--space-5); }
#viewer-sidebar h2 { font-size: var(--text-sm); margin: 0 0 var(--space-2) 0; }
#upload-form { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-2); }
#model-list { list-style: none; padding: 0; margin: var(--space-2) 0 0 0; }
#model-list li { padding: var(--space-2); border-radius: var(--radius-sm); cursor: pointer; font-size: var(--text-sm); }
#model-list li:hover { background: var(--bg); }
#viewer-pane { flex: 1; position: relative; }
#viewer { position: absolute; inset: 0; }

/* ================================================================================================
   HELP TAB (#tab-help) - static content, no data of its own. See index.html's own comment on why
   it's not part of switchTab's per-tab refresh chain in app.js.
   ================================================================================================ */
.help-list { display: flex; flex-direction: column; gap: var(--space-2); }
/* Same accordion recipe as the landing page's FAQ (.landing-faq-item in landing.css) - duplicated
   rather than shared, since main.css and landing.css are two separate stylesheets loaded by two
   separate pages (index.html never loads landing.css). */
.help-item {
    background: var(--card-bg-inner);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-4);
}
.help-item summary {
    cursor: pointer;
    font-weight: 600;
    color: var(--text-heading);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    list-style: none;
}
.help-item summary::-webkit-details-marker { display: none; }
.help-item summary::after {
    content: "+";
    flex: none;
    font-size: var(--text-lg);
    font-weight: 400;
    color: var(--text-secondary);
    transition: transform 0.15s ease;
}
.help-item[open] summary::after { transform: rotate(45deg); }
.help-item p { margin: var(--space-3) 0 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; }

.help-role-row { display: flex; align-items: center; gap: var(--space-3); }
.help-role-row .role-pill { flex: none; width: 130px; text-align: center; }
.help-role-row p { margin: 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; }

.help-kbd-table { width: 100%; border-collapse: collapse; }
.help-kbd-table td { padding: var(--space-3) 0; border-top: 1px solid var(--border); font-size: var(--text-sm); color: var(--text-secondary); vertical-align: top; }
.help-kbd-table tr:first-child td { border-top: none; }
.help-kbd-table td:first-child { white-space: nowrap; width: 1%; padding-right: var(--space-5); }
kbd {
    display: inline-block;
    min-width: 20px;
    padding: 2px 7px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border-strong);
    border-bottom-width: 2px;
    background: var(--card-bg-inner);
    color: var(--text-heading);
    font-family: inherit;
    font-size: var(--text-xs);
    font-weight: 600;
    text-align: center;
}

.help-plugin-list { list-style: none; margin: var(--space-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.help-plugin-list li { font-size: var(--text-sm); line-height: 1.6; color: var(--text-secondary); padding-left: var(--space-5); position: relative; }
.help-plugin-list li::before { content: ""; position: absolute; left: 0; top: 0.55em; width: 6px; height: 6px; border-radius: var(--radius-pill); background: var(--brand-red-action); }
.help-plugin-list strong { color: var(--text-heading); }

/* ================================================================================================
   FOOTER
   ================================================================================================ */
/* Top padding trimmed down from the bottom's - the trademark logo already carries its own visual
   weight right under the divider line, so the full var(--space-5) gap above it (still used below)
   read as too much empty space between the two - per direct user feedback on a live render. */
#site-footer { border-top: 1px solid var(--border); padding: var(--space-3) var(--space-6) var(--space-5); text-align: center; }
/* Trademark lockup leads the footer, tagline underneath it - per direct user feedback on a live
   render. Width-based sizing (~11:1 wordmark, same reasoning as .rail-logo/.login-logo above),
   matched to .landing-footer-logo's own width so the same mark reads at the same size in both
   footers. display:block (not the <img> default of inline) is load-bearing here, not decorative -
   an inline image sits on the text baseline, which leaves several px of descender space
   UNDERNEATH it that margin alone can't remove - exactly the uneven gap (tighter above the logo
   than below it) reported on a live render. Block removes it from that baseline entirely; margin:
   0 auto centers it the normal block way instead of relying on the parent's text-align. */
.footer-trademark-logo { display: block; width: 170px; height: auto; margin: 0 auto; }
/* .footer-tagline (ARCHITECTS/ENGINEERS/FACILITIES SOLUTIONS) removed per direct user feedback -
   just the logo + copyright now, same as .landing-footer-note's own treatment. */
.footer-note { display: block; font-size: var(--text-2xs); color: var(--text-secondary); margin-top: var(--space-3); }

/* ================================================================================================
   RESPONSIVE
   ================================================================================================ */
@media (max-width: 860px) {
    .header-row { padding: var(--space-2) var(--space-4); gap: var(--space-3); flex-wrap: wrap; }
    #content { padding: var(--space-4); }
    .stat-grid, .project-grid { grid-template-columns: 1fr 1fr; }
    .viewer-layout { flex-direction: column; height: auto; }
    #viewer-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
    #viewer-pane { height: 60vh; }
}

/* A fixed-width rail eats a large share of a phone-width screen regardless of what's in it.
   Collapses the Phase 2 labeled sidebar back to a compact icon rail (labels/group headers hidden,
   switcher text hidden but the button itself - and the project context it opens - stays reachable)
   rather than hiding it outright: the app targets desktop, but staying navigable, and keeping
   "which project am I on" answerable, matters more than the saved width. Also gives the
   switcher/notifications popovers an actual narrow-viewport width instead of a fixed 320-340px
   that could run past a phone's own edge. */
@media (max-width: 560px) {
    .stat-grid, .project-grid, #gallery-test-list, .gallery-grid { grid-template-columns: 1fr; }
    .list-toolbar { flex-direction: column; align-items: stretch; }
    /* Phase 13 (Responsive Design) audit finding: .health-metric-row (Coordination Health AND
       Needs Attention - they share this one class) already wraps at narrow width via its own
       base flex-wrap:wrap, but each .health-metric's divider is a border-LEFT with only the
       true first child (:first-child) getting border:none - once the row actually wraps to a
       second/third line, every row's own leading item still carries that left border with
       nothing to its left anymore, reading as a stray orphaned line. Stacking to a single column
       and flipping the divider to border-top sidesteps the problem entirely - there's only ever
       one real "first item" once it's a column, so :first-child stays correct regardless of how
       many metrics are present. */
    .health-metric-row { flex-direction: column; }
    .health-metric { border-left: none; border-top: 1px solid var(--border); padding: var(--space-2) 0; }
    .health-metric:first-child { border-top: none; padding-top: 0; }
    #nav-rail { width: 56px; padding: var(--space-2) var(--space-1); }
    .rail-brand { display: flex; justify-content: center; padding: 0 0 var(--space-2); }
    /* The full wordmark genuinely can't fit this rail width at any legible height (it's an ~11:1
       lockup - even at a bare 20px tall it'd be over 200px wide). Swap to the cropped icon-only
       mark instead of shrinking the wordmark into an unreadable sliver. */
    .rail-logo-full { display: none; }
    .rail-logo-icon { display: block; height: 28px; width: auto; }
    #nav-rail .nav-item { justify-content: center; padding: 9px; }
    #nav-rail .nav-item-label, .nav-group-label { display: none; }
    #nav-rail .nav-item svg { width: 17px; height: 17px; }
    .project-switcher-btn { justify-content: center; padding: 8px; }
    .project-switcher-text { display: none; }
    .project-switcher-panel { left: 4px; width: calc(100vw - 8px); max-width: 320px; }
    .notifications-panel { right: 4px; width: calc(100vw - 8px); max-width: 340px; }
}

/* Field View - the one piece phone-width users actually depend on (open a clash from "My Work",
   view/zoom the image, comment, change status - see Phase 13's own list) that the two breakpoints
   above never touched: the clash lightbox. Its floating "theater" card (min-width: 460px, see
   .lightbox-card above) and its absolutely-positioned 196px controls panel (.lightbox-controls-grid)
   both assume desktop-class width; below this width the lightbox instead goes edge-to-edge and the
   controls drop into normal document flow beneath the image, so nothing overlaps or gets clipped.
   Also stacks the clash-grid filter bar/bulk toolbar (already usable, just cramped at this width).
   Same phone breakpoint as the nav-rail/grid rules above, not a new number. */
@media (max-width: 560px) {
    .lightbox-card {
        position: fixed; inset: 0; width: 100vw; height: 100vh;
        min-width: 0; min-height: 0; max-width: 100vw; max-height: 100vh;
        border-radius: 0; resize: none;
    }
    .lightbox-stage-wrap { flex-direction: column; overflow-y: auto; }
    .lightbox-images { min-height: 45vh; }
    /* Drops out of the image's top-right corner into normal flow below it - a fixed 196px overlay
       would cover roughly half a phone-width image and put every control under a thumb's worth of
       photo instead of beside it. */
    .lightbox-controls-grid {
        position: static; width: auto; flex-direction: row; flex-wrap: wrap;
        backdrop-filter: none; background: var(--lb-surface); border-radius: 0;
        border-left: none; border-right: none;
    }
    .lightbox-control { flex: 1 1 140px; min-width: 120px; }
    .lightbox-header { padding: 14px 14px 10px; }
    .lightbox-footer { flex-wrap: wrap; gap: var(--space-2); }
    .clash-filter-bar { flex-direction: column; align-items: stretch; }
    .clash-filter-select, .clash-filter-input { width: 100%; }
    .bulk-trade-bar { flex-direction: column; align-items: stretch; }
    #gallery-bulk-group-input { width: 100%; }
}
